From a63e155f52e7cd91133e8c4229eab3ce34731738 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 1 Jul 2026 10:43:26 -0500 Subject: [PATCH] Generate index.yaml on merge instead of gating it in PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit index.yaml is what datumctl reads and is fully derived from plugins/*.yaml. Treating it as a reviewed source file — regenerated and committed in every PR, with CI failing on drift — put that work on every contributor and on the release bot, which only edits plugins/.yaml (its catalog PRs failed the drift gate). It was also the source of the earlier merge conflict, since the same generated file was edited on both a PR branch and main. Treat it as a build artifact instead: - Drop the index-sync gate from PR validation. PRs (human or bot) touch only plugins/*.yaml. Validation still schema-checks each manifest and verifies every download URL and checksum, so correctness is enforced before merge. - Regenerate index.yaml on merge (push to main touching plugins/**) and commit it straight back to main. main is unprotected, so the built-in token pushes directly — no app token, no trailing PR. The commit touches only index.yaml and token pushes don't retrigger workflows, so it never loops; a concurrency group plus rebase-retry handles closely-spaced merges. Contributors now edit one file and never touch the index by hand. --- .github/workflows/generate-index.yaml | 61 +++++++++++++++++++++++++++ .github/workflows/validate.yaml | 11 ++--- README.md | 12 ++---- 3 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/generate-index.yaml diff --git a/.github/workflows/generate-index.yaml b/.github/workflows/generate-index.yaml new file mode 100644 index 0000000..cfb237b --- /dev/null +++ b/.github/workflows/generate-index.yaml @@ -0,0 +1,61 @@ +name: Regenerate index.yaml + +# index.yaml is what datumctl reads, and it is fully derived from plugins/*.yaml. +# Rather than asking every contributor (and the release bot) to regenerate and +# commit it in their PR, it is reconciled here on merge: when plugins/ changes on +# main, regenerate the index and commit it straight back to main. +# +# main is not a protected branch, so the built-in GITHUB_TOKEN can push directly +# — no app token needed. The commit touches only index.yaml (not plugins/**) and +# GITHUB_TOKEN pushes don't retrigger workflows, so this never loops. +on: + push: + branches: [main] + paths: + - "plugins/**" + workflow_dispatch: + +# Serialize so two quick merges can't race to push index.yaml. +concurrency: + group: regenerate-index + cancel-in-progress: false + +permissions: + contents: write + +jobs: + regenerate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install tooling + run: pip install --quiet "PyYAML>=6" + + - name: Regenerate index.yaml + run: python3 scripts/generate_index.py + + - name: Commit and push if index.yaml changed + run: | + if git diff --quiet -- index.yaml; then + echo "index.yaml already in sync; nothing to do." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add index.yaml + git commit -m "chore: regenerate index.yaml" + # Rebase onto any commits that landed while this ran, then push. + for attempt in 1 2 3; do + if git pull --rebase origin main && git push; then + echo "Pushed regenerated index.yaml." + exit 0 + fi + echo "Push attempt $attempt failed; retrying." + done + echo "::error::Failed to push regenerated index.yaml after 3 attempts." + exit 1 diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 05661f1..4d00da7 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -26,13 +26,10 @@ jobs: - name: Install tooling run: pip install --quiet "jsonschema[format-nongpl]>=4" "PyYAML>=6" - # index.yaml is what datumctl reads, and it is fully derived from - # plugins/*.yaml. Fail if the committed index doesn't match what the - # generator produces, so main's index can never drift from the manifests. - - name: Check index.yaml is in sync with plugins/ - run: python3 scripts/generate_index.py --check - # Schema-validate every manifest and prove every advertised download - # actually resolves and matches its checksum. + # actually resolves and matches its checksum. index.yaml is a generated + # artifact reconciled on merge (see generate-index.yaml), so it is not + # reviewed or gated here — contributors and the release bot only ever touch + # plugins/*.yaml. - name: Validate manifests and verify release assets run: python3 scripts/verify_manifests.py diff --git a/README.md b/README.md index 65cd6a2..9711bf8 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,9 @@ datumctl ipam pool list ## Submitting a plugin 1. Add the `datumctl-plugin` topic to your plugin's GitHub repository. -2. Open a pull request adding `plugins/.yaml`, following the [schema](schema/plugin-v1alpha1.json). -3. Regenerate the index and commit it alongside your manifest: - - ```sh - python3 scripts/generate_index.py - ``` - - [`index.yaml`](index.yaml) — the file `datumctl` actually reads — is derived entirely from `plugins/*.yaml`, and CI fails if the two drift. -4. CI validates every manifest: schema conformance, that each download URL resolves, and that each SHA256 matches the published archive. A weekly [health check](.github/workflows/catalog-health.yaml) re-verifies the whole catalog so a plugin's links can't rot unnoticed. +2. Open a pull request adding `plugins/.yaml`, following the [schema](schema/plugin-v1alpha1.json). That's the only file you edit. +3. CI validates every manifest: schema conformance, that each download URL resolves, and that each SHA256 matches the published archive. A weekly [health check](.github/workflows/catalog-health.yaml) re-verifies the whole catalog so a plugin's links can't rot unnoticed. +4. On merge, [`index.yaml`](index.yaml) — the file `datumctl` actually reads — is regenerated from `plugins/*.yaml` and committed automatically. You never edit it by hand. (To preview locally: `python3 scripts/generate_index.py`.) ## Plugin manifest format