step 22b: repo context (#11)
* Step 22 B: RepoContext over the current repository A RepoContext bundles a repository's primary checkout with the state that lives inside or is keyed by it (results, artifact store) and carries its name. Today there is exactly one, opened over the current working directory; the result and artifact-store beans now come from it, so nothing else changes yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: the watcher polls a RepoContext start/poll/recoverOnStartup take the context instead of a working directory and read results and artifacts from it; the per-repository poll memory (logged fetch error, deprecation warning, cached branch definitions) moves into a RepoWatch keyed by context, so the next session can iterate contexts without one repository's outage silencing another's. The shared WatcherState is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: the executor runs builds of a RepoContext startBuild takes the context first; builds serialize per (context, branch) and share the global maxConcurrent cap across repositories, results and artifacts go to the build's own context. ConsoleBuildRunner, the build/retry commands and the builds API restart pass the current repository's context along. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: the UI and the branch listing read their RepoContext UiController and BuildsApiController take the current repository's context instead of a settable working directory; BranchListing lists the branches of a context and reads the results from it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: document the RepoContext, PR-doc for PR #11 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
713edf77d6
commit
096cce3659
@@ -61,9 +61,13 @@ Three places must stay in sync when config keys change: the `WerkatorConfig` dat
|
||||
|
||||
`GitService` shells out to the `git` CLI via `GitCommandRunner` (a thin `ProcessBuilder` wrapper; no JGit). Commands that need repo information take it as a constructor dependency so tests can mock it. HTTPS fetches authenticate via a temporary, secret-free `GIT_ASKPASS` script (`GitAskPass`) with credentials from config passed through environment variables.
|
||||
|
||||
## Repository Context
|
||||
|
||||
Everything repository-scoped goes through a `RepoContext` (`repo` package, ADR 0009): the primary checkout (`workingDir`), the repository's `BuildResultRepository` (`.git/werkator/build-results.json`), its `ArtifactStore` (keyed by the repository path), and a short `name` defaulting to the directory basename — the future route segment. `RepoContexts.open(dir)` builds one; `RepoConfiguration` provides the single current-directory context as a bean, and the `BuildResultRepository`/`ArtifactStore` beans are that context's, so code that still injects them sees the same objects. Git access and config loading stay path-based services taking `repo.workingDir`. The context object is the identity (executor pools, watcher memory are keyed by it), so exactly one is opened per repository; the registry of step 22 session C opens one per entry. Not yet repository-scoped and left for that session: `StateDirMigration` (once per process on the cwd), the metrics collector's repository size, `ServerCommand`'s config, and `RunningBuild`, which carries no repository yet.
|
||||
|
||||
## Build Execution
|
||||
|
||||
`BuildExecutor` runs builds asynchronously: up to `executor.maxConcurrent` branches concurrently (default 1), but never more than one build per branch at a time. Each branch builds in its own reusable git worktree at `.git/werkator/worktrees/<branchKey>` (`BranchWorkspaces`), checked out detached at the requested commit — the primary checkout is never used for builds. Status transitions are persisted via `BuildResultRepository` (JSON file under `.git/werkator/`), published to Gitea non-fatally, and emitted as `BuildStatusChangedEvent`s. Every run belongs to a named build definition (job, ADR 0007): the YAML `builds` section defines triggers (`onPush`, `atTimes`), branch selectors (`branches` globs, `activeWithin`), and build-setting overrides applied last over the merged branch config; the implicit `default` build (`onPush`, all branches) preserves the job-less behavior. Definitions are part of the branch layer — a branch may add and override its own, and they apply to that branch alone (its selectors are evaluated for it only) — while `executor.maxConcurrent` stays pinned. `BuildResult.build` records the job; restart, retry, and startup recovery re-run by that name, resolving settings from the *current* config. `BuildResult.name` — the pool, `<branch>@<build>` for non-default builds — keys everything display- and retention-side (repository grouping via `latestPerName`, retention pools, branches-view rows, permanent latest-green links), while `BuildResult.branch` keys everything git-side: origin lookups, gone-from-origin pruning, worktrees (every build runs in its branch's worktree, serialized per branch), and Gitea links/statuses. `branches.*.autoBuild` survives as a deprecated alias for a scheduled default-pool rebuild. Cancellation addresses a build by artifact key and terminates the whole process tree. Future code (watcher, server, UI) must not assume a single running build.
|
||||
`BuildExecutor` runs builds asynchronously: `startBuild(repo, branch, commit, build)` takes the `RepoContext` first; up to `executor.maxConcurrent` builds run concurrently across all repositories (default 1, sized once from the first build's config — an instance-level setting), but never more than one build per (repository, branch) at a time. Each branch builds in its own reusable git worktree at `.git/werkator/worktrees/<branchKey>` (`BranchWorkspaces`), checked out detached at the requested commit — the primary checkout is never used for builds. Status transitions are persisted in the build's own `RepoContext.results` (JSON file under that repository's `.git/werkator/`), published to Gitea non-fatally, and emitted as `BuildStatusChangedEvent`s. Every run belongs to a named build definition (job, ADR 0007): the YAML `builds` section defines triggers (`onPush`, `atTimes`), branch selectors (`branches` globs, `activeWithin`), and build-setting overrides applied last over the merged branch config; the implicit `default` build (`onPush`, all branches) preserves the job-less behavior. Definitions are part of the branch layer — a branch may add and override its own, and they apply to that branch alone (its selectors are evaluated for it only) — while `executor.maxConcurrent` stays pinned. `BuildResult.build` records the job; restart, retry, and startup recovery re-run by that name, resolving settings from the *current* config. `BuildResult.name` — the pool, `<branch>@<build>` for non-default builds — keys everything display- and retention-side (repository grouping via `latestPerName`, retention pools, branches-view rows, permanent latest-green links), while `BuildResult.branch` keys everything git-side: origin lookups, gone-from-origin pruning, worktrees (every build runs in its branch's worktree, serialized per branch), and Gitea links/statuses. `branches.*.autoBuild` survives as a deprecated alias for a scheduled default-pool rebuild. Cancellation addresses a build by artifact key and terminates the whole process tree. Future code (watcher, server, UI) must not assume a single running build.
|
||||
|
||||
On context close (e.g. systemd SIGTERM), a `ContextClosedEvent` listener in `BuildExecutor` terminates the process trees of all executing builds and waits (bounded) until their results are persisted as INTERRUPTED — a shutdown is never recorded as FAILED. Builds still queued stay PENDING and start no process. Both are re-enqueued by the watcher's startup recovery; INTERRUPTED therefore publishes as Gitea state `pending`, not `failure` (`GiteaStateMapping`).
|
||||
|
||||
@@ -73,7 +77,7 @@ The runtime is selected per build behind the `BuildRunner` interface: `Dispatchi
|
||||
|
||||
## Watcher
|
||||
|
||||
`Watcher` replaces the legacy blocking main loop with a non-blocking fixed-delay poll cycle: fetch origin, enqueue due branches (changed local, recent new origin, due auto-build slots) via `BuildExecutor`, then prune results, artifacts, and stale worktrees. Branches whose build has `requirePullRequest` are enqueued only while their head commit matches a pull-request head, detected without an API token by listing `refs/pull/*/head` via `git ls-remote` (lazily, at most once per poll cycle); manual `build` commands bypass this gate, and `watcher.pullRequestGate: false` disables it globally for plain-git origins without pull-request refs. Which builds are due is decided per branch from that branch's own definitions (`definitionsFor`): the primary config with the branch's committed `.werkator.yml` merged on top, cached per branch by its head commit *and* the primary config it was merged with, so the `git show` runs only when the branch moved while an edited machine or project config still takes effect on the next poll, and falling back to the primary definitions when that config is unreadable. Nothing is scheduled until `Watcher.start()` is called explicitly (server/watch mode) — CLI commands and tests never start the loop. "Already built" is tracked via the result repository, not by moving local branch refs.
|
||||
`Watcher` replaces the legacy blocking main loop with a non-blocking fixed-delay poll cycle over a `RepoContext` (`start(repo)`, `poll(repo)`, `recoverOnStartup(repo)`; what it remembers per repository — the logged fetch error, the deprecation warning, the cached branch definitions — lives in a `RepoWatch` keyed by context, while `WatcherState` is still one per instance): fetch origin, enqueue due branches (changed local, recent new origin, due auto-build slots) via `BuildExecutor`, then prune results, artifacts, and stale worktrees. Branches whose build has `requirePullRequest` are enqueued only while their head commit matches a pull-request head, detected without an API token by listing `refs/pull/*/head` via `git ls-remote` (lazily, at most once per poll cycle); manual `build` commands bypass this gate, and `watcher.pullRequestGate: false` disables it globally for plain-git origins without pull-request refs. Which builds are due is decided per branch from that branch's own definitions (`definitionsFor`): the primary config with the branch's committed `.werkator.yml` merged on top, cached per branch by its head commit *and* the primary config it was merged with, so the `git show` runs only when the branch moved while an edited machine or project config still takes effect on the next poll, and falling back to the primary definitions when that config is unreadable. Nothing is scheduled until `Watcher.start()` is called explicitly (server/watch mode) — CLI commands and tests never start the loop. "Already built" is tracked via the result repository, not by moving local branch refs.
|
||||
After the enqueue decision — and only after it, because a local ref lagging behind origin *is* the change signal — the cycle fast-forwards the primary checkout's local branch refs to their origin counterparts (`watcher.fastForwardLocalRefs`, `GitService.fastForwardLocalBranches`), so build tools reading the shared `.git` from a worktree see the refs they expect; diverged or ahead branches are never touched. Auto-build slot state lives in `.git/werkator/auto-builds.json`; watcher health is exposed via `Watcher.state()`.
|
||||
|
||||
## System Metrics
|
||||
|
||||
Reference in New Issue
Block a user