From 95bae1c1aa77bb5a8a4b59be78d5831824db528c Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Sat, 29 Aug 2026 03:08:22 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(ci):=20registry=E2=86=92trunk=20catalo?= =?UTF-8?q?g=20sync=20automation=20+=20drift=20alarm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Module registration is two merges (registry PR + trunk fixture/catalog sync); the docs deploy regenerates the published catalog from the LIVE registry, so a missed half silently dropped the module from the site (happened with mighty, 2026-08-29). Two mechanical guards: - deploy_docs_from_develop: drift alarm step — gen_docs_catalog --check against a fresh registry clone (empty modules-dir, the committed-page variant); on mismatch emits a warning annotation, a job summary, and files one docs-catalog-drift issue. Never fails the deploy. - sync-modules-index (new, daily + dispatch): mirrors registry entries into tests/meta/fixtures/modules_index/, regenerates docs/modules/, bumps VERSION for the increment gate, and opens/refreshes the bot/sync-modules-index PR (scripts/registry_sync.py, locally testable). AGENTS.md + create-module/extract-module skills updated to point at the automation. VERSION 0.20.0-alpha.21. Co-Authored-By: Claude Fable 5 --- .agents/skills/create-module/SKILL.md | 8 +- .agents/skills/extract-module/SKILL.md | 10 ++ .env | 2 +- .../workflows/deploy_docs_from_develop.yaml | 35 ++++ .github/workflows/scripts/registry_sync.py | 149 ++++++++++++++++++ .github/workflows/sync-modules-index.yml | 67 ++++++++ AGENTS.md | 3 +- docs/release_notes/index.md | 11 ++ 8 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/scripts/registry_sync.py create mode 100644 .github/workflows/sync-modules-index.yml diff --git a/.agents/skills/create-module/SKILL.md b/.agents/skills/create-module/SKILL.md index ae255d40b..ae3d4901e 100644 --- a/.agents/skills/create-module/SKILL.md +++ b/.agents/skills/create-module/SKILL.md @@ -132,8 +132,12 @@ so only deviations appear in stack files.) regenerated with `tools/gen_docs_catalog.py`, mkdocs nav updated). The docs deploy regenerates the catalog from the LIVE registry, so the trunk PR alone leaves the published catalog without your module — merge the - registry PR first. Full checklist + recovery command: the - [extract-module](../extract-module/SKILL.md) registration step. + registry PR first. The trunk PR is automated: after the registry PR + merges, dispatch the `sync-modules-index` workflow (also runs daily) and + merge the PR it opens; the develop docs deploy files a + `docs-catalog-drift` issue if the two ever disagree. Full checklist + + caveats: the [extract-module](../extract-module/SKILL.md) registration + step. ## References diff --git a/.agents/skills/extract-module/SKILL.md b/.agents/skills/extract-module/SKILL.md index c9aa48c54..aa4abab65 100644 --- a/.agents/skills/extract-module/SKILL.md +++ b/.agents/skills/extract-module/SKILL.md @@ -193,6 +193,16 @@ delete, written for the orchestrator of that PR. Include: with** the trunk PR; if it lands late, re-run the deploy: `gh workflow run deploy_docs_from_develop.yaml --repo castacks/AirStack --ref develop`. + **Automation (2026-08-29):** the trunk half no longer needs to be + hand-built — after the registry PR merges, dispatch the + `sync-modules-index` workflow (Actions tab; also runs daily) and it opens + the trunk sync PR (fixture mirror + regenerated pages + VERSION bump). + The develop docs deploy independently raises a `docs-catalog-drift` issue + whenever the committed catalog and the live registry disagree, so a missed + sync can no longer stay silent. Caveat: the bot PR is opened with the + workflow token, which does not trigger CI — close and reopen it to run + the checks. + ## References - [create-module](../create-module/SKILL.md) — manifest, anatomy, canonical-defaults rule diff --git a/.env b/.env index efc34f962..0d6edf834 100644 --- a/.env +++ b/.env @@ -12,7 +12,7 @@ PROJECT_NAME="airstack" # If you've run ./airstack.sh setup, then this will auto-generate from the git commit hash every time a change is made # to a Dockerfile or docker-compose.yaml file. Otherwise this can also be set explicitly to make a release version. # auto-generated from git commit hash -VERSION="0.20.0-alpha.20" +VERSION="0.20.0-alpha.21" # Image-tag discriminator ONLY (appears in the image tag suffix, e.g. ..._robot-x86-64_dev). # No Dockerfile consumes it: "prebuilt" does NOT bake the built ros_ws into the image today — # a real prebuilt (workspace-baked) stage is future work. Keep "dev" (mounted code, built live). diff --git a/.github/workflows/deploy_docs_from_develop.yaml b/.github/workflows/deploy_docs_from_develop.yaml index 191d83f71..686132fc0 100644 --- a/.github/workflows/deploy_docs_from_develop.yaml +++ b/.github/workflows/deploy_docs_from_develop.yaml @@ -20,6 +20,7 @@ on: - cron: "17 6 * * 1" # weekly, Mondays 06:17 UTC permissions: contents: write + issues: write # drift alarm files an issue when the committed catalog lags the registry jobs: deploy: runs-on: ubuntu-latest @@ -37,6 +38,40 @@ jobs: run: | pip install mkdocs-material mkdocs-same-dir mkdocs-redirects pyyaml pip install pillow cairosvg mike + # DRIFT ALARM: the committed docs/modules pages + the fixture index + # (tests/meta/fixtures/modules_index/) must mirror the LIVE registry. + # A merged trunk PR with an unmerged registry PR — or vice versa — + # otherwise publishes a catalog that silently disagrees with git + # (this happened with `mighty`, 2026-08-29). Develop deploy only: + # main/release committed pages intentionally lag develop. NEVER fails + # the deploy (the regeneration below publishes the current catalog + # regardless); it just makes the mismatch loud — a warning annotation, + # a job summary, and at most one open issue. The sync-modules-index + # workflow opens the PR that clears it. + - name: Catalog drift alarm (committed pages vs live registry) + env: + GH_TOKEN: ${{ github.token }} + run: | + rm -rf .modules-index-drift + git clone --depth 1 https://github.com/castacks/airstack-modules-index .modules-index-drift \ + || { echo "skipped: registry unreachable — no drift verdict"; exit 0; } + mkdir -p /tmp/no-fetched-modules + if python3 tools/gen_docs_catalog.py --index .modules-index-drift \ + --modules-dir /tmp/no-fetched-modules --check 2> drift.txt; then + echo "committed catalog is in sync with the live registry" + exit 0 + fi + echo "::warning title=Module catalog drift::committed docs/modules + fixture disagree with the live registry. The published catalog is regenerated and current; trunk needs a sync PR — run the sync-modules-index workflow." + { echo "## Module catalog drift"; echo; echo '```'; head -c 5000 drift.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + if [ -z "$(gh issue list --label docs-catalog-drift --state open --json number --jq '.[].number')" ]; then + gh label create docs-catalog-drift --color D93F0B \ + --description "Committed module catalog disagrees with the live registry" 2>/dev/null || true + gh issue create --title "Module catalog drift: committed docs/modules disagrees with the live registry" \ + --label docs-catalog-drift \ + --body "$(printf 'Detected by the develop docs deploy: %s/%s/actions/runs/%s\n\nThe committed docs/modules pages + tests/meta/fixtures/modules_index/ do not match regeneration from the live airstack-modules-index (usually a registry PR merged without its trunk sync, or the reverse). The published develop catalog is regenerated live and stays correct; git is what lags.\n\nFix: run the **sync-modules-index** workflow (Actions tab) and merge its PR — or sync manually per the extract-module skill registration step.\n\n```\n%s\n```' "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID" "$(head -c 5000 drift.txt)")" + else + echo "drift issue already open — not filing a duplicate" + fi # RFC #379 §9: module docs ride the docs deploy. Shallow-clone the # registry index and each REGISTERED module repo at its registered_ref # into the gitignored modules/ dir, then regenerate docs/modules/ so diff --git a/.github/workflows/scripts/registry_sync.py b/.github/workflows/scripts/registry_sync.py new file mode 100644 index 000000000..190d9a523 --- /dev/null +++ b/.github/workflows/scripts/registry_sync.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Sync trunk's committed module catalog from a registry checkout. + +Module registration is TWO merges (registry PR + trunk PR — see the +extract-module skill). This script produces the trunk half mechanically from +a local checkout of castacks/airstack-modules-index: + +1. mirror the registry's ``modules/*.yaml`` and ``stacks/*.yaml`` into the + contract-test fixture ``tests/meta/fixtures/modules_index/`` (deleting + entries the registry no longer has); +2. regenerate the committed ``docs/modules/`` pages with + ``tools/gen_docs_catalog.py`` against that fixture and an EMPTY + fetched-modules dir (the committed-page variant the docs-catalog contract + test expects); +3. with ``--bump``, and only when step 1/2 changed anything: bump the ``.env`` + VERSION pre-release counter (the check-version-increment gate requires a + strict bump on every PR) and add a release-notes bullet. + +Run by ``.github/workflows/sync-modules-index.yml``, which turns a non-empty +diff into the trunk sync PR. Also runnable locally from the repo root: + + git clone --depth 1 https://github.com/castacks/airstack-modules-index /tmp/idx + python3 .github/workflows/scripts/registry_sync.py --index /tmp/idx + +stdlib only (gen_docs_catalog.py itself needs PyYAML). +""" +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +TRUNK = Path(__file__).resolve().parents[3] +FIXTURE = TRUNK / "tests" / "meta" / "fixtures" / "modules_index" +GENERATOR = TRUNK / "tools" / "gen_docs_catalog.py" +ENV_FILE = TRUNK / ".env" +RELEASE_NOTES = TRUNK / "docs" / "release_notes" / "index.md" + + +def mirror_entries(index: Path) -> None: + """Mirror registry modules/ and stacks/ YAMLs into the fixture.""" + for sub in ("modules", "stacks"): + src, dst = index / sub, FIXTURE / sub + if not src.is_dir(): + sys.exit(f"registry checkout has no {sub}/ directory: {src}") + if dst.is_dir(): + shutil.rmtree(dst) + dst.mkdir(parents=True) + for entry in sorted(src.glob("*.yaml")): + shutil.copy2(entry, dst / entry.name) + + +def regenerate_catalog() -> None: + with tempfile.TemporaryDirectory() as empty: + subprocess.run( + [sys.executable, str(GENERATOR), + "--index", str(FIXTURE), + "--modules-dir", f"{empty}/no-fetched-modules"], + check=True, cwd=TRUNK, + ) + + +def changed_paths() -> "list[str]": + out = subprocess.run( + ["git", "status", "--porcelain", "--", + str(FIXTURE.relative_to(TRUNK)), "docs/modules"], + check=True, capture_output=True, text=True, cwd=TRUNK, + ).stdout + return [line[3:].strip() for line in out.splitlines() if line.strip()] + + +def bump_version() -> "tuple[str, str]": + """Increment the .env VERSION pre-release counter (or patch).""" + text = ENV_FILE.read_text() + m = re.search(r'^VERSION="([^"]+)"$', text, flags=re.M) + if not m: + sys.exit('.env has no VERSION="..." line') + old = m.group(1) + pre = re.fullmatch(r"(\d+\.\d+\.\d+-(?:alpha|beta|rc)\.)(\d+)", old) + if pre: + new = f"{pre.group(1)}{int(pre.group(2)) + 1}" + else: + rel = re.fullmatch(r"(\d+\.\d+\.)(\d+)", old) + if not rel: + sys.exit(f"unrecognized VERSION format: {old}") + new = f"{rel.group(1)}{int(rel.group(2)) + 1}" + ENV_FILE.write_text(text.replace(f'VERSION="{old}"', f'VERSION="{new}"', 1)) + return old, new + + +def add_release_note(entries: "list[str]") -> None: + """Insert a bullet under the current '(Unreleased)' section heading.""" + lines = RELEASE_NOTES.read_text().splitlines(keepends=True) + names = ", ".join(f"`{e}`" for e in entries) or "registry entries" + bullet = ( + "- Module catalog synced from the registry " + f"(`sync-modules-index`): {names}.\n" + ) + for i, line in enumerate(lines): + if re.match(r"^## .*\(Unreleased\)", line): + lines.insert(i + 2, bullet) + lines.insert(i + 3, "\n") + RELEASE_NOTES.write_text("".join(lines)) + return + print("warning: no '(Unreleased)' release-notes section — bullet skipped", + file=sys.stderr) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--index", required=True, type=Path, + help="local checkout of castacks/airstack-modules-index") + parser.add_argument("--bump", action="store_true", + help="on change, bump .env VERSION and add a " + "release-notes bullet (for the sync PR)") + args = parser.parse_args() + + mirror_entries(args.index.resolve()) + regenerate_catalog() + + changed = changed_paths() + if not changed: + print("in-sync: committed catalog already matches the registry") + return 0 + + print("changed:") + for path in changed: + print(f" {path}") + if args.bump: + # Only the registry-entry files (not the regenerated pages) make a + # readable release note. + entry_names = sorted( + Path(p).stem for p in changed + if "fixtures/modules_index" in p and p.endswith(".yaml") + ) + old, new = bump_version() + add_release_note(entry_names) + print(f"VERSION {old} -> {new}; release-notes bullet added") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/sync-modules-index.yml b/.github/workflows/sync-modules-index.yml new file mode 100644 index 000000000..310910a0c --- /dev/null +++ b/.github/workflows/sync-modules-index.yml @@ -0,0 +1,67 @@ +# Module registration is TWO merges: the registry PR (castacks/ +# airstack-modules-index) and a trunk PR mirroring the entries into the +# contract-test fixture + regenerating the committed docs/modules pages +# (see the extract-module skill). This workflow automates the trunk half: +# it diffs trunk against the live registry and opens/refreshes one sync PR +# when they disagree. The develop docs deploy's drift alarm is the loud +# signal; this is the fix. +# +# Cadence: daily + manual dispatch (run it right after merging a registry +# PR for an immediate sync). No cross-repo secrets: the registry is cloned +# read-only and the PR is opened in this repo with the workflow token. +# KNOWN LIMITATION of that token: PRs it opens do not trigger other +# workflows (unit-tests, check-version-increment) — close and reopen the +# PR to run them. +name: Sync module catalog from registry +on: + workflow_dispatch: + schedule: + - cron: "43 7 * * *" # daily, 07:43 UTC +permissions: + contents: write + pull-requests: write +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # schedule/workflow_dispatch run on the default branch; the sync + # PR always targets develop. + ref: develop + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + - name: Install Dependencies + run: pip install pyyaml + - name: Sync fixture + committed catalog from the live registry + id: sync + run: | + rm -rf .modules-index + git clone --depth 1 https://github.com/castacks/airstack-modules-index .modules-index + python3 .github/workflows/scripts/registry_sync.py --index .modules-index --bump + rm -rf .modules-index + if [ -n "$(git status --porcelain)" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + - name: Open or refresh the sync PR + if: steps.sync.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + git config user.name "Registry Sync" + git config user.email "registry.sync@example.co.uk" + git checkout -B bot/sync-modules-index + git add -A + git commit -m "chore(catalog): sync committed module catalog from airstack-modules-index" + git push -f origin bot/sync-modules-index + if [ -z "$(gh pr list --head bot/sync-modules-index --state open --json number --jq '.[].number')" ]; then + gh pr create --base develop --head bot/sync-modules-index \ + --title "chore(catalog): sync committed module catalog from the registry" \ + --body "$(printf 'Automated trunk half of module registration (see the extract-module skill): mirrors the live [airstack-modules-index](https://github.com/castacks/airstack-modules-index) into tests/meta/fixtures/modules_index/ and regenerates the committed docs/modules pages with tools/gen_docs_catalog.py. VERSION is bumped only to satisfy the check-version-increment gate (docs-only change; publish will retag, not rebuild).\n\nEverything here is deterministic output of already-reviewed registry entries.\n\nNote: PRs opened by the workflow token do not trigger CI — close and reopen this PR to run the checks. The branch is force-refreshed from develop on each run, so a stale VERSION bump heals itself on the next run.\n\nWorkflow: sync-modules-index (%s/%s/actions/runs/%s)' "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")" + else + echo "sync PR already open — branch refreshed in place" + fi diff --git a/AGENTS.md b/AGENTS.md index 2f4b0e0d5..ac77afe5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ AirStack is transitioning from a monolith to **modules** (thin external repos wi - Docs: [Module & Stack Catalog](docs/modules/index.md) (marketplace, generated by [`tools/gen_docs_catalog.py`](tools/gen_docs_catalog.py)) · [Modules](docs/development/modules.md) · [Stacks](docs/development/stacks.md) · [Fleets](docs/development/fleets.md) · [Module CI](docs/development/module_ci.md) · [Modular AirStack Walkthrough](docs/getting_started/modular_airstack.md) - Skills: `create-module`, `create-stack`, `integrate-module-into-layer` (now stack-centric), `configure-multi-robot` (fleets) -- Registry: [castacks/airstack-modules-index](https://github.com/castacks/airstack-modules-index) — one YAML per registered module/stack; DECLARED compat in the entry, VERIFIED compat CI-stamped under `compat/`. **Registering a module is TWO merges**: the registry-repo PR *and* the trunk PR (fixture entries under `tests/meta/fixtures/modules_index/` + regenerated `docs/modules/`). The docs deploy regenerates the catalog from the **live registry**, so a merged trunk PR with an unmerged registry PR silently drops the module from the published site — merge the registry PR first (details + recovery command: the `extract-module` skill's registration step) +- Registry: [castacks/airstack-modules-index](https://github.com/castacks/airstack-modules-index) — one YAML per registered module/stack; DECLARED compat in the entry, VERIFIED compat CI-stamped under `compat/`. **Registering a module is TWO merges**: the registry-repo PR *and* the trunk PR (fixture entries under `tests/meta/fixtures/modules_index/` + regenerated `docs/modules/`). The docs deploy regenerates the catalog from the **live registry**, so a merged trunk PR with an unmerged registry PR silently drops the module from the published site — merge the registry PR first. The trunk half is automated: the daily [`sync-modules-index`](.github/workflows/sync-modules-index.yml) workflow opens the trunk sync PR (dispatch it manually for an immediate sync), and the develop docs deploy raises a `docs-catalog-drift` issue whenever git and the live registry disagree (details: the `extract-module` skill's registration step) ## Repository Architecture @@ -311,6 +311,7 @@ GitHub Actions workflows live in [`.github/workflows/`](.github/workflows/): | [`system-tests.yml`](.github/workflows/system-tests.yml) | PR opened/synchronized/reopened, `/pytest` PR comment (write-access only), or `workflow_dispatch` | Runs automatic package builds or selected simulation marks on an ephemeral GPU runner; only complete simulation campaigns are compared in metrics reports | | [`docker-build.yml`](.github/workflows/docker-build.yml) | Push to `main`/`develop` that changes `.env` (`VERSION=`), or manual dispatch | Builds, pushes, and cosign-signs all compose images on the ephemeral runner | | [`check-version-increment.yml`](.github/workflows/check-version-increment.yml) | Pull request | Validates `.env` `VERSION=` is valid semver and strictly greater than the base branch | +| [`sync-modules-index.yml`](.github/workflows/sync-modules-index.yml) | Daily schedule, or manual dispatch after merging a registry PR | Mirrors the live [module registry](https://github.com/castacks/airstack-modules-index) into `tests/meta/fixtures/modules_index/` + the committed `docs/modules/` pages and opens the trunk sync PR when they disagree (the develop docs deploy's drift alarm files a `docs-catalog-drift` issue meanwhile) | | `deploy_docs_from_{main,develop,release}.yaml` | Push to the matching branch (`docs/**`, `mkdocs.yml`, `*.md`) | Publishes versioned MkDocs site via `mike` | **`/pytest` PR comments** trigger `system-tests.yml` for users with write access (OWNER/MEMBER/COLLABORATOR), pulling args from the first line of the comment (e.g. `/pytest -m liveliness --sim msairsim`). Fork PRs are blocked — same-repo only — to keep arbitrary code off the self-hosted runner. diff --git a/docs/release_notes/index.md b/docs/release_notes/index.md index 6c3632ca8..e6f742d92 100644 --- a/docs/release_notes/index.md +++ b/docs/release_notes/index.md @@ -19,6 +19,17 @@ its own notes. --> ## 0.20.0 (Unreleased) +- **Module-catalog sync automation + drift alarm.** Registering a module is + two merges (registry PR to + [airstack-modules-index](https://github.com/castacks/airstack-modules-index) + + trunk fixture/catalog sync), and the docs deploy regenerates the + published catalog from the **live** registry — so a missed half used to + drop the module from the site silently. Now the develop docs deploy + raises a `docs-catalog-drift` issue whenever the committed catalog and + the live registry disagree, and the new `sync-modules-index` workflow + (daily + manual dispatch) opens the trunk sync PR automatically + (`.github/workflows/scripts/registry_sync.py`). + - **Trustworthy system-test outcomes.** A red system-tests run now always means the code under test got worse, never that CI infrastructure hiccuped: `run_meta.json` (schema v2) classifies every failure as From 9b7339b180c73ce3706dbc4f7ae273b68eeae94f Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Sat, 29 Aug 2026 03:13:03 -0400 Subject: [PATCH 2/2] sync-modules-index: prefer REGISTRY_SYNC_TOKEN so bot PRs trigger CI Falls back to the workflow token when the secret is absent (close/reopen the bot PR to run checks in that mode). Registry-side dispatch workflow uses the same secret name. Co-Authored-By: Claude Fable 5 --- .agents/skills/extract-module/SKILL.md | 19 +++++++++++-------- .github/workflows/sync-modules-index.yml | 24 +++++++++++++++--------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.agents/skills/extract-module/SKILL.md b/.agents/skills/extract-module/SKILL.md index aa4abab65..535af8aa8 100644 --- a/.agents/skills/extract-module/SKILL.md +++ b/.agents/skills/extract-module/SKILL.md @@ -194,14 +194,17 @@ delete, written for the orchestrator of that PR. Include: `gh workflow run deploy_docs_from_develop.yaml --repo castacks/AirStack --ref develop`. **Automation (2026-08-29):** the trunk half no longer needs to be - hand-built — after the registry PR merges, dispatch the - `sync-modules-index` workflow (Actions tab; also runs daily) and it opens - the trunk sync PR (fixture mirror + regenerated pages + VERSION bump). - The develop docs deploy independently raises a `docs-catalog-drift` issue - whenever the committed catalog and the live registry disagree, so a missed - sync can no longer stay silent. Caveat: the bot PR is opened with the - workflow token, which does not trigger CI — close and reopen it to run - the checks. + hand-built — merging the registry PR dispatches the `sync-modules-index` + workflow (registry-side `trigger-trunk-sync`; also daily sweep + manual + dispatch), which opens the trunk sync PR (fixture mirror + regenerated + pages + VERSION bump). The develop docs deploy independently raises a + `docs-catalog-drift` issue whenever the committed catalog and the live + registry disagree, so a missed sync can no longer stay silent. Both + automations authenticate with the `REGISTRY_SYNC_TOKEN` secret (same + fine-grained PAT in both repos); if it is missing/expired, the registry + trigger no-ops with a warning (daily sweep covers it) and the bot PR falls + back to the workflow token, whose PRs don't trigger CI — close and reopen + such a PR to run the checks. ## References diff --git a/.github/workflows/sync-modules-index.yml b/.github/workflows/sync-modules-index.yml index 310910a0c..b1ed33247 100644 --- a/.github/workflows/sync-modules-index.yml +++ b/.github/workflows/sync-modules-index.yml @@ -6,17 +6,21 @@ # when they disagree. The develop docs deploy's drift alarm is the loud # signal; this is the fix. # -# Cadence: daily + manual dispatch (run it right after merging a registry -# PR for an immediate sync). No cross-repo secrets: the registry is cloned -# read-only and the PR is opened in this repo with the workflow token. -# KNOWN LIMITATION of that token: PRs it opens do not trigger other -# workflows (unit-tests, check-version-increment) — close and reopen the -# PR to run them. +# Cadence: dispatched by the registry's trigger-trunk-sync workflow on +# every registry merge, plus a daily sweep and manual dispatch. The +# registry itself is cloned read-only. +# +# TOKEN: branch push + PR creation prefer the REGISTRY_SYNC_TOKEN secret +# (a fine-grained PAT on castacks/AirStack with contents+pull-requests +# read/write) so the bot PR triggers CI like any human PR. Without the +# secret it falls back to the workflow token, whose PRs do NOT trigger +# other workflows (unit-tests, check-version-increment) — close and +# reopen such a PR to run them. name: Sync module catalog from registry on: workflow_dispatch: schedule: - - cron: "43 7 * * *" # daily, 07:43 UTC + - cron: "43 7 * * *" # daily sweep, 07:43 UTC permissions: contents: write pull-requests: write @@ -30,6 +34,8 @@ jobs: # PR always targets develop. ref: develop fetch-depth: 0 + # PAT when configured (bot PR triggers CI); workflow token otherwise. + token: ${{ secrets.REGISTRY_SYNC_TOKEN || github.token }} - uses: actions/setup-python@v4 with: python-version: 3.10.6 @@ -50,7 +56,7 @@ jobs: - name: Open or refresh the sync PR if: steps.sync.outputs.changed == 'true' env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.REGISTRY_SYNC_TOKEN || github.token }} run: | git config user.name "Registry Sync" git config user.email "registry.sync@example.co.uk" @@ -61,7 +67,7 @@ jobs: if [ -z "$(gh pr list --head bot/sync-modules-index --state open --json number --jq '.[].number')" ]; then gh pr create --base develop --head bot/sync-modules-index \ --title "chore(catalog): sync committed module catalog from the registry" \ - --body "$(printf 'Automated trunk half of module registration (see the extract-module skill): mirrors the live [airstack-modules-index](https://github.com/castacks/airstack-modules-index) into tests/meta/fixtures/modules_index/ and regenerates the committed docs/modules pages with tools/gen_docs_catalog.py. VERSION is bumped only to satisfy the check-version-increment gate (docs-only change; publish will retag, not rebuild).\n\nEverything here is deterministic output of already-reviewed registry entries.\n\nNote: PRs opened by the workflow token do not trigger CI — close and reopen this PR to run the checks. The branch is force-refreshed from develop on each run, so a stale VERSION bump heals itself on the next run.\n\nWorkflow: sync-modules-index (%s/%s/actions/runs/%s)' "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")" + --body "$(printf 'Automated trunk half of module registration (see the extract-module skill): mirrors the live [airstack-modules-index](https://github.com/castacks/airstack-modules-index) into tests/meta/fixtures/modules_index/ and regenerates the committed docs/modules pages with tools/gen_docs_catalog.py. VERSION is bumped only to satisfy the check-version-increment gate (docs-only change; publish will retag, not rebuild).\n\nEverything here is deterministic output of already-reviewed registry entries.\n\nIf CI checks did not start on this PR, it was opened with the fallback workflow token (REGISTRY_SYNC_TOKEN secret not configured) — close and reopen it to trigger them. The branch is force-refreshed from develop on each run, so a stale VERSION bump heals itself on the next run.\n\nWorkflow: sync-modules-index (%s/%s/actions/runs/%s)' "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")" else echo "sync PR already open — branch refreshed in place" fi