ci: complete the template alignment with the copier-rendered workflow pipeline - #43
Merged
Conversation
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`.
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes the
jacaudi/templatealignment that #42 started. #42 landed the non-workflow half —taskfile.yml,.taskfiles/,scripts/, the lint configs. This lands the workflows, rendered fromgh:jacaudi/template@a676cfewith 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.ymlcannot publish either image, so it gets afile:input. This is the one placecopier updatewill conflict, and it is deliberate. Upstream cutfile:, reasoning that a repo whose Dockerfile is elsewhere moves itscontext:. That assumes the Dockerfile and the context can be the same directory. Both images here live atimages/*/Dockerfileand bothCOPY 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 theCOPYNo value of
build_contextbuilds either one.extra_build_contextsis not an escape hatch —ci.yaml's build-check matrix passes onlycontext:, same constraint, andci-build-check.ymlnever pushes. The block added isci-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.ymldeclaresoutputs.image, and matrixed outputs are last-leg-wins, which would handsmokewhichever digest finished last.build-agentis in thecigate'sneeds, or it could fail without failingci.3. The controller-gen drift gate, carried over from #39. Invoked as
make manifests generaterather than a barecontroller-gencall, soCONTROLLER_TOOLS_VERSIONand 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 fromconfig/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 againstconfig/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'ssetup-envtestpin does not resolve, in any consumer of the template.go install sigs.k8s.io/controller-runtime/tools/setup-envtest@v0.22.1fails withmodule ... found, but does not contain package .../tools/setup-envtest. It is a separate module whose only released tags arev0.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. Pinnedv0.24.1, annotation corrected. Asset version lowered from1.34.xto1.30.0to match this repo'sk8s.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:
$(LOCALBIN)as a normal prerequisite, so installing any one tool made every other look stale; make re-ran its installer and kustomize's refuses withexists. Remove it first.ci-lint.ymlreproduces the trigger exactly. Fixed with order-only prerequisites.generate-release-fileandrun-e2e"restored" twoconfig/files withgit 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 atrap.release.yamlwas tracked and gitignored. Inert, since tracking wins — but the drift gate's own remediation is "regenerate and commit", andgit addagainst an ignored path is a silent no-op.Known coverage gap: no e2e stage
pull-request-workflow.yamlran 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 intoci— the single required check — would leavemainrandomly unmergeable. The right fix is waiting onobservedGeneration; #2 narrows the window but does not close it.make run-e2estill works locally. Recorded inci.yaml.Verified before pushing
task cicontext: .+ explicitfile:task smokeactionlint,local-refs,run-scripts-exist,run-interpolationrelease.yamldrift gatesci-lint's exact step orderTag/stage agreement checked with
go test -list, since a wrong tag runs zero tests and reports success:-tags=integrationselectsinternal/controller,-tags=e2eselectsinternal/it, and untagged selects neither.Reviewer notes
copier updatewill conflict onci-build.yml. Keep thefile:input; see the comment on the input itself.mainopens 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, neverrelease-as.release-republish.ymlrepairs the manager only — noted in the file with the manual incantation for the agent.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.