Skip to content

feat(release): publish devices.json as Release asset on v* tag push - #16

Merged
tclancy merged 3 commits into
mainfrom
claude/15-release-publish-devices-json
Jul 20, 2026
Merged

feat(release): publish devices.json as Release asset on v* tag push#16
tclancy merged 3 commits into
mainfrom
claude/15-release-publish-devices-json

Conversation

@metaframework-dispatch-bot

Copy link
Copy Markdown

Summary

Adds .github/workflows/release.yml — on every v* tag push, runs the exporter regression test, builds devices.json from devices/*.yaml, and attaches it to the tag's GitHub Release as an asset. Downstream consumers (the extracted parsons-remote PWA, next in the extraction sequence) fetch a stable, immutable URL at build time so the derived JSON never needs to be committed alongside its source YAML.

Why now / how this fits

PR 1 of the 3-PR extraction of the current homelab services/22-parsons-remote/ PWA into its own repo — driven by homelab#256. Tom's picks on that scoping memo:

  • Deploy shape: B — homelab fetches a release artifact (container C is the eventual long-term shape).
  • devices.json flow: α — radiofrequency CI publishes the derived JSON on tag push, downstream fetches by version.
  • New repo name: parsons-remote.

Design

  • Trigger: on: push: tags: ['v*'] — decoupled from ci.yml (which stays PR/main-push driven).
  • Idempotency: gh release create --generate-notes || true + gh release upload --clobber. Re-runs on the same tag are safe.
  • Silent-success gate: before building the artifact, the workflow runs the existing tests/test_export_web_devices.py regression fence (shape / sort / JSON-serializability) so a subtle exporter break can't ship valid-JSON-but-wrong-shape. Fixes a code-reviewer finding aligned with the "ansible changed=0" class-of-bug in our own durables.
  • Permissions: default contents: read at workflow scope; contents: write scoped to the publish job only.
  • Supply chain: third-party actions pinned to full commit SHAs with version comments, matching the fleet standard (actions/checkout@v7.0.0 SHA, astral-sh/setup-uv@v8.3.2 SHA). gh is a pre-installed runner tool, no new action to pin.

Test plan

  • Local uv run pytest -q — 65 passed (see inline showboat below).
  • Local dry-run of the exporter — 4 lights (window, couch, speaker, chairs), 14 KB JSON.
  • Workflow syntax validated with python -c "import yaml; yaml.safe_load(open(...))".
  • Full end-to-end verification happens when Tom cuts the first v0.1.0 tag on main — that's the only way to exercise the tag-push trigger against the real GitHub Release API.
Captured test run (showboat)

Issue #15 test run — release workflow local verification

2026-07-19T18:37:37Z by Showboat 0.6.1

uv run pytest -q
warning: `VIRTUAL_ENV=/Users/tom/Documents/work/metaframework/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
.................................................................        [100%]
65 passed in 0.23s
uv run python scripts/export_web_devices.py > /tmp/rf-devices.json && wc -c /tmp/rf-devices.json && head -3 /tmp/rf-devices.json
warning: `VIRTUAL_ENV=/Users/tom/Documents/work/metaframework/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
   14393 /tmp/rf-devices.json
{
  "lights": [
    {
uv run python -c 'import json; d = json.load(open("/tmp/rf-devices.json")); lights = d["lights"]; print(f"lights: {len(lights)}"); print("units:", [l["unit"] for l in lights])'
warning: `VIRTUAL_ENV=/Users/tom/Documents/work/metaframework/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
lights: 4
units: ['window', 'couch', 'speaker', 'chairs']

Out of scope (explicit)

  • SHA-pin sweep of existing ci.yml — orthogonal; agent.md rule is "pin opportunistically when you touch a repo's CI" and this PR adds a new workflow rather than editing the existing one.
  • Cutting a v* tag — that's Tom's call after this lands.
  • PR 2 (new parsons-remote repo, fetches the artifact at build) and PR 3 (homelab swaps the copy task for a release-artifact download, deletes the old tree) — filed as separate tracker issues.

Closes #15
Refs homelab#256

dispatch-bot[bot] added 2 commits July 19, 2026 14:34
Adds .github/workflows/release.yml — on v* tag push, exports the PWA
device bundle via scripts/export_web_devices.py and attaches devices.json
to the tag's GitHub Release.

Prerequisite for extracting the parsons-remote PWA into its own repo
(homelab#256). Downstream repos fetch this artifact at build time so the
JSON never has to be committed alongside its source YAML — derive-don't-
duplicate stays enforced.

Uses `gh release create --generate-notes || true` + `gh release upload
--clobber` for a two-step idempotent publish that survives re-runs of
the same tag without third-party actions to pin.

Refs #15
…n sanity check

Addresses code-reviewer feedback on the release workflow:

1. Silent-success class-of-bug (ansible changed=0 shape from memory.md): the
   sanity check only asserted `lights` was non-empty, so a subtly broken
   exporter (empty commands dict, PT2260 filter regression) would produce
   valid JSON with lights but wrong shape. Now runs the existing
   test_export_web_devices.py fence before building — 5s cost, closes the gap.

2. Sanity-check `python` was the system Python (currently 3.12 on ubuntu-latest),
   not the uv-managed 3.13 the rest of the workflow uses. Switched to
   `uv run python` so one interpreter runs everything; also tightened the shape
   assertion to check each entry has unit + non-empty commands.

3. `uv sync --frozen` bumped to `--all-groups` so pytest is available.
   Matches the fleet-standard pattern in ci.yml.

Refs #15
@metaframework-dispatch-bot

Copy link
Copy Markdown
Author

Note: GHA minutes are exhausted for July per the 14:25 ET PM tick — this PR's own release.yml won't get to exercise itself on tag push until Aug 1 reset, and the existing ci.yml on the PR will fail red for the same reason. Local verification (inlined showboat above) stands as the authoritative evidence per agent.md L1170; no need to wait on GHA before merging if the diff reads clean. First actual end-to-end run happens when a v0.1.0 tag is cut post-reset.

@tclancy

tclancy commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Looks good with one question: given a situation like right now where we are out of Github Action Minutes until August,

  1. What would happen if this fails in terms of versioning, etc?
  2. Can we move most of the logic into a bash script so it's do-able locally?

…elease.sh

Tom's Q2 on #16: "can we move most of the logic into a bash script so it's
do-able locally?" — yes.

release.yml now runs setup steps (checkout, uv install, uv sync --frozen)
then calls `bash scripts/publish-devices-release.sh`. The script owns the
exporter regression fence, JSON build, JSON sanity check, and idempotent
`gh release create || true` + `gh release upload --clobber` — one source
of truth for CI and local runs.

Local usage:  TAG=v0.1.0 scripts/publish-devices-release.sh
CI usage:     TAG="$GITHUB_REF_NAME" scripts/publish-devices-release.sh

Falls back to `git describe --tags --exact-match HEAD` when TAG is unset
locally so a tagged HEAD works without ceremony.

Refs #15
@metaframework-dispatch-bot

Copy link
Copy Markdown
Author

On the two questions:

Q1 — versioning if the workflow fails:

Bit of good news buried under the cap situation: radiofrequency is public, so it's on GitHub's unlimited-minutes tier for public repos. The July private-repo minute exhaustion doesn't touch this workflow — it will run on the first v* tag push whenever you cut it.

Even for the hypothetical "workflow failed for some other reason" case, the tag doesn't lose anything:

  • git push --tags succeeds regardless of Actions state — the tag exists in the repo.
  • A failed workflow means no release is created and no devices.json asset is attached — no half-published state.
  • Re-running is safe: gh release create --generate-notes || true + gh release upload --clobber are both idempotent, so kicking the workflow again (or now: running the script locally) on the same tag is fine.

So worst case is "tag exists but no asset yet"; downstream fetches would 404 until re-run — same failure mode as a network blip during upload. No orphan releases, no version skew.

Q2 — extract to a bash script:

Done in 3727bc0. New file scripts/publish-devices-release.sh owns the sequence:

  1. uv run pytest tests/test_export_web_devices.py -x (regression fence)
  2. uv run python scripts/export_web_devices.py > devices.json (build)
  3. inline python sanity check (has lights, each has unit+commands)
  4. gh release create ... || true (idempotent)
  5. gh release upload ... --clobber (idempotent re-upload)

Workflow now just does setup (checkout, uv install, uv sync --frozen --all-groups) and calls the script — single source of truth for both CI and local.

Local invocation:

TAG=v0.1.0 scripts/publish-devices-release.sh
# or, with a tagged HEAD checked out:
git checkout v0.1.0 && scripts/publish-devices-release.sh

The TAG env var falls back to git describe --tags --exact-match HEAD when unset, so a tagged checkout works with no ceremony. gh uses your local gh auth, so you don't need to pass a token.

Ready for merge whenever you are — you can cut v0.1.0 immediately after merge and either wait for the workflow or run the script locally to prove the end-to-end.

@tclancy
tclancy merged commit 7fde346 into main Jul 20, 2026
1 check passed
@tclancy
tclancy deleted the claude/15-release-publish-devices-json branch July 20, 2026 01:12
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.

CI: publish devices.json as a GitHub Release asset on tag push

1 participant