Skip to content

ci: complete the template alignment with the copier-rendered workflow pipeline - #43

Merged
jacaudi merged 6 commits into
mainfrom
ci/workflow-alignment
Aug 29, 2026
Merged

ci: complete the template alignment with the copier-rendered workflow pipeline#43
jacaudi merged 6 commits into
mainfrom
ci/workflow-alignment

Conversation

@jacaudi

@jacaudi jacaudi commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Completes the jacaudi/template alignment that #42 started. #42 landed the non-workflow half — taskfile.yml, .taskfiles/, scripts/, the lint configs. This lands the workflows, rendered from gh:jacaudi/template@a676cfe with the answers now recorded in .copier-answers.yml (withheld from #42 on purpose so the recorded template state never claimed more than was there).

Deletes build-images.yaml, pull-request-workflow.yaml, manual-dev-release-workflow.yaml, release.yaml.

Refs #37, #41. Lands the registry half of #36. Closes the Go-pinning criterion left open on #40.

What the template does not cover for this repo

1. ci-build.yml cannot publish either image, so it gets a file: input. This is the one place copier update will conflict, and it is deliberate. Upstream cut file:, reasoning that a repo whose Dockerfile is elsewhere moves its context:. That assumes the Dockerfile and the context can be the same directory. Both images here live at images/*/Dockerfile and both COPY go.mod cmd api internal, so both need the repo root as context while neither sits at it:

  • context: .no Dockerfile or Containerfile in .
  • context: images/manager → resolves, then dies at the COPY

No value of build_context builds either one. extra_build_contexts is not an escape hatch — ci.yaml's build-check matrix passes only context:, same constraint, and ci-build-check.yml never pushes. The block added is ci-build-check.yml's resolution logic verbatim; that stage already ships this exact input for the same reason, so this is the template's own pattern, merely absent from the stage that publishes. Worth upstreaming to drop the deviation.

2. A second build job for the agent, not a matrix leg — ci-build.yml declares outputs.image, and matrixed outputs are last-leg-wins, which would hand smoke whichever digest finished last. build-agent is in the ci gate's needs, or it could fail without failing ci.

3. The controller-gen drift gate, carried over from #39. Invoked as make manifests generate rather than a bare controller-gen call, so CONTROLLER_TOOLS_VERSION and the generator flags stay single-sourced in the Makefile.

4. A drift gate for release.yaml (hack/release-file-drift.sh). It is kustomize-generated from config/ and is the documented install path. Pins are read back out of the committed file rather than passed in: the Makefile defaults are dev values, and the committed pins are release-scoped. So it checks structure against config/ and leaves versioning to release time.

Both gates fail on drift and never fix it — a CI step that mutates the tree hides the problem it exists to surface.

Three defects found while building

ci-integration.yml's setup-envtest pin does not resolve, in any consumer of the template. go install sigs.k8s.io/controller-runtime/tools/setup-envtest@v0.22.1 fails with module ... found, but does not contain package .../tools/setup-envtest. It is a separate module whose only released tags are v0.24.0/v0.24.1, so a controller-runtime version is never valid for it — and the stock renovate annotation names the wrong module, so it would keep proposing uninstallable versions. Pinned v0.24.1, annotation corrected. Asset version lowered from 1.34.x to 1.30.0 to match this repo's k8s.io/* v0.30 stack behind controller-runtime v0.18.5.

Two latent Makefile bugs, both surfaced by running the new drift gate rather than by reading:

  • The four tool rules took $(LOCALBIN) as a normal prerequisite, so installing any one tool made every other look stale; make re-ran its installer and kustomize's refuses with exists. Remove it first. ci-lint.yml reproduces the trigger exactly. Fixed with order-only prerequisites.
  • generate-release-file and run-e2e "restored" two config/ files with git checkout, which restores from the index and therefore silently discards uncommitted edits to them. It ate an in-progress rename during this branch, with no output. Replaced with an explicit save/restore in a trap.

release.yaml was tracked and gitignored. Inert, since tracking wins — but the drift gate's own remediation is "regenerate and commit", and git add against an ignored path is a silent no-op.

Known coverage gap: no e2e stage

pull-request-workflow.yaml ran a third tier on a kind cluster. The template models unit + envtest only, so this swap drops it. Not restored deliberately: internal/it's wstunnel spec waits on Deployment readiness, which the pre-patch Deployment already satisfies, so it can assert before the controller rebuilds the pod spec. Wiring an intermittently-red suite into ci — the single required check — would leave main randomly unmergeable. The right fix is waiting on observedGeneration; #2 narrows the window but does not close it. make run-e2e still works locally. Recorded in ci.yaml.

Verified before pushing

task ci golangci-lint 0 issues, govulncheck 0
envtest suite 79s — the real signature, not a silent zero-test pass
both images build with context: . + explicit file:
task smoke passes against the built manager
actionlint, local-refs, run-scripts-exist, run-interpolation clean
codegen + release.yaml drift gates pass, including under ci-lint's exact step order
commits 6, all signed

Tag/stage agreement checked with go test -list, since a wrong tag runs zero tests and reports success: -tags=integration selects internal/controller, -tags=e2e selects internal/it, and untagged selects neither.

Reviewer notes

  • copier update will conflict on ci-build.yml. Keep the file: input; see the comment on the input itself.
  • First merge to main opens a release PR at 2.11.0 → 2.11.1/2.12.0 depending on the commit types here. The manifest is seeded via .github/release-please-manifest.json, never release-as.
  • release-republish.yml repairs the manager only — noted in the file with the manual incantation for the agent.
  • Still to do after merge: create a ruleset requiring exactly ci. There is no ruleset at all today, so nothing can break before then, and it must be created after these workflows land, never before.

Completes the jacaudi/template alignment PR #42 started. That PR landed the
non-workflow half — taskfile.yml, .taskfiles/, scripts/, the lint configs. This
lands the workflows themselves, rendered from gh:jacaudi/template at a676cfe
with the answers now recorded in .copier-answers.yml (deliberately withheld from
#42 so the recorded template state never claimed more than was actually there).

.github/actions/setup/ ships alongside the workflows because it has to:
actionlint is verifiably blind to a `uses: ./path` that does not resolve, so a
missing composite action passes lint and fails at run time. scripts/local-refs.sh
is the gate that catches it, and it runs first in ci-lint.

Deletes build-images.yaml, pull-request-workflow.yaml,
manual-dev-release-workflow.yaml and release.yaml.

Four things the template does not cover for this repo:

1. A `file:` INPUT ON ci-build.yml — a local deviation, and the one place
   `copier update` will conflict. Upstream cut `file:` on the reasoning that a
   repo whose Dockerfile is elsewhere moves its `context:` instead. That assumes
   the Dockerfile and the context can be the same directory. This repo publishes
   two images, images/manager and images/agent, and both `COPY go.mod cmd api
   internal` — so both need the repo root as context while neither sits at it.
   `context: .` finds no ./Dockerfile; `context: images/manager` finds one and
   dies at the COPY. There is no answer to build_context that builds either
   image. The block added here is ci-build-check.yml's resolution logic verbatim,
   so this is the template's own pattern, merely absent from the stage that
   publishes. Worth upstreaming so the deviation can be dropped.

2. A SECOND BUILD JOB for the agent. The template models one published image and
   its extra_build_contexts route to ci-build-check.yml, which declares no
   outputs and never pushes. The agent must actually be published — the manager
   takes it as a runtime argument — so it gets its own job, not a matrix leg:
   ci-build.yml declares outputs.image, and a matrixed job's outputs are
   last-leg-wins, which would hand smoke whichever digest finished last.
   build-agent is in the `ci` gate's needs list, or it could fail without
   failing `ci`.

3. A CODEGEN DRIFT GATE, carried over from PR #39. CRDs, RBAC and deepcopy are
   generated from the API types and committed, so they can fall behind with
   nothing to say so. Invoked as `make manifests generate` rather than a bare
   controller-gen call, so CONTROLLER_TOOLS_VERSION and the generator flags stay
   single-sourced in the Makefile.

4. A DRIFT GATE FOR release.yaml (hack/release-file-drift.sh). It is generated by
   kustomize from config/ and is this repo's documented install path. The pins
   are read back out of the committed file rather than passed in: the Makefile
   defaults are dev values, and the committed pins are release-scoped, so this
   checks structure against config/ and leaves versioning to release time.

Both gates fail on drift and never fix it — a CI step that mutates the tree
hides the problem it exists to surface.

Also corrected, both verified rather than assumed:

- ci-integration.yml's setup-envtest pin. The template's `v0.22.1` DOES NOT
  RESOLVE in any consumer: tools/setup-envtest is a separate module whose only
  released tags are v0.24.0 and v0.24.1, so a controller-runtime version number
  is not a valid version for it. Pinned to v0.24.1, and the renovate annotation
  now names that module instead of controller-runtime.
- The envtest asset version, lowered from the template's 1.34.x to 1.30.0 to
  match this repo's k8s.io/* v0.30 client stack behind controller-runtime
  v0.18.5. Verified: the suite runs for 79s, the known envtest signature, rather
  than passing in seconds having run nothing.
- release.yaml was tracked AND gitignored. Inert, because tracking wins, but the
  drift gate's own remediation is "regenerate and commit", and `git add` against
  an ignored path is a silent no-op.

Two gaps recorded in ci.yaml rather than left to be discovered:

- NO RELEASE WIRING. ci-release.yml requires APP_ID and APP_PRIVATE_KEY with no
  GITHUB_TOKEN fallback, and this repo has neither — the API reports
  total_count 0, with no environments and a personal-account owner. Wiring it
  anyway opens a release PR that never gets a run, never reports `ci`, and is
  unmergeable forever. The stage files and the manifest seeded at 2.11.0 are
  committed, so restoring the two jobs later is purely additive.
- NO e2e STAGE. The deleted pull-request-workflow.yaml ran a kind-based third
  tier; the template models only unit and envtest. Not restored here because
  internal/it carries a known race, and a flaky suite wired into the single
  required check leaves main randomly unmergeable.

Refs #37, #41. Completes the pipeline half of #36's registry decision.
…erator

This fork inherited the upstream nccloud image names, which it cannot publish
to. The registry half of #36, landed here because it is an input to the copier
answers the workflow commit records — the pipeline and the manifests have to
name the same repository or the operator points at an image that is never built.

Both images move together, and they have to: the manager takes the agent image
as a runtime argument, so renaming one without the other produces a deployment
that pulls something that does not exist.

  Makefile                                IMAGE_TAG_BASE, which also derives BUNDLE_IMG
  config/manager/kustomization.yaml       the manager's newName
  config/default/manager_args_patch.yaml  the --agent-image default
  bundle/manifests/...clusterserviceversion.yaml
  release.yaml                            regenerated, not hand-edited

release.yaml is regenerated with `make generate-release-file` at the pins it
already carried (v2.11.0), so the diff is the namespace and nothing else.
hack/release-file-drift.sh now proves that on every push.

The module path is deliberately NOT touched — github.com/nccloud/wireguard-operator
stays until #36's other half, which rewrites the import block in all 18 Go files
and wants a quiet tree. README.md still points at the upstream Helm chart; this
fork publishes none, so that is a docs fix rather than part of the rename.
Two latent Makefile bugs, both found by running the new release.yaml drift gate
rather than by reading, and both of which the gate makes far more likely to fire
because ci-lint.yml now invokes these targets on every push.

TOOL INSTALLS RE-RAN AND FAILED. The four tool rules took $(LOCALBIN) as a
normal prerequisite, so installing any one tool updated bin/'s mtime and made
every other tool look stale. Make then re-ran its installer, and kustomize's
refuses outright:

  .../bin/kustomize exists. Remove it first.
  make: *** [.../bin/kustomize] Error 1

ci-lint.yml reproduces the triggering order exactly — `make manifests generate`
installs controller-gen, then the drift gate calls generate-release-file, which
needs kustomize. A fresh runner happens to survive it by ordering luck, since
each tool is installed before the next touches the directory; any cached bin/ or
any second invocation does not. Order-only prerequisites (`| $(LOCALBIN)`) say
"ensure the directory exists" without comparing timestamps against it.

`git checkout` SILENTLY DISCARDED UNCOMMITTED WORK. generate-release-file and
run-e2e both stamp release-time image pins into two config/ files, build, then
restore them with `git checkout ./config/...`. That restores from the INDEX, so
it does not put back what was there — it throws away any uncommitted edit to
those files. It ate an in-progress image-rename edit while this branch was being
written, with no output, and the only reason it was noticed is that the drift
gate then reported release.yaml and config/ disagreeing.

Replaced with an explicit save/restore around the mutation, in a trap so it also
runs when the build fails. One shared PINNED_AT_BUILD list rather than the same
two paths written out at four call sites.
go.mod said `go 1.26`, so actions/setup-go installed whatever patch the runner
image happened to ship. That made govulncheck's STDLIB findings a property of
the runner rather than of this repo: 7 findings on go1.26.4, zero on go1.26.6,
with CI observed on go1.26.7. A gate whose verdict depends on the day it ran is
not a gate — the same fail-quietly shape as the lint ratchet at fetch-depth 1
and a build tag no file carries, both of which bit this repo on #42.

Pinned via `toolchain` rather than by raising the `go` directive, so the module's
minimum requirement stays 1.26 for anyone importing it while the compiler CI
actually uses is fixed. Renovate bumps it like any other dependency.

Left open as an acceptance criterion on #40; closing it here because the new
pipeline runs govulncheck as a gating step, which is the point at which
non-determinism stops being cosmetic.
The release jobs were held back earlier in this branch only because APP_ID and
APP_PRIVATE_KEY were absent — ci-release.yml requires both with no GITHUB_TOKEN
fallback, and wiring it without them opens a release PR that never gets a run,
never reports `ci`, and is unmergeable forever. The secrets are now set
(verified: actions/secrets reports both), so the reason is gone and the comment
describing it would have shipped false.

  release              main-only, calls ci-release.yml with both secrets
  release-image        retags the manager digest that passed smoke
  release-image-agent  the same for the agent

THE AGENT'S RETAG IS A SEPARATE JOB BECAUSE ci-retag.yml TAKES ONE IMAGE. Its
absence would not have failed anything: the agent would have kept the :sha and
:latest tags build-agent gives it and silently never received v2.11.1, v2.11 or
v2, while the manager did. The two images would then drift apart at exactly the
tags a versioned deploy uses — and the manager's --agent-image default names a
versioned agent tag, so that is a broken deployment rather than a cosmetic gap.

Retag rather than rebuild, for both: release-please tags the release PR's merge
commit, which is the commit this run already built, so retagging publishes the
same bytes that passed smoke. A rebuild yields a new digest nothing has tested.

release-republish.yml still repairs the manager only — both its paths take one
hardcoded image and `task smoke` only knows the manager's shape. Recorded in the
file with the two-line imagetools incantation for repairing the agent by hand,
rather than left to be discovered during an incident.
docs/prompts/ holds scaffolding written for a fresh Claude Code session, not
project artefacts.

Committed on this branch rather than on main because this branch already rewrites
the surrounding block of .gitignore — landing the same line in two places would
resurrect the `/release.yaml` entry this branch deliberately removes. The
equivalent uncommitted edit sitting in the main checkout is superseded by this
and can be discarded with `git restore .gitignore`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant