Skip to content

[Consumer] Harden the status.json consumer example: pointer fix, poll dedup, and an interactive dry-run - #91

Merged
HereThereBeDragons merged 5 commits into
developfrom
users/lpromber/improv_rocm_examples
Sep 2, 2026
Merged

[Consumer] Harden the status.json consumer example: pointer fix, poll dedup, and an interactive dry-run#91
HereThereBeDragons merged 5 commits into
developfrom
users/lpromber/improv_rocm_examples

Conversation

@HereThereBeDragons

@HereThereBeDragons HereThereBeDragons commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Upstreams the findings from ROCm/rocm-examples#505 into Quartz's own
status.json consumer docs and helper, and hardens the example against the
failure modes a real downstream poller hits.

The core bug: latest.json and prerelease/latest.json are git symlinks, and
raw.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 of
latest.json therefore returns that path, and parsing it as JSON fails. This
broke the most obvious way to consume the "latest" endpoint.

Read helper (scripts/consumer/read_status_json.py)

  • load_status now follows the symlink pointer transparently: on a JSON decode
    failure it checks whether the body is a bare <date>/status.json pointer,
    resolves it against the source URL with urljoin, and fetches the real
    document 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.
  • Added read_status_json_test.py covering 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:

  • Deduplication via actions/cache keyed on (rocm_version, build_date).
    restore is lookup-only (no download) and only probes; the marker is saved
    only after the React step succeeds, so a failed run is retried on the next
    poll rather than being marked done.
  • Concurrency: a static concurrency group serializes overlapping polls and
    lets 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.
  • Fire-and-forget caveat: a concurrency group only serializes runs of the
    same 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.yml sample.

The Python example (example_consume_status.py) was also hardened:

  • A transient fetch failure (status.json momentarily unavailable around a
    release) reports ready=false and exits 0 so the next poll retries.
  • An unsupported schema major is treated as permanent and fails loudly
    (sys.exit, non-zero): retrying cannot fix it, and a new major can move the
    fields the accessors read, so continuing would risk silently misreading the
    document.
  • The pip dry-run step now echoes the command and streams pip's output live, so
    the wait is visible, and targets a smaller device package (device-gfx1150)
    to keep the example quick.
  • Added example_consume_status_test.py (9 tests) covering the gate, the
    outputs glue, and the ready / not-ready / schema-fail branches.

Docs

  • README documents the latest.json pointer behavior and how to resolve it by
    hand if you fetch it yourself.
  • tutorial.md gains the "Dispatching a separate long-running workflow" section
    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.py against the live
    latest.json endpoint (confirms the pointer fix end to end)

Comment thread docs/status-json/README.md Outdated
Comment thread scripts/consumer/read_status_json.py
Comment thread docs/status-json/example_poll_status.yml Outdated
Comment thread docs/status-json/example_poll_status.yml Outdated
Comment thread docs/status-json/example_poll_status.yml Outdated
Comment thread docs/status-json/example_poll_status.yml
Comment thread docs/status-json/tutorial.md Outdated
Comment thread docs/status-json/tutorial.md Outdated
Comment thread docs/status-json/tutorial.md
Comment thread docs/status-json/tutorial.md Outdated
Comment on lines +210 to +211
```yaml
# my_build.yml - the long-running workflow the poll dispatches.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this block needed or rather, can we link / inline the example_poll_status.yml to not duplicate?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 zichguan-amd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ...

@HereThereBeDragons HereThereBeDragons Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }}

@HereThereBeDragons HereThereBeDragons Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

@HereThereBeDragons
HereThereBeDragons merged commit 910cc21 into develop Sep 2, 2026
2 checks passed
@HereThereBeDragons
HereThereBeDragons deleted the users/lpromber/improv_rocm_examples branch September 2, 2026 12:19
quartz-sync-github-app Bot pushed a commit that referenced this pull request Sep 2, 2026
910cc21, [Consumer] Harden the status.json consumer example: pointer fix, poll dedup, and an interactive dry-run (#91), Laura Promberger (laura.promberger@amd.com), Wed Sep 2 14:19:20 2026 +0200
zichguan-amd added a commit to ROCm/rocm-examples that referenced this pull request Sep 2, 2026
…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>
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.

4 participants