fix(groundskeeper): order the sims after the image build

Both groundskeeper sim arms ran before the job that builds the image they run in. This adds a needs: override at each include site so they wait for build:ci-groundskeeper whenever it is in the pipeline.

main is already green. Retrying the two jobs once ci-groundskeeper:latest existed passed both and took pipeline 2774262574 to success, so this MR removes the need for that retry rather than repairing a break that is still open.

What happened

The first main pipeline after !144 (merged) merged failed on sim-groundskeeper and sim-groundskeeper-image, both allow_failure: false. Both died in prepare_executor with runner_configuration_error:

WARNING: Failed to pull image with policy "always": Error response from daemon: manifest for
registry.gitlab.com/gitlab-com/public-sector/pipeline/ci-groundskeeper:latest not found:
manifest unknown: manifest unknown

No script ran and no assertion was evaluated. The two sim jobs started at 21:38:48, and build:ci-groundskeeper started at 21:41:11 and finished at 21:42:00. The registry records ci-groundskeeper:latest as created at 21:41:48, inside that build window. The sims pulled two minutes and twenty-three seconds before their image existed.

Timing makes this deterministic rather than a race: the build takes roughly 50 seconds and the sims pull within 3 seconds of pipeline start.

Why the merge request pipeline was green

The jobs were never created on it. The shared rules anchor at .gitlab/ci/sim/sim-groundskeeper.yml:53 has three clauses: schedule to never, a release tag, and main with changes:. There is no merge_request_event clause, so on a merged-results pipeline no clause matches. Pipeline 2774262574 was the first execution these two jobs ever had.

That omission is the house pattern for sims whose assertions depend on a freshly built image, and it is intact here. Giving these arms merge request coverage would require giving build:ci-groundskeeper the same coverage, because otherwise arm 2 compares against whatever main last built. That is a larger change and is left alone.

The cause, and the part that outlives the retry

Arm 1 inherits needs: [] from the component at templates/groundskeeper/template.yml:77; arm 2 declared it directly. That default is correct for a real consumer, whose scheduled upkeep lane should run even when an unrelated earlier job fails. It does not fit a pipeline that also builds the image.

With needs: [] the sims always pull the previous pipeline's :latest. Arm 2 asserts byte identity between the baked engine and scripts/groundskeep.py, so it fails on any commit that touches the engine, and the next-action it prints points at build:ci-groundskeeper's rules:changes, which already lists that path. A reader would be sent to a correct configuration looking for a fault. These rules also fire on the tag lane, so a release pipeline reaches the same failure.

The image was sound throughout

Nothing that failed evaluated a claim about the component:

  • build:ci-groundskeeper succeeded, and containers/ci-groundskeeper.Containerfile:50 self-tests the installed engine during the build, covering install path, executable bit, and startup.
  • validate:groundskeeper ran the offline suite in the same pipeline: 217 assertions, 0 failures.
  • ci-groundskeeper:latest and :b49664ac share digest sha256:58effb99d9c9, so the published image was built from current main.

A consumer pulling the image at any point got a real, signed artifact.

Why optional: true

Three of the sim's rules:changes paths are absent from the build's: .gitlab/ci/sim/sim-groundskeeper.yml, .gitlab/ci/sim/fixtures/groundskeeper.json, and templates/groundskeeper/template.yml. A hard needs: fails pipeline creation on a change touching only those. This MR is itself such a change, so the first pipeline to exercise the fix takes that path. None of the three can stale the baked engine, since they exclude scripts/groundskeep.py, so :latest is current whenever the build is absent.

The idiom is already house style: pipeline-self-validates uses it six times, and build:ci-groundskeeper uses it for build:ci-tools.

Why artifacts: false

.image-build publishes a dotenv report, and dotenv variables inherited through needs: take precedence over a job's own variables:. Nothing here reads PRIMARY or DIGEST, so this closes the path before something does.

Limitation

GitLab drops absent optional needs and treats an empty resolved list as needs: []. Ordering therefore applies only when the build is present. The sim-only case keeps its immediate start, where the current image is already correct.

Validation

Run locally against this branch:

  • scripts/lint-templates.py: all 22 templates pass the v2 canonical shape, zero advisories against templates/groundskeeper/template.yml.
  • tests/groundskeeper/run.sh: 217 assertions, 0 failures.
  • tests/groundskeeper/validate-manifests.py: both manifests validate.
  • Template syntax check: 22 templates valid.
  • Strict duplicate-key parse across .gitlab-ci.yml, .gitlab/ci/**/*.yml, presets/*.gitlab-ci.yml and templates/*/template.yml: 31 files, no duplicate mapping keys.
  • POST /ci/lint on the interpolated config: valid: true, with a warning set identical to the main baseline (15 pre-existing retry:when deprecations). Resolved needs confirmed on both jobs.
  • A YAML load confirms needs sits at job level in both places and that rules: *sim_rules still resolves to the anchor.

The sim arms cannot run on this MR's pipeline, for the reasons above. validate:groundskeeper, validate:templates:syntax, validate:templates:shape and pipeline-self-validates are created and were green locally. The fix gets its real exercise on the post-merge main pipeline.

Also included

One vale SubstitutionWarning that !144 (merged) introduced in CONTRIBUTING.md, changing "normally" to "usually". The five other findings in that file predate it and are untouched. sim-vale-advisory is allow_failure: true and has been reporting those five on prior main pipelines, including ones that finished green.

Merge request reports

Loading
Loading