feat(worker): open change requests on GitLab, not only Forgejo #46

Merged
ryangr0 merged 4 commits from gitlab-worker into development 2026-09-02 06:13:18 +00:00
Owner

Why

ploegd has spoken GitLab since rc.31 — pkg/provider/gitlab comments on a merge request and verifies inbound webhooks. ploeg-worker never learned. It polled /api/v1/repos/{owner}/{name}/pulls and its briefing named the Forgejo API as the way to open a change request, so against a GitLab target nothing was ever opened.

That one absence removes most of the review loop. No change request means the Shift has nothing for a reviewing Role to comment on, publishRound logs findings not published: no pull request on this shift yet, no forge webhook can arrive, and close-the-review-loop's fix Rounds never open. Ingest, routing, Rounds and budgets all work — the work simply never leaves the factory. Every step of that is silent: the writing Run reports success.

What was already fine

Three things are forge-agnostic already and are untouched here, which is why this is one change and not three:

  • pkg/worker/git.go builds clone and push URLs from owner and name — already correct for a GitLab subgroup path. Git is git.
  • pkg/shiftengine's prPathRe is /(?:pulls?|merge_requests)/(\d+)/?$ and has matched GitLab all along.
  • pullRequest(reports) takes the Shift's change request from the writing Run's OutcomeReport links. The poll is ground truth for a Run that died before reporting, not the primary path.

The gap was exactly two places that name a forge: the poll, and the briefing.

What changes

  • harness.RepoRef.Forge — the API dialect, additive and optional on taskspec.v1. Empty means forgejo, so every stored Target and every existing deployment keeps its exact meaning. The value travels on the Work Item (work.Target.Forge, there since ADR-0016) and falls back to a deployment default.
  • findPR dispatches. GitLab filters source_branch server-side, so unlike the Forgejo call it cannot be defeated by a repo with more than a page of open requests. The project is addressed by URL-encoded full path — code14nl/internal/poc-silk is three segments and the slashes must survive as %2F.
  • The briefing dispatches, in vocabulary as well as endpoint. An agent told to open a "pull request" against GitLab looks for an endpoint that is not there, and the noun is what it searches its tools and the repo's docs for. changeRequestNoun and openChangeRequest switch on the same value so they cannot drift.
  • An unknown dialect fails loudly. Falling back to Forgejo would poll a real endpoint shape against the wrong host and report "no change request" forever — indistinguishable from an agent that never opened one.
  • Chart: executor.forge selects one active forge, executor.gitlab configures it, and a new ploeg.forge helper resolves whichever is active so no template reads .forgejo or .gitlab directly.

Two things worth a reviewer's attention

A nil pointer removed in passing. ploeg.workerPodTemplate dereferenced .Values.executor.forgejo.url unconditionally, so forgejo: null — the documented way to empty a block whose defaults name Secrets you have no reason to hold — became a nil pointer the moment executor.enabled went true. The new executor-gitlab fixture renders with forgejo: null on purpose, so it cannot come back without failing a golden.

A fourth golden. Selecting a forge changes the worker pod — a different credential Secret, a different API — and the worker pod is the boundary the goldens exist to police. executor-gitlab.yaml pins ADR-0013 tier 1 holding on GitLab:

ploeg-worker-silver-analyst   -> agent-reader-token  / GITLAB_TOKEN
ploeg-worker-silver-builder   -> agent-builder-token / GITLAB_TOKEN
ploeg-worker-silver-reviewer  -> agent-reader-token  / GITLAB_TOKEN

Process, stated plainly

The code was written before it was proposed. It came out of a survey of what rc.31 could and could not do for code14's staging cluster; the durable commitment was extracted afterwards. That is the opposite of the order AGENTS.md asks for, and it is why ADR-0023 is proposed and nothing here marks it accepted — the schema's rule against retro-justification is exactly the risk. The record states the two alternatives genuinely weighed (per-Team, per-deployment) and what each would have cost, so ratifying it is a decision rather than a rubber stamp. openspec/changes/open-change-requests-on-gitlab/adr.md says so out loud.

If ADR-0023 is rejected the change does not shrink, it changes shape: per-Team dialect is about the same code with a different owner for the answer.

A contract gap the gate caught, not me. RepoRef.Forge first landed without its schema edit. repo in taskspec.v1.schema.json is additionalProperties: false, so a Task Spec carrying the field would have been rejected by any consumer validating against the published contract. The gate missed it initially because fullTaskSpec — the fixture whose job is to carry every field — did not set it. Fixed in the order the tasks rules ask for: fixture first, observed to fail with at '/repo': additional properties 'forge' not allowed, then the schema. Second commit.

No VIK trailer — this did not come from a board ticket.

Non-goals

  • ADR-0013 tier 2 on GitLab. Per-run push credentials are minted through /api/v1/admin/users/forge, a Forgejo admin endpoint with no GitLab equivalent. The nearest analogue is a project access token — a different escalation with a different blast radius, deserving its own record rather than an implied one. GitLab runs on the shared token, which is the documented pre-tier-2 behaviour. Tier 1 does port and is configured. Named as a re-evaluation trigger on 0023.
  • Two forges from one deployment. The dialect varies per Run; URL and credential do not, because a pod holds one of each.
  • A worker-side ForgeProvider SPI. Two dialects justify a switch; three justify an SPI, and 0023 names that as the trigger to supersede it.

Downstream

openspec/.../tasks.md group 5 carries the wiring in code14's staging cluster, which is what this exists for and what proves the loop closes. The ordering constraint that matters: the chart has no additionalProperties: false, so executor.forge against rc.31 is silently ignored — values must not land before the OCIRepository bump.

Gates

Run locally with the toolchain CI pins. Note helm v4.2.3, not 4.2.4 — the two disagree about the blank line before a document separator, exactly as scripts/helm-golden.sh warns on failure; it cost a diagnosis here.

gofmt -l .                                    clean
go vet ./...                                  clean
go build ./...                                ok
go test ./...                                 ok — all packages, incl. pkg/store
helm lint ops/helm/ploeg                      1 chart linted, 0 failed
helm template (default)                       ok
helm template -f ci/executor-values.yaml      ok
helm template -f ci/executor-cronjob-values.yaml   ok
helm template -f ci/executor-gitlab-values.yaml    ok   (new)
./scripts/helm-golden.sh check                ok, 4 renders
go test ./internal/ledger/                    ok
openspec validate --all                       6 passed, 0 failed

New tests: pkg/worker/forge_test.go (both dialects, empty-means-forgejo, subgroup path encoding, wrong-base rejection on both, unknown dialect named in the error, HTTP failure) and pkg/worker/prompt_forge_test.go (GitLab writer and reader vocabulary and endpoint, already-open branch, and the Forgejo contract byte-for-byte unchanged).

## Why ploegd has spoken GitLab since rc.31 — `pkg/provider/gitlab` comments on a merge request and verifies inbound webhooks. `ploeg-worker` never learned. It polled `/api/v1/repos/{owner}/{name}/pulls` and its briefing named the Forgejo API as the way to open a change request, so against a GitLab target **nothing was ever opened**. That one absence removes most of the review loop. No change request means the Shift has nothing for a reviewing Role to comment on, `publishRound` logs `findings not published: no pull request on this shift yet`, no forge webhook can arrive, and `close-the-review-loop`'s fix Rounds never open. Ingest, routing, Rounds and budgets all work — the work simply never leaves the factory. Every step of that is silent: the writing Run reports success. ## What was already fine Three things are forge-agnostic already and are untouched here, which is why this is one change and not three: - `pkg/worker/git.go` builds clone and push URLs from owner and name — already correct for a GitLab subgroup path. Git is git. - `pkg/shiftengine`'s `prPathRe` is `/(?:pulls?|merge_requests)/(\d+)/?$` and has matched GitLab all along. - `pullRequest(reports)` takes the Shift's change request from the writing Run's OutcomeReport links. The poll is ground truth for a Run that died before reporting, not the primary path. The gap was exactly two places that name a forge: the poll, and the briefing. ## What changes - **`harness.RepoRef.Forge`** — the API dialect, additive and optional on `taskspec.v1`. Empty means `forgejo`, so every stored Target and every existing deployment keeps its exact meaning. The value travels on the Work Item (`work.Target.Forge`, there since ADR-0016) and falls back to a deployment default. - **`findPR` dispatches.** GitLab filters `source_branch` server-side, so unlike the Forgejo call it cannot be defeated by a repo with more than a page of open requests. The project is addressed by URL-encoded full path — `code14nl/internal/poc-silk` is three segments and the slashes must survive as `%2F`. - **The briefing dispatches, in vocabulary as well as endpoint.** An agent told to open a "pull request" against GitLab looks for an endpoint that is not there, and the noun is what it searches its tools and the repo's docs for. `changeRequestNoun` and `openChangeRequest` switch on the same value so they cannot drift. - **An unknown dialect fails loudly.** Falling back to Forgejo would poll a real endpoint shape against the wrong host and report "no change request" forever — indistinguishable from an agent that never opened one. - **Chart:** `executor.forge` selects one active forge, `executor.gitlab` configures it, and a new `ploeg.forge` helper resolves whichever is active so no template reads `.forgejo` or `.gitlab` directly. ### Two things worth a reviewer's attention **A nil pointer removed in passing.** `ploeg.workerPodTemplate` dereferenced `.Values.executor.forgejo.url` unconditionally, so `forgejo: null` — the documented way to empty a block whose defaults name Secrets you have no reason to hold — became a nil pointer the moment `executor.enabled` went true. The new `executor-gitlab` fixture renders with `forgejo: null` **on purpose**, so it cannot come back without failing a golden. **A fourth golden.** Selecting a forge changes the worker pod — a different credential Secret, a different API — and the worker pod is the boundary the goldens exist to police. `executor-gitlab.yaml` pins ADR-0013 tier 1 holding on GitLab: ``` ploeg-worker-silver-analyst -> agent-reader-token / GITLAB_TOKEN ploeg-worker-silver-builder -> agent-builder-token / GITLAB_TOKEN ploeg-worker-silver-reviewer -> agent-reader-token / GITLAB_TOKEN ``` ## Process, stated plainly **The code was written before it was proposed.** It came out of a survey of what rc.31 could and could not do for code14's staging cluster; the durable commitment was extracted afterwards. That is the opposite of the order AGENTS.md asks for, and it is why **ADR-0023 is `proposed` and nothing here marks it accepted** — the schema's rule against retro-justification is exactly the risk. The record states the two alternatives genuinely weighed (per-Team, per-deployment) and what each would have cost, so ratifying it is a decision rather than a rubber stamp. `openspec/changes/open-change-requests-on-gitlab/adr.md` says so out loud. If ADR-0023 is rejected the change does not shrink, it changes shape: per-Team dialect is about the same code with a different owner for the answer. **A contract gap the gate caught, not me.** `RepoRef.Forge` first landed without its schema edit. `repo` in `taskspec.v1.schema.json` is `additionalProperties: false`, so a Task Spec carrying the field would have been rejected by any consumer validating against the published contract. The gate missed it initially because `fullTaskSpec` — the fixture whose job is to carry every field — did not set it. Fixed in the order the tasks rules ask for: fixture first, observed to fail with `at '/repo': additional properties 'forge' not allowed`, then the schema. Second commit. **No VIK trailer** — this did not come from a board ticket. ## Non-goals - **ADR-0013 tier 2 on GitLab.** Per-run push credentials are minted through `/api/v1/admin/users/forge`, a Forgejo admin endpoint with no GitLab equivalent. The nearest analogue is a project access token — a different escalation with a different blast radius, deserving its own record rather than an implied one. GitLab runs on the shared token, which is the documented pre-tier-2 behaviour. Tier 1 does port and is configured. Named as a re-evaluation trigger on 0023. - **Two forges from one deployment.** The dialect varies per Run; URL and credential do not, because a pod holds one of each. - **A worker-side ForgeProvider SPI.** Two dialects justify a switch; three justify an SPI, and 0023 names that as the trigger to supersede it. ## Downstream `openspec/.../tasks.md` group 5 carries the wiring in code14's staging cluster, which is what this exists for and what proves the loop closes. The ordering constraint that matters: the chart has no `additionalProperties: false`, so `executor.forge` against rc.31 is **silently ignored** — values must not land before the `OCIRepository` bump. ## Gates Run locally with the toolchain CI pins. Note **helm v4.2.3, not 4.2.4** — the two disagree about the blank line before a document separator, exactly as `scripts/helm-golden.sh` warns on failure; it cost a diagnosis here. ``` gofmt -l . clean go vet ./... clean go build ./... ok go test ./... ok — all packages, incl. pkg/store helm lint ops/helm/ploeg 1 chart linted, 0 failed helm template (default) ok helm template -f ci/executor-values.yaml ok helm template -f ci/executor-cronjob-values.yaml ok helm template -f ci/executor-gitlab-values.yaml ok (new) ./scripts/helm-golden.sh check ok, 4 renders go test ./internal/ledger/ ok openspec validate --all 6 passed, 0 failed ``` New tests: `pkg/worker/forge_test.go` (both dialects, empty-means-forgejo, subgroup path encoding, wrong-base rejection on both, unknown dialect named in the error, HTTP failure) and `pkg/worker/prompt_forge_test.go` (GitLab writer and reader vocabulary and endpoint, already-open branch, and **the Forgejo contract byte-for-byte unchanged**).
ploegd has spoken GitLab since rc.31 — pkg/provider/gitlab comments on a merge
request and verifies inbound webhooks. ploeg-worker never learned: it polled
/api/v1/repos/{owner}/{name}/pulls and its briefing named the Forgejo API as
the way to open one. On a GitLab target no change request was ever created, so
the Shift had none for a reviewer to comment on, publishRound logged "no pull
request on this shift yet", and the review loop could not close. Silent all the
way to a human.

Less was missing than it looks. Three things were already forge-agnostic and
are untouched here: git.go's authURL/plainURL build owner/name into a URL that
is already correct for a GitLab subgroup; shiftengine's prPathRe already
matches /merge_requests/(\d+); and the Shift takes its change-request URL from
the OutcomeReport links, not from the poll. The gap was two places that name a
forge, so that is what this changes.

RepoRef gains Forge, the API DIALECT. Empty means forgejo, so every stored
target, every taskspec and every deployment that never set it keeps its exact
current meaning. The dialect travels on the work item — pkg/work.Target has
carried Forge all along — and falls back to the worker's configured default,
matching the "empty = the default forge" promise ploegd's own registry makes.

- findPR dispatches. GitLab filters source_branch server-side, so unlike the
  Forgejo call it cannot be defeated by a repo with 50+ open requests. The
  project is addressed by URL-ENCODED full path: code14nl/internal/poc-silk is
  three segments and the slashes must survive as %2F.
- The briefing dispatches, in vocabulary as well as endpoint. An agent told to
  open a "pull request" on GitLab looks for an endpoint that is not there, and
  the noun is what it searches its tools and the repo's docs for. Noun and
  endpoint come out of the same switch so they cannot drift.
- An unknown dialect fails loudly. Falling back to Forgejo would poll a real
  endpoint shape against the wrong host and report "no change request" forever
  — indistinguishable from an agent that never opened one.

Chart: executor.forge selects one active forge and executor.gitlab configures
it. The new ploeg.forge helper resolves whichever is active into one shape, so
no template touches .forgejo or .gitlab directly. That also removes a trap: the
worker template used to dereference .Values.executor.forgejo.url
unconditionally, which made `forgejo: null` — the documented way to empty an
unused block — a nil pointer the moment executor.enabled flipped true. The new
GitLab fixture renders with forgejo null precisely so that cannot come back.

FORGE_URL is the name; FORGEJO_URL is emitted alongside it and still accepted,
so a ScaledJob starting a pod from the previous image mid-upgrade still finds a
forge.

A fourth golden, executor-gitlab, because selecting a forge changes the worker
pod — a different credential Secret and a different API — and the worker pod is
the boundary the goldens exist to police. It pins the ADR-0013 tier-1 split
holding on GitLab: readers draw agent-reader-token, the writer draws
agent-builder-token.

Tier 2 is deliberately absent. ploegd mints per-run push credentials through
/api/v1/admin/users/forge, a Forgejo admin endpoint with no GitLab equivalent;
the nearest analogue is a project access token, a different escalation that
deserves its own ADR rather than an implied one. Unset means the shared token,
which is the documented pre-tier-2 behaviour.

Gates run locally with the pinned toolchain (helm v4.2.3 as CI pins; 4.2.4
disagrees about the blank line before a document separator, as scripts/
helm-golden.sh warns):

  gofmt -l .              clean
  go vet ./...            clean
  go build ./...          ok
  go test ./...           ok, all packages incl. pkg/store
  helm lint               1 chart linted, 0 failed
  helm-golden.sh check    ok, 4 renders

No VIK trailer: this did not come from a board ticket.
RepoRef.Forge landed without its schema edit, which docs/contracts/README.md
does not allow: v1 changes additively, and the Go type and the published schema
change together. `repo` is additionalProperties:false, so a Task Spec carrying
the field would have been rejected by any consumer validating against the
published contract.

The gate did not catch it because fullTaskSpec — the fixture whose whole job is
to carry every field — did not set Forge, and omitempty dropped it.

Fixed in the order the tasks rules ask for: the fixture first, observed to fail
with

    at '/repo': additional properties 'forge' not allowed

then the schema. The enum is constrained to the dialects the worker actually
implements, so a Task Spec naming a third forge fails at the contract rather
than at run time.
docs(openspec): record the forge dialect decision as ADR-0023
All checks were successful
On Pull Request / checks (pull_request) Successful in 1m13s
215c9e5b12
The change was built before it was proposed, which is the wrong order and is
why this is a separate commit rather than a backdated one. AGENTS.md routes
non-trivial changes through OpenSpec — proposal → specs → design → adr → tasks
— and the adr step gates tasks precisely so a durable commitment cannot reach
implementation unrecorded. It did. This records it and leaves the verdict open.

ADR-0023: the forge DIALECT is a property of the Work Item and varies per Run;
the forge URL and credential stay deployment-global because a worker pod holds
one of each. Status proposed — a human ratifies it or does not. The record
states the two options genuinely weighed and what each would have cost
(per-Team re-couples capability to codebase, undoing ADR-0014; per-deployment
makes a second forge a second release and leaves Target.Forge meaning one thing
to the engine and nothing to the worker), so the ratification is a decision
rather than a rubber stamp. adr.md says all of this out loud.

Tier 2 is a named non-goal, not an omission: per-run push credentials are
minted through /api/v1/admin/users/forge, which GitLab has no equivalent for,
so GitLab runs on the shared token until that is decided on its own evidence.
It is a re-evaluation trigger on 0023, alongside a third forge arriving — two
dialects justify a switch, three justify an SPI and a supersession.

New capability spec `forge-dialect`: where the dialect is decided, that an
unknown one stops the Run loudly rather than falling back, that the poll is
scoped to the Run and filtered server-side where the forge allows it, that the
delivery contract uses the forge's own vocabulary, and that selecting a forge
shows up in a committed golden of the worker pod.

architecture.md §9 item 15 corrected. Two of its claims were true when written
and are not now — ForgeProvider has implementations, and pkg/worker/forge.go no
longer hardcodes one dialect — which is the staleness that section warns about
in both directions. What remains a singleton (URL, credential) and what remains
open (tier 2) are stated rather than implied.

tasks.md group 5 carries the downstream wiring in code14's staging cluster,
which is what this change exists for and what proves the loop closes. It
includes the ordering constraint that matters: the chart has no
additionalProperties:false, so executor.forge against rc.31 is silently
ignored — values must not land before the OCIRepository bump.

openspec validate --all: 6 passed, 0 failed.
go test ./internal/ledger/: ok
refactor(worker): name things instead of explaining them
All checks were successful
On Pull Request / checks (pull_request) Successful in 45s
a7907770ff
Review feedback. Every comment this change added is gone; the diff now adds
none. What the prose carried is carried by names, types and the ADR instead.

  prMatches            -> isRunChangeRequest(changeRequest, runBranch, base)
  findPR               -> findOpenChangeRequest
  listForgejoPRs       -> listForgejoPullRequests
  listGitLabMRs        -> listGitLabMergeRequests
  forgeGet             -> getJSON
  openChangeRequest    -> openChangeRequestInstruction
  Config.Forge         -> Config.DefaultForge
  changeRequest{URL,Head,Base} -> {URL,HeadBranch,BaseBranch}

The "no silent fallback" comment is now errUnsupportedForge, a named sentinel
the test asserts with errors.Is rather than a substring. The subgroup comment
is RepoRef.ProjectPath, which joins and never splits. The chart's helper
comment is the helper's own shape.

TWO NAMES FOR ONE VALUE, REMOVED. requireEnvOneOf("FORGE_URL", "FORGEJO_URL")
was a shim for a rolling upgrade that cannot happen: the release train keeps
chart and appVersion in lockstep, so the chart and the image it configures move
together. The worker now requires FORGE_URL and nothing else, and the chart
emits only that. requireEnvOneOf is deleted.

The dialect had the same problem in a worse form: PLOEG_FORGE on the worker was
a second spelling of PLOEG_TARGET_FORGE, which ploegd has read since the forge
registry landed — the same concept, the same default, two names, one of them
invented here. The worker now reads PLOEG_TARGET_FORGE too, and the chart
renders it once for both binaries from executor.forge, so they cannot disagree
about which forge is the default.

Both renames are breaking for anyone setting these by hand and neither is for a
chart-driven deployment. That trade is the point: two names for one value is a
worse thing to own than a rename under a version bump.

values.yaml loses its comment blocks; the meaning moved into
values.schema.json descriptions, which is where a Helm chart keeps structured
intent — validated, machine-readable, and shown by tooling rather than only to
whoever opens the file.

Goldens regenerated (helm v4.2.3, as CI pins). Gates: gofmt clean, go vet
clean, go build ok, go test ./... ok, helm lint ok, 4 renders ok,
helm-golden.sh check ok, go test ./internal/ledger/ ok, openspec validate --all
6 passed.
Author
Owner

Review feedback addressed in a790777. Two corrections to the description above, which is now stale on both points.

1. Every comment this change added is gone. The Go diff now adds zero. What the prose carried is carried by names, types and the ADR instead:

prMatches                    -> isRunChangeRequest(changeRequest, runBranch, base)
findPR                       -> findOpenChangeRequest
listForgejoPRs               -> listForgejoPullRequests
listGitLabMRs                -> listGitLabMergeRequests
forgeGet                     -> getJSON
openChangeRequest            -> openChangeRequestInstruction
Config.Forge                 -> Config.DefaultForge
changeRequest{URL,Head,Base} -> {URL,HeadBranch,BaseBranch}

The "no silent fallback" paragraph is now errUnsupportedForge, a named sentinel the test asserts with errors.Is rather than a substring match. The subgroup paragraph is RepoRef.ProjectPath, which joins and never splits. values.yaml lost its comment blocks and the meaning moved into values.schema.json descriptions — validated, machine-readable, and surfaced by tooling rather than only to whoever opens the file.

2. FORGE_URL / FORGEJO_URL is gone — one name. The description above says both are emitted. They were, and that was wrong. requireEnvOneOf("FORGE_URL", "FORGEJO_URL") hedged a rolling upgrade that cannot happen: the release train keeps chart and appVersion in lockstep, so the chart and the image it configures move together. The worker now requires FORGE_URL and the chart emits only that; requireEnvOneOf is deleted.

The dialect had the same problem in a worse form. PLOEG_FORGE on the worker was a second spelling of PLOEG_TARGET_FORGE, which ploegd has read since the forge registry landed — same concept, same default, two names, and the second one invented here. The worker now reads PLOEG_TARGET_FORGE too, and the chart renders it once from executor.forge for both binaries, so they cannot disagree about the default forge.

Both renames break anyone setting these by hand; neither breaks a chart-driven deployment. That trade is the point — two names for one value is a worse thing to own than a rename under a version bump.

Gates re-run, goldens regenerated with helm v4.2.3:

gofmt -l .                     clean
go vet ./...                   clean
go build ./...                 ok
go test ./...                  ok
helm lint                      1 chart linted, 0 failed
helm template x4               ok
./scripts/helm-golden.sh check ok
go test ./internal/ledger/     ok
openspec validate --all        6 passed, 0 failed

Still open and unchanged: ADR-0023 needs ratification (tasks 1.2) — it is proposed, and the code predates it, which adr.md states outright.

Review feedback addressed in `a790777`. Two corrections to the description above, which is now stale on both points. **1. Every comment this change added is gone.** The Go diff now adds zero. What the prose carried is carried by names, types and the ADR instead: ``` prMatches -> isRunChangeRequest(changeRequest, runBranch, base) findPR -> findOpenChangeRequest listForgejoPRs -> listForgejoPullRequests listGitLabMRs -> listGitLabMergeRequests forgeGet -> getJSON openChangeRequest -> openChangeRequestInstruction Config.Forge -> Config.DefaultForge changeRequest{URL,Head,Base} -> {URL,HeadBranch,BaseBranch} ``` The "no silent fallback" paragraph is now `errUnsupportedForge`, a named sentinel the test asserts with `errors.Is` rather than a substring match. The subgroup paragraph is `RepoRef.ProjectPath`, which joins and never splits. `values.yaml` lost its comment blocks and the meaning moved into `values.schema.json` descriptions — validated, machine-readable, and surfaced by tooling rather than only to whoever opens the file. **2. `FORGE_URL` / `FORGEJO_URL` is gone — one name.** The description above says both are emitted. They were, and that was wrong. `requireEnvOneOf("FORGE_URL", "FORGEJO_URL")` hedged a rolling upgrade that cannot happen: the release train keeps chart and appVersion in lockstep, so the chart and the image it configures move together. The worker now requires `FORGE_URL` and the chart emits only that; `requireEnvOneOf` is deleted. The dialect had the same problem in a worse form. `PLOEG_FORGE` on the worker was a second spelling of **`PLOEG_TARGET_FORGE`**, which ploegd has read since the forge registry landed — same concept, same default, two names, and the second one invented here. The worker now reads `PLOEG_TARGET_FORGE` too, and the chart renders it once from `executor.forge` for both binaries, so they cannot disagree about the default forge. Both renames break anyone setting these by hand; neither breaks a chart-driven deployment. That trade is the point — two names for one value is a worse thing to own than a rename under a version bump. Gates re-run, goldens regenerated with helm v4.2.3: ``` gofmt -l . clean go vet ./... clean go build ./... ok go test ./... ok helm lint 1 chart linted, 0 failed helm template x4 ok ./scripts/helm-golden.sh check ok go test ./internal/ledger/ ok openspec validate --all 6 passed, 0 failed ``` Still open and unchanged: **ADR-0023 needs ratification** (tasks 1.2) — it is `proposed`, and the code predates it, which `adr.md` states outright.
ryangr0 merged commit 20cfe1a7d7 into development 2026-09-02 06:13:18 +00:00
Commenting is not possible because the repository is archived.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/ploeg!46
No description provided.