Skip to content

Sync branch ci rollout - #327

Open
mcocdawc wants to merge 16 commits into
developfrom
sync-branch-ci-rollout
Open

Sync branch ci rollout#327
mcocdawc wants to merge 16 commits into
developfrom
sync-branch-ci-rollout

Conversation

@mcocdawc

@mcocdawc mcocdawc commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Description

Contributor Declaration

By opening this pull request, I affirm the following:

  • All authors agree to the Contributor License Agreement.
  • The code follows the project's coding standards.
  • I have performed self-review and added comments where needed.
  • I have added or updated tests to verify that my changes are effective and functional.
  • I have run all existing tests and confirmed they pass.

🌦️ >> Documentation << 🌦️
https://sites.ecmwf.int/docs/dev-section/eckit/pull-requests/PR-327

mcocdawc and others added 7 commits August 19, 2026 11:30
Migrates eckit onto the manifest-driven CI in ecmwf/ci-infrastructure, the
same shape ecbuild and stack-dependencies already run. The legacy
ecmwf/downstream-ci pipeline is untouched and keeps running: it moves to
old_CI.yml (name: old CI) so the new pipeline can take the plain `CI` name,
which is what the generated downstream orchestrators match their
`workflow_run` trigger against.

.ci/manifest.toml declares ecbuild (compiler-independent) and stack-deps
(libaec, for FEATURE AEC) as dependencies, and eccodes as the one downstream
consumer. Both build legs use platform ubuntu-24.04 so they reuse the upstream
artifacts published under that slug rather than forcing rebuilds; runs-on and
container are scheduling only and stay out of artifact identity.

Build and test share a job and a container: the artifact is published as soon
as the install step completes, so consumers are unblocked without waiting for
ctest.

No pre-commit job. The repo has a .pre-commit-config.yaml, but nothing has ever
run it, and the shared workflow's `pre-commit run --all-files` fails today on
the pre-existing workflows (yamllint --strict flags document-start and the
`on:` truthy rule on every one). Enforcing that here would fail the pipeline
for reasons unrelated to this migration; it is left as a follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the generated cross-repo-trigger.yml, and trigger-downstream.yml for the
three repos that have consumers. Regenerate with the ci-infrastructure-generate
CLI; never edit by hand.

[[trigger-downstream]].ref stays at develop. That ref is what the orchestrator
pins its `uses:` to — GHA forbids an expression there — and it selects only
which workflow DEFINITION runs, never the code. Branch coordination is entirely
a runtime affair and needs no help from it: the orchestrator passes
branch: workflow_run.head_branch, pick-ref checks out the same-named branch in
each consumer when one exists, and resolve_deps sees a current-branch matching
^sync-branch- and resolves every upstream's manifest and artifacts from it.

One consequence worth knowing: `on: workflow_run` only fires for a workflow
file present on the default branch, and runs that copy. So the fan-out cannot
be exercised from sync-branch-ci-rollout at all — it goes live only once these
files reach develop. Nothing to flip later; this is the steady-state form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci-infrastructure removed its reusable .github/workflows/pre-commit.yml
(ecmwf/ci-infrastructure#14), so the comment here pointed at a workflow that no
longer exists. Each repo now carries the job itself; stack-dependencies' ci.yml
has the copyable version.

Comment only — no job runs either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The expensive work — the actual compile — is already on arc-runner-very-large
via the matrix legs. resolve was not: it sat on arc-runner-normal inside the
base container, for a job that is one batched GraphQL walk of the manifest
graph with no compiler and nothing to build.

ubuntu-slim is the cheap 1-CPU ARC runner and needs no container, which is
also what ci-infrastructure's generator picks for the identical resolve job it
emits into cross-repo-trigger.yml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These are the first expensive compiles in the new system, and artifact reuse
does not help them: it avoids a rebuild only when nothing changed, so a PR
touching one source file still recompiles the whole tree in a fresh container.
That is exactly what sccache is for. ecbuild and stack-dependencies did without
it for good reason — one compiles nothing, the other builds once and is then
reused from its artifact.

The steps live in the build action rather than the workflow so both entry
points get them: the generated cross-repo-trigger.yml calls this action
directly and never sees the workflow's own steps. It already exports
SCCACHE_BUCKET; ci.yml now does too, alongside the artifact bucket. They share
the object store's endpoint and credentials and differ only in bucket.

Opt-in by presence of that bucket. sccache has no default bucket and a daemon
that cannot reach one refuses to start, which would fail the build outright, so
with SCCACHE_BUCKET unset both steps no-op, the launcher flags come out empty
and the compile just runs uncached. A repo without the secret still builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the ubuntu-slim change: it saved runner size and lost far more to a
per-job venv build.

ci-infrastructure is baked into the base image and advertised via
CI_INFRASTRUCTURE_PYTHON, which makes ensure-infrastructure-present a true
no-op — no build, no PyPI/GitHub egress. ubuntu-slim is a runner label, not an
image, so with no container there is no bake and every run installs the package
from scratch. It cannot even be cached between runs: the venv lives under
RUNNER_TOOL_CACHE inside _work, and ARC's Kubernetes mode overlays _work with
an ephemeral per-job volume, so the cache is discarded each time.

resolve is short, so the container dominates its cost, not the runner size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.62%. Comparing base (da7bb69) to head (3d2e951).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #327   +/-   ##
========================================
  Coverage    67.61%   67.62%           
========================================
  Files         1182     1182           
  Lines        61697    61697           
  Branches      4667     4667           
========================================
+ Hits         41718    41724    +6     
+ Misses       19979    19973    -6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

mcocdawc and others added 9 commits August 20, 2026 12:11
Temporary, and paired with landing the generated orchestration on the default
branches ahead of the rest of the rollout.

on: workflow_run only ever executes the DEFAULT BRANCH's copy of
trigger-downstream.yml, so the fan-out cannot be exercised from a branch at all
— a branch's own orchestrator is inert. The only way to see the full downstream
CI before merging is to put the orchestrator on the default branch and have it
call consumer logic that still lives on the sync branch. [[trigger-downstream]]
.ref is what selects that: it pins the `uses:`, which GHA forbids being an
expression, and so decides which workflow DEFINITION runs.

Runtime branch coordination is unaffected and needs no pinning: the
orchestrator passes branch: workflow_run.head_branch, pick-ref checks out the
same-named branch in each consumer, and resolve_deps resolves upstream
manifests and artifacts from it.

Flip these to develop and regenerate when the rollout merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the temporary sync-branch pin. It only had a purpose alongside a
separate plumbing branch that put the orchestrator on the default branch early;
with the whole rollout landing from one branch, [[trigger-downstream]].ref
should just name where the consumer workflow will actually live, and there is
nothing to unwind at merge time.

The consequence is worth stating: on: workflow_run only ever executes the
default branch's copy of trigger-downstream.yml, so the fan-out cannot fire
until this branch merges. Each repo's own CI — resolve, build, test, publish —
is fully exercised on the branch; the cross-repo edges, including
eckit -> eccodes, are first exercised on develop after the merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eckit built and tested inside a SLURM job, linking the atos-hpc-gnu ecbuild and
stack-deps artifacts. The recipe runs ctest, so a green job proves those
artifacts link and run on a compute node rather than merely that they
downloaded.

`needs` names the HPC kinds, never the runner ones: the two lanes are separate
artifact universes distinguished by platform, and a runner rebuild must not be
what re-runs this. The compiler fields are discriminators for artifact identity
and dep matching, not the binaries the recipe invokes — the job loads whatever
prgenv/gnu provides, and they only have to agree with what the upstreams
published under.

The job runs in the internal-tools container for its cluster ssh identity, and
stages dep prefixes with install-python-deps: false — they are consumed on the
compute node, not in this container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Temporary, and paired with landing the generated orchestration on the default
branches ahead of the rest of the rollout.

on: workflow_run only ever executes the DEFAULT BRANCH's copy of
trigger-downstream{,-hpc}.yml, so the fan-out cannot be exercised from a branch
at all. The only way to see the full downstream CI before merging is to put the
orchestrator on the default branch and have it call consumer logic that still
lives here. [[trigger-downstream]].ref selects that: it pins the `uses:`, which
GHA forbids being an expression.

Runtime branch coordination needs no pinning and is unaffected: the orchestrator
passes branch: workflow_run.head_branch, pick-ref checks out the same-named
branch in each consumer, and resolve_deps resolves upstream manifests and
artifacts from it.

Flip to develop and regenerate when the rollout merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci.yml has run ctest here all along, but the generated cross-repo-trigger.yml
job only built and published. So when an upstream fanned out to us, a green
downstream/runner proved only that eckit still LINKED against the new
ecbuild and stack-deps -- never that it still worked.

Set `ctest = true` on [matrix.build] and regenerate. The step lands after the
publish step, matching ci.yml: whoever consumes our artifact picks it up as soon
as publish runs and never waits on these tests. Plain `ctest`, exactly what
ci.yml runs.

Also add .github/actionlint.yaml so the ARC scale-set labels stop reading as
unknown runners.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
Publish ran before Test, so a failing suite still put its output in the store —
and nothing downstream can tell the difference afterwards. resolve_deps and
fetch_deps both decide with a bare object_exists, and check_artifact stops asking
about the producing run the moment the object is found.

The orchestrator's `conclusion == 'success'` gate does not cover this: it stops
the fan-out from starting, not a rebuild-request waiter (whose poll returns as
soon as Publish completes, before ctest has begun) or a later run that finds the
object already present.

Swapping the two steps closes it — a failing step ends the job, so nothing is
uploaded. The cost is that consumers now wait for our tests.

`Print dependency table` moves above `Test` in the same pass. It does not
depend on the publish step, and having it run on a red suite is the point: it
names the upstream shas the failing build resolved against.

The generated cross-repo-trigger.yml carries the same swap, from the regenerated
ci-infrastructure. HPC is unaffected: those recipes already ctest before
`cmake --install`, and build-on-hpc publishes only after the job succeeds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
Every suite ran one test at a time. The builds are parallel — `cmake --build
--parallel $(nproc)` on the runner, `--parallel ${SLURM_NTASKS:-8}` on HPC —
but ctest defaults to serial and nothing overrode it, so the test phase left an
arc-runner-very-large (or an 8-task SLURM allocation) idle but for one process.

Visible in every run so far as cumulative time equal to wall-clock:

    ecbuild  gh-hosted   48 tests    80.65 sec*proc    80.70 sec real
    ecbuild  HPC gnu     48 tests   126.85 sec*proc   127.10 sec real
    eccodes  fan-out    121 tests    74.19 sec*proc    74.26 sec real

A ratio of 1.00 is the signature of no concurrency at all.

-j $(nproc) on the runner lanes and -j ${SLURM_NTASKS:-8} on HPC, mirroring
what the build step already asks for on each. Applied to ci.yml, the manifest's
ctest-args (so the fan-out matches) and the HPC recipes.

Not applied to ecflow in the same shape: its suite starts real servers, so it
takes the -j 8 its own cd-config.yml already uses, on its own commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
Repoints every container reference at the renamed Harbor projects:

    eccr.ecmwf.int/public-playground-ci/...  -> public-ci-images/...
    eccr.ecmwf.int/private-playground-ci/... -> private-ci-images/...

in .ci/manifest.toml (the `container` field on each matrix leg) and in ci.yml's
hardcoded resolve-job image. The "playground" name has outlived the playground.

Stage 3 of 3, and deliberately last: nothing may point at a project before the
images exist in it. ci-infrastructure's rename published all six public images
first (its own run is green and the repositories are visible in the registry),
then ci-container-images published internal-tools to private-ci-images. Every
image named here has been confirmed present before this commit was made.

No artifact churn: `container` is scheduling only and never enters artifact
identity, so nothing published under the old image names is invalidated and no
rebuild cascades from this.

Regenerating changes nothing, which is the expected result rather than a missed
step — the generated cross-repo-trigger workflows carry `image: ${{
matrix.container || '' }}` and resolve the image from the matrix at run time, so
the names live in the manifest alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
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.

2 participants