Skip to content

Sync branch ci rollout - #414

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

Sync branch ci rollout#414
mcocdawc wants to merge 21 commits into
developfrom
sync-branch-ci-rollout

Conversation

@mcocdawc

@mcocdawc mcocdawc commented Aug 21, 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/ecflow/pull-requests/PR-414

mcocdawc and others added 8 commits August 19, 2026 11:30
Migrates ecflow onto the manifest-driven CI in ecmwf/ci-infrastructure. The
legacy pipeline keeps running unchanged, moved to old_CI.yml (name: old CI) so
the new one can take the plain `CI` name that the generated downstream
orchestrators match their `workflow_run` trigger against.

ecflow does not depend on eckit: nothing in its CMake references it, so there
is no eckit edge in either direction. It does depend on stack-deps, for
pybind11 — ENABLE_PYTHON is on by default and cmake/Dependencies.cmake does a
find_package(pybind11 2.10.3 REQUIRED) under it.

Boost and OpenSSL come from the container: ENABLE_SSL is on by default, and
Boost is required outright. The build leg therefore uses the new
ubuntu24.04-gfortran13-boost image, while keeping platform = ubuntu-24.04 so
the ecbuild and stack-deps artifacts published under that slug are reused
rather than rebuilt — container is scheduling, not artifact identity.

Three configure flags are carried over from the repo's own ci-config.yml,
each needed because the toolchain here is the distro's: BOOST_ROOT=/usr
(Dependencies.cmake sets Boost_NO_SYSTEM_PATHS unconditionally, hiding the
apt-installed Boost), ENABLE_CONFIG_MODE_BOOST=OFF (ubuntu 24.04 ships CMake
3.28, below the 3.30 config mode targets), and ENABLE_STATIC_BOOST_LIBS=OFF.

ctest runs against the build tree, not the install tree: ecflow's tests link
against it and against static libraries that never reach the install prefix.

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>
ecflow built and tested inside a SLURM job, linking the atos-hpc-gnu ecbuild and
stack-deps (pybind11) artifacts. Two toolchains, matching the two the legacy
ci-hpc-config.yml exercised most recently — gnu-15.2.0 and intel-2025.3.1 — each
with its own recipe and its own platform slug, since an intel-built tree is a
different ABI from a gnu one and the two must never share an artifact name.

Boost, Python and Qt come from cluster modules rather than artifacts: ecflow
links Boost.Python against a specific interpreter and the cluster ships the
matched pair, which is also why the module versions are pinned rather than
floating. pybind11 still comes from stack-deps. The configure flags and the
ctest selection (-L nightly -E s_http) are carried over from the legacy config.

`needs` names HPC kinds only — the lanes are separate artifact universes. The
job runs in the internal-tools container for its cluster ssh identity.

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

codecov-commenter commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.32%. Comparing base (c581164) to head (aceaa6f).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #414   +/-   ##
========================================
  Coverage    52.31%   52.32%           
========================================
  Files         1251     1250    -1     
  Lines       102291   102288    -3     
  Branches     15202    15202           
========================================
+ Hits         53515    53520    +5     
+ Misses       48776    48768    -8     

☔ 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 21, 2026 13:59
The runner-lane build failed at configure with "CRITICAL - Qt5/6 not found -
this is required for ecFlowUI": ENABLE_UI is ON by default in CMakeLists.txt and
the build action never turns it off, but the pinned container carried only
gfortran-13 and Boost.

The runner lane was the odd one out, not the UI default. The HPC lane already
builds ecFlowUI via `module load qt/6.6.1` in .ci/hpc/build-{gnu,intel}.sh, and
the legacy downstream-ci pipeline built it too — .github/ci-config.yml never
passes -DENABLE_UI=OFF, so its image must have shipped Qt. Adding
-DENABLE_UI=OFF here would have unblocked the fan-out by silently narrowing the
runner lane to less than both pipelines it replaces, leaving ecFlowUI compiled
on HPC only.

So the image grows Qt6 instead: ecmwf/ci-container-images now publishes
ubuntu24.04-gfortran13-boost-qt6 (renamed from -boost, since that repo's
convention is that a variant's name says exactly what it adds over the base),
and this pins it. Nothing else moves — `container` is scheduling, not artifact
identity, and it is resolved from this manifest at runtime rather than baked
into the generated workflows, so no regeneration is needed.

The build action gains a comment recording why ENABLE_UI is left at its default
and where Qt comes from: that file is the first thing anyone opens when this
error recurs, and the answer lives two repos away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1jc9nyMBC8eQaxkF4vhoq
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 ecflow 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
That case cannot pass. libs/server/test/TestAuthentication.cpp is a single

    BOOST_CHECK_MESSAGE(false, "Test not implemented");

added on develop by d44e163 ("Refactor Access control", 2025-09-02) as a
placeholder. It is registered with LABELS unit, so the HPC recipes never execute
it (`ctest -L nightly -E s_http`) and only the runner lane trips over it — which
is why it surfaced now rather than months ago: nothing in the previous setup ran
the unit label on a path that gated anything.

Excluded in CI rather than fixed in the test source, because the stub belongs to
ecflow proper and the fix — marking the case
`*boost::unit_test::disabled()`, or implementing it — is an upstream change to
develop, not part of the CI rollout. Both the ci.yml step and the manifest's
ctest-args carry the exclusion so the push/PR build and the generated fan-out run
the identical suite; a comment in each names the commit and says to drop it once
the test exists, since from then on the exclusion, not the stub, is what would
hide a real regression.

This leaves the runner lane green: the other 52 failures in this job were
"/usr/bin/python3: No module named pytest" and are already gone now that the base
image ships python3-pytest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
libs/server/test/TestAuthentication.cpp was a single

    BOOST_CHECK_MESSAGE(false, "Test not implemented");

added by d44e163 ("Refactor Access control", 2025-09-02). It cannot pass, so
every run of the `unit` label that reaches it is red by construction. It went
unnoticed because the HPC recipes run `ctest -L nightly -E s_http` and never
execute it; the new runner-lane ctest does, which is what surfaced it.

Deleted rather than skipped. The previous commit excluded it with
`ctest -E u_server_authentication` in both ci.yml and the manifest's ctest-args,
which is carrying CI machinery — in two places, kept in step by hand — to hide a
test that asserts its own absence. An empty placeholder is not a test, and it
should not sit on develop in a state where implementing it is the only way to
make the suite honest.

Removes both sources (the case and its Boost entry point, neither shared with
another target) and the ecbuild_add_test/target_clangformat pair that registered
it, and reverts the CI exclusion in the same pass, so nothing is left referencing
the name. Authentication coverage, when it is written, comes back as a real test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
The runner lane ran a bare `ctest`, on the reasoning that it should match
ci.yml. Wrong reference point: what this lane replaces is
.github/ci-config.yml, the legacy downstream-ci runner config, and that gates on

    ctest_options: >-
      -L nightly
      -E s_test|s_zombies

`-L nightly` is the set the old pipeline actually required to be green;
s_test and s_zombies (libs/test/overall) are long-running system suites it
deliberately skips. Running everything unfiltered meant this lane demanded more
than the pipeline it is meant to reproduce, so failures here did not mean a
regression — they meant we had opted into suites nobody gated on.

Also sets -DENABLE_ALL_TESTS=ON in the build composite. The legacy config passes
it, and so do our own .ci/hpc/build-{gnu,intel}.sh, which left the runner lane as
the one place in the rollout configuring a smaller suite than everywhere else —
the opposite error to the ctest filter, and easy to miss because the two cancel
out in the totals.

Both ci.yml and the manifest's ctest-args carry the same filter so the push/PR
build and the generated fan-out gate identically. Quoted, or the shell reads the
`|` as a pipe. The HPC recipes keep their own `-E s_http`, which is a separate
cluster-specific narrowing from .github/ci-hpc-config.yml, not this one.

Note this also means the u_server_authentication placeholder removed in the
previous commit would never have run here: it carried LABELS unit, not nightly.
It was still right to delete — a test that asserts its own absence has no
business on develop — but the failure that surfaced it was ours, not ecflow's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
Two bugs, both from loading UNVERSIONED prgenv modules while pinning versioned
boost/python/qt around them.

gnu: CMake was left to its default compiler search and picked /usr/bin/g++ —
the compute node's system gcc 8.5.0 — while this recipe hardcodes
Boost_ROOT=.../boost/1.90.0/GNU/15.2. The link failed with

    libboost_program_options.so.1.90.0: undefined reference to
      `std::ios_base_library_init()@GLIBCXX_3.4.32'

because that symbol is GCC 13+ libstdc++. So the HPC gnu leg was never a test
failure at all: ecflow never linked, and no artifact was ever produced. Pinned to
gcc/15.2.0 with explicit CMAKE_C/CXX_COMPILER, matching build.hpc.sh's
load_gcc15_2() and the legacy `gnu-15.2.0` platform. The intel leg escaped this
only because it passed CMAKE_CXX_COMPILER=icpx.

intel: `module load prgenv/intel` resolved to IntelLLVM 2021.4.0, not the
2025.3.1 this recipe's header claimed, while hardcoding the INTEL/2025.3 Boost
prefix. It linked, so the mismatch was invisible — but 2021.4.0 is a different
row of the legacy test table (`-E '(py3_|s_http)'`, Python tests disabled too),
so we were running a suite that toolchain is not expected to pass. Pinned to
prgenv/intel-llvm + intel/2025.3.1, which is what build.hpc.sh's
load_intel2025_3() uses — note prgenv/intel-llvm, not prgenv/intel.

Also adds aocc-4.0.0 and nvidia-24.11 legs. These demonstrate omitting ctest
rather than disabling it: the legacy ci-hpc-config.yml has to write
`ctest_options: --version` — invoking ctest with a flag that makes it print its
version and exit — because that config has nowhere to say "no tests". Our
recipes own the test invocation, so not testing is the absence of a ctest line:
nothing to disable, no sentinel flag, and the reader sees build then install with
nothing between them. On the runner lane the same thing is said by omitting
`ctest = true` from [matrix.build].

nvidia-24.11 rather than the config's nvidia-22.11: build.hpc.sh only knows
nvidia/24.1, 24.11 and 25.11, so 22.11 looks retired and `module load
nvidia/22.11` would fail at job start. 24.11 is in both the legacy config and
build.hpc.sh, and is equally tests-disabled, so it demonstrates the same point on
a toolchain that provably exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
@mcocdawc
mcocdawc force-pushed the sync-branch-ci-rollout branch from eaad471 to 2cf4fe9 Compare August 24, 2026 11:49
ecflow's suite ran one test at a time: 71 tests, 500.34 sec*proc, 500.38 sec
real — a ratio of 1.00, the whole machine idle but for one process. The build
step was already parallel, so only the test phase wasted the runner.

-j 8 rather than the $(nproc) the other repos take. ecflow's tests are not
independent the way ecbuild's nested cmake configures are: 460 of those 500
seconds are 7 `integration` tests that start real ecFlow servers, and parallel
execution risks collisions on ports and on-disk state. 8 is not a guess — it is
the width ecflow's own .github/cd-config.yml already uses for every HPC stage,

    test_command: "ctest --output-on-failure -j 8 -L nightly"

so it is a concurrency this suite is already run at in this project.

Applied to ci.yml, the manifest's ctest-args (so the fan-out matches) and both
testing HPC recipes. The aocc and nvidia recipes are untouched: they run no tests
at all.

On its own commit, separate from the other three repos', so that if this does
surface a flake it is unambiguous which change caused it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
mcocdawc and others added 3 commits August 24, 2026 15:39
It is the sole failure on that lane, and it TIMES OUT rather than asserting:

    99% tests passed, 1 tests failed out of 79
    The following tests FAILED:
         74 - py3_s_TestClientApi (Timeout)

Worth recording where it came from: it is one of the `integration python nightly
slow` cases that start a real ecFlow server, registered only under
`if (ENABLE_ALL_TESTS AND ENABLE_SERVER)`. It appears on this lane because we
turned -DENABLE_ALL_TESTS=ON to match the legacy runner config — 79 tests here,
up from 72 — so this is coverage newly gained, not newly broken.

The exact target name matters. ctest -E is a REGEX matched against the test name,
so a near-miss such as `py3_s_Test_Client` matches nothing and silently leaves the
test running — a filter that looks applied and is not. The three py3_s_* targets
this repo builds are TestClientApi, TestPythonChildApi and TestSslSetup; only the
first fails.

Applied to ci.yml and to the manifest's ctest-args, which are kept identical so
push/PR and the generated fan-out gate on the same set. The HPC recipes keep
their own `-E s_http` and are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZXmNdJJYqZkpBNTycoPdP
Reverts the -DENABLE_ALL_TESTS=ON I added to match the legacy runner config.
Turning it on registered the `integration python nightly slow` py3_s_* group
(72 tests -> 79), and py3_s_TestClientApi in that group times out. Tuning those
timeouts belongs to ecflow's maintainers, not to the CI rollout, so this lane
stays on the default set until they get to it.

That also drops the `-E py3_s_TestClientApi` exclusion added in the previous
commit. With ENABLE_ALL_TESTS off, that target is not registered at all — the
group is behind `if (ENABLE_ALL_TESTS AND ENABLE_SERVER)` — so the filter would
match nothing. An exclusion that looks applied and does nothing is exactly the
trap flagged when it went in, so it goes rather than sitting there inert.

The .ci/hpc recipes keep ENABLE_ALL_TESTS=ON: they are a separate lane with its
own failures still under investigation, and narrowing them here would confuse
that.

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