[Consumer] Harden the status.json consumer example: pointer fix, poll dedup, and an interactive dry-run - #91
Conversation
| ```yaml | ||
| # my_build.yml - the long-running workflow the poll dispatches. |
There was a problem hiding this comment.
Is this block needed or rather, can we link / inline the example_poll_status.yml to not duplicate?
There was a problem hiding this comment.
shortened it. its not identical. it talks about how in workflow dispatch workflows you need another cache marker/concurrency group (thanks also found the bug where i missed to change the concurrency group name)
zichguan-amd
left a comment
There was a problem hiding this comment.
Thanks for upstreaming this in Quartz, LGTM
| # group is static because build_date is not known yet when concurrency is | ||
| # evaluated. Note this only serializes runs of THIS workflow, not any separate | ||
| # workflow it might dispatch (see the react step below). | ||
| concurrency: |
There was a problem hiding this comment.
concurrency:
queue: max
should be better according to https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency#using-concurrency-in-different-scenarios . If a long-running poll is active, a manual forced run is pending, and the next scheduled poll arrives, GitHub cancels the pending forced run and replaces it from what I can tell. Also maybe use "group: ${{ github.workflow }}-poll-rocm-nightly" to avoid collisions ...
There was a problem hiding this comment.
ah down the rabbit hole :)
To conditionally cancel currently running jobs or workflows in the same concurrency group, you can specify cancel-in-progress as an expression with any of the allowed expression contexts.
how about instead
cancel-in-progress: ${{ inputs.force }}
so that the queue is the normal "last request to rerun" and if you want a manual forced run it cancels the ongoing one?
can add the link to the concurrency page for workflows: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency
for people to discover more
leaving it as an exercise for the downstream user:
you can extend this even further of creating totally separate groups depending if scheduled or not. needs to be then propagated to workflow dispatch though.
concurrency:
group: poll-rocm-nightly-${{ inputs.force == true && 'manual' || 'scheduled' }}
cancel-in-progress: ${{ inputs.force }}
There was a problem hiding this comment.
thinking about it a bit more:
i think for this example it is better to remove the input for force overwrite.
if anyone wants it they can come up with it and its implications themselves
There was a problem hiding this comment.
Also maybe use "group: ${{ github.workflow }}-poll-rocm-nightly" to avoid collisions ...
i would leave as-is to stay minimal. we have a section in the tutorial about concurrency groups. people can have a look there. if you start with concurrency groups you should know at least a little bit about it...
…tly (v2) (#512) ## Summary Adds a standalone nightly workflow that tests rocm-examples against the **latest READY** ROCm nightly reported by [Quartz](https://github.com/ROCm/Quartz) (ROCm's CI/CD data hub), running the expensive matrix **once per new version on success** — a passing version is recorded and skipped thereafter, while a failing one is re-tested on later polls until it passes or the next nightly supersedes it. This is the sparse-checkout revision of #505: instead of vendoring Quartz's reader, the workflow reuses it directly from ROCm/Quartz at a pinned commit (unblocked by ROCm/Quartz#91). Co-authored with @zichguan-amd. - **Gate:** resolves Quartz `release-nightly/latest.json` and requires the Linux **ROCm build** to be `success`. It deliberately does *not* gate on `overall_status`, which folds in every pipeline/phase and is routinely red from unrelated test failures. - **Dedup:** a lookup-only `actions/cache` marker keyed on `(rocm_version, build_date)`, written **only on a green run**, skips a version once it has passed; a failing version stays unmarked and is re-tested on later polls (bounded — see *Record on pass only*). - **Fail-safe:** an in-progress build or an unreachable Quartz yields `resolved=false` and the run does nothing — doubling as a Quartz availability canary. ### Files - `read_status_json.py` is **not vendored**. The `gate` job sparse-checks out the single file from ROCm/Quartz (`scripts/consumer/read_status_json.py`) at pinned commit `910cc21e` (the ROCm/Quartz#91 merge, which ships the schema-v2 reader that follows the `latest.json` symlink) and puts it on `PYTHONPATH`. Bump the SHA to pick up reader fixes or a schema-major update. - `.github/quartz/consume_status.py` — the consumer/gate: imports Quartz's reader (via `PYTHONPATH`) and calls `load_status`, which follows the `latest.json` symlink pointer internally; an unsupported schema **major** is a hard failure (not a soft skip); requires `linux.rocm.build == success`; emits `resolved/rocm_version/build_date/source/wheels_url/tarballs_url` to `$GITHUB_OUTPUT`. - `.github/quartz/tests/test_consume_status.py` — unit tests for the gate/resolve logic (good / not-ready / unavailable / bad-schema-major-exits / real-bug-propagates / absent platform). Requires `PYTHONPATH` pointed at a Quartz checkout (see the module docstring: `git clone --depth 1 https://github.com/ROCm/Quartz /tmp/quartz`). - `.github/workflows/quartz_test.yml` — polls every 2h `cron "30 1-11/2 * * *"` (01:30–11:30 UTC, around the ~02:30 build-green time) + `workflow_dispatch` (`force`). Jobs: **gate** (sparse-checkout reader + consume + cache dedup + matrix), **test** (reusable build), **record** (marker on pass only), **report** (dynamic skip titles). - `.github/workflows/build-rocm-examples-reusable.yml` — new optional `rocm_version` input that pins the exact nightly for both whl (`==<ver>`) and tarball (exact filename) installs, plus `wheels_url`/`tarballs_url` inputs so the install index/base come straight from Quartz's published URLs; all empty preserves today's latest-index behavior, so existing callers are unaffected. - `.github/build_tools/configure_ci.py` — new opt-in `--exclude-preinstalled` flag that drops preinstalled-only images (the version-pinned stable image) from the matrix. Defaults off, so `ci_nightly.yml` is unaffected. ### Design decisions - **Reuse the Quartz reader via sparse checkout, don't vendor it** — a single-file sparse checkout at a pinned SHA keeps the reader byte-identical to upstream with no copy to re-sync on schema bumps; the pin makes reader updates an explicit SHA bump. `SUPPORTED_SCHEMA_MAJOR` in the consumer and the pinned SHA must be bumped together on a schema-major change. - **Record on pass only** — a green run is the sole conclusive "tested." Any failure (transient infra *or* a real build/test break) leaves the version unrecorded so the next poll re-tests it, rather than masking it until the 7-day cache eviction. Retry is bounded: `resolve()` always prefers the newest ready build, so a genuinely broken nightly is superseded within the morning poll window, not retried hourly. - **Matrix = full nightly minus preinstalled images** — 4 multi-arch distros × {gfx1100, gfx1151} × {whl, tarball} = 16 legs. The version-pinned stable image is excluded (via `configure_ci.py --exclude-preinstalled`) because its ROCm version is baked in and unrelated to the nightly we pin. - **Poll window** — every 2h from 01:30–11:30 UTC. The Linux ROCm build starts ~00:02 and goes green ~02:24–03:09 on a normal night, so 01:30 sees it in progress (cheap skip), 03:30 usually catches it green, and the morning tail absorbs publish lag. A morning finish drops only the rare evening re-run, which self-heals when the next nightly supersedes it. - **Cache eviction** — the 7-day marker eviction is accepted (a stale week means one harmless re-run). - **`repository_dispatch`** (Quartz pushing on each build, removing polling+dedup) is deferred as a follow-up. ## Test plan - [x] `consume_status.py` unit tests pass (`PYTHONPATH=<quartz>/scripts/consumer python3 -m unittest discover -s .github/quartz/tests`) - [x] Consumer resolves the live Quartz `latest.json` (build=success) and emits the pinned version + install URLs - [x] `configure_ci.py --exclude-preinstalled` drops the preinstalled image and keeps `distros`/`distro_map` in sync - [x] Both workflow YAMLs parse - [x] Triggered run: gate resolves and the matrix pins the exact nightly (observed — gate resolved `10.1.0a20260902`; whl leg installed `rocm==10.1.0a20260902`) - [ ] `record` writes the dedup marker on a **green** run (blocked: current nightly fails to build due to TheRock `libhipcxx`/`libomp` packaging, so no green run yet) - [ ] Second immediate run shows the **skip** (already-tested) report --------- Co-authored-by: zichguan-amd <zichuan.guan@amd.com> Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
Summary
Upstreams the findings from ROCm/rocm-examples#505 into Quartz's own
status.jsonconsumer docs and helper, and hardens the example against thefailure modes a real downstream poller hits.
The core bug:
latest.jsonandprerelease/latest.jsonare git symlinks, andraw.githubusercontent.com serves a symlink as its target path (a one-line
body like
20260707/status.json), not the file it points to. A plain fetch oflatest.jsontherefore returns that path, and parsing it as JSON fails. Thisbroke the most obvious way to consume the "latest" endpoint.
Read helper (
scripts/consumer/read_status_json.py)load_statusnow follows the symlink pointer transparently: on a JSON decodefailure it checks whether the body is a bare
<date>/status.jsonpointer,resolves it against the source URL with
urljoin, and fetches the realdocument once. Anything that is not clearly a pointer re-raises the original
error, so genuine malformed JSON still surfaces. Local paths follow symlinks
natively and never take this fallback.
read_status_json_test.pycovering the pointer-resolution path.Poll workflow and consumer example (the larger change)
The workflow side is where most of the work went, because a naive poller either
re-triggers on every poll or double-processes a build:
actions/cachekeyed on(rocm_version, build_date).restoreis lookup-only (no download) and only probes; the marker issavedonly after the React step succeeds, so a failed run is retried on the next
poll rather than being marked done.
concurrencygroup serializes overlapping polls andlets an in-flight run finish. This is what makes a single save-after marker
sufficient even when the work outlives the poll interval: React always writes
its marker before the next poll starts.
concurrencygroup only serializes runs of thesame workflow. If the poll dispatches a separate long-running workflow, that
run is independent and the poll's serialization does not extend to it. The
tutorial documents the fix (move the marker into the dispatched workflow, key
its own concurrency per build) with a worked
my_build.ymlsample.The Python example (
example_consume_status.py) was also hardened:release) reports
ready=falseand exits 0 so the next poll retries.(
sys.exit, non-zero): retrying cannot fix it, and a new major can move thefields the accessors read, so continuing would risk silently misreading the
document.
the wait is visible, and targets a smaller device package (
device-gfx1150)to keep the example quick.
example_consume_status_test.py(9 tests) covering the gate, theoutputs glue, and the ready / not-ready / schema-fail branches.
Docs
latest.jsonpointer behavior and how to resolve it byhand if you fetch it yourself.
and the per-workflow marker guidance.
Test plan
python3 -m unittest discover -s docs/status-json/tests -p '*_test.py'python3 -m unittest discover -s scripts/consumer/tests -p '*_test.py'python3 docs/status-json/example_consume_status.pyagainst the livelatest.jsonendpoint (confirms the pointer fix end to end)