From 52c470e0e6f752b1c31692dcabc9eca3d36e3fdb Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 30 Jun 2026 23:32:57 -0500 Subject: [PATCH 1/5] Update cli-plugins catalog on release and fix goreleaser double-run Gate the plugin-publishing jobs on the release event instead of the tag ref. Publishing a release creates the tag, firing both push:tags and release:published; keying off the tag ref ran goreleaser twice concurrently with two processes racing to upload the same assets. The release-event gate makes it run exactly once. Add an update-plugin-index job that, after goreleaser attaches the archives and checksums.txt, opens a PR against milo-os/cli-plugins bumping plugins/ipam.yaml to the released version with refreshed URLs and checksums. Pinned to the datum-cloud/actions branch until that workflow ships in a tagged release. --- .github/workflows/release.yml | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71e649c..256e385 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,12 +45,18 @@ jobs: secrets: inherit # Build and publish the milo-ipam datumctl plugin as per-platform archives + - # checksums.txt, attached as GitHub release assets. Only runs on version tags - # (vX.Y.Z); independent of the container/kustomize jobs above. The produced - # asset names are consumed by the datumctl plugin catalog entry in - # milo-os/cli-plugins (plugins/ipam.yaml). + # checksums.txt, attached as GitHub release assets. The produced asset names + # are consumed by the datumctl plugin catalog entry in milo-os/cli-plugins + # (plugins/ipam.yaml). + # + # Runs ONLY on `release: published`. Publishing a release via the GitHub UI + # creates the tag, which fires BOTH `push: tags` and `release: published`; if + # this job keyed off the tag ref it would run twice concurrently and two + # goreleaser processes would race to upload the same assets. Gating on the + # release event makes it run exactly once and gives us a real release for + # goreleaser to attach artifacts to. publish-plugin: - if: startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'release' runs-on: ubuntu-latest permissions: contents: write @@ -76,3 +82,30 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # After goreleaser attaches the archives + checksums.txt to the release, open + # a PR against the datumctl plugin catalog (milo-os/cli-plugins) bumping + # plugins/ipam.yaml to this release: new version, per-platform download URLs, + # and refreshed sha256s pulled from checksums.txt. Gated on the release event + # (matching publish-plugin) so it runs exactly once, and depends on + # publish-plugin so checksums.txt exists before it reads it. + # + # NOTE: pinned to the datum-cloud/actions branch that introduces this workflow + # until it ships in a tagged release; switch this ref to a version tag (e.g. + # @v1.15.0) once that release is cut. + update-plugin-index: + needs: + - publish-plugin + if: github.event_name == 'release' + permissions: + contents: read + uses: datum-cloud/actions/.github/workflows/update-plugin-index.yaml@feat/update-plugin-index + with: + index-repo: milo-os/cli-plugins + plugin-name: ipam + version: ${{ github.event.release.tag_name }} + secrets: + # Cross-repo PR: the built-in GITHUB_TOKEN cannot push to milo-os/cli-plugins. + # PLUGIN_INDEX_TOKEN is a PAT / GitHub App token with contents:write + + # pull-requests:write on that repo, stored as a repo/org secret. + PLUGIN_INDEX_TOKEN: ${{ secrets.PLUGIN_INDEX_TOKEN }} From d7cee27d9e6c56c736dd78feeb36ca93b3313a91 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 30 Jun 2026 23:53:15 -0500 Subject: [PATCH 2/5] Authenticate the catalog PR via the milo-os GitHub App Mint a short-lived, repo-scoped installation token from a milo-os GitHub App and pass it to the catalog-update job, instead of relying on a long-lived PAT secret. The token is scoped to milo-os/cli-plugins and expires in ~1 hour; the App's ID and private key are stored as secrets. --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 256e385..b6fde9e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,6 +83,26 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Mint a short-lived token for the cross-repo catalog PR. The built-in + # GITHUB_TOKEN only reaches this repo, so we exchange the milo-os GitHub App's + # credentials for an installation token scoped to the catalog repo. The App's + # ID and private key are stored as secrets (ideally org-level, shared by every + # milo plugin repo); the minted token is repo-scoped and expires in ~1 hour. + generate-index-token: + if: github.event_name == 'release' + runs-on: ubuntu-latest + outputs: + token: ${{ steps.app-token.outputs.token }} + steps: + - name: Mint catalog token from the milo-os GitHub App + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.PLUGIN_INDEX_APP_ID }} + private-key: ${{ secrets.PLUGIN_INDEX_APP_PRIVATE_KEY }} + owner: milo-os + repositories: cli-plugins + # After goreleaser attaches the archives + checksums.txt to the release, open # a PR against the datumctl plugin catalog (milo-os/cli-plugins) bumping # plugins/ipam.yaml to this release: new version, per-platform download URLs, @@ -96,6 +116,7 @@ jobs: update-plugin-index: needs: - publish-plugin + - generate-index-token if: github.event_name == 'release' permissions: contents: read @@ -105,7 +126,7 @@ jobs: plugin-name: ipam version: ${{ github.event.release.tag_name }} secrets: - # Cross-repo PR: the built-in GITHUB_TOKEN cannot push to milo-os/cli-plugins. - # PLUGIN_INDEX_TOKEN is a PAT / GitHub App token with contents:write + - # pull-requests:write on that repo, stored as a repo/org secret. - PLUGIN_INDEX_TOKEN: ${{ secrets.PLUGIN_INDEX_TOKEN }} + # Cross-repo PR auth: a repo-scoped installation token minted above from + # the milo-os GitHub App. The built-in GITHUB_TOKEN cannot push to + # milo-os/cli-plugins, and this avoids a long-lived PAT. + PLUGIN_INDEX_TOKEN: ${{ needs.generate-index-token.outputs.token }} From 9164857ce6964c6a3d4a1fa53d583ba12ba15dd0 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 1 Jul 2026 00:28:15 -0500 Subject: [PATCH 3/5] Pin the plugin-index workflow to datum-cloud/actions v1.17.0 The reusable update-plugin-index workflow has shipped in a tagged release, so pin to it instead of the development branch. --- .github/workflows/release.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6fde9e..1219885 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,10 +109,6 @@ jobs: # and refreshed sha256s pulled from checksums.txt. Gated on the release event # (matching publish-plugin) so it runs exactly once, and depends on # publish-plugin so checksums.txt exists before it reads it. - # - # NOTE: pinned to the datum-cloud/actions branch that introduces this workflow - # until it ships in a tagged release; switch this ref to a version tag (e.g. - # @v1.15.0) once that release is cut. update-plugin-index: needs: - publish-plugin @@ -120,7 +116,7 @@ jobs: if: github.event_name == 'release' permissions: contents: read - uses: datum-cloud/actions/.github/workflows/update-plugin-index.yaml@feat/update-plugin-index + uses: datum-cloud/actions/.github/workflows/update-plugin-index.yaml@v1.17.0 with: index-repo: milo-os/cli-plugins plugin-name: ipam From c8fdea6e72d8e5754131be7b2a38872bbfc7570d Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 1 Jul 2026 00:58:37 -0500 Subject: [PATCH 4/5] Use the update-plugin-index composite action in one job Collapse the token-mint and catalog-update jobs into a single job that mints the GitHub App token and calls the composite action in the same job, so the token isn't scrubbed crossing a job boundary. --- .github/workflows/release.yml | 57 ++++++++++++++++------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1219885..de6deff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,16 +83,27 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Mint a short-lived token for the cross-repo catalog PR. The built-in - # GITHUB_TOKEN only reaches this repo, so we exchange the milo-os GitHub App's - # credentials for an installation token scoped to the catalog repo. The App's - # ID and private key are stored as secrets (ideally org-level, shared by every - # milo plugin repo); the minted token is repo-scoped and expires in ~1 hour. - generate-index-token: + # After goreleaser attaches the archives + checksums.txt to the release, open + # a PR against the datumctl plugin catalog (milo-os/cli-plugins) bumping + # plugins/ipam.yaml to this release: new version, per-platform download URLs, + # and refreshed sha256s pulled from checksums.txt. + # + # One job: mint a short-lived, repo-scoped installation token from the milo-os + # GitHub App and hand it straight to the composite action in the same job. A + # GitHub App token minted in a separate job and passed via outputs is scrubbed + # to empty (masked values don't survive job-to-job hops), so mint + use must + # share a job. Gated on the release event (matching publish-plugin) so it runs + # once, and depends on publish-plugin so checksums.txt exists before it reads + # it. The App's ID and private key are stored as secrets (org-level, shared by + # every milo plugin repo); the minted token is repo-scoped and expires ~1h. + # + # NOTE: pinned to the datum-cloud/actions branch that introduces the composite + # action; switch to a version tag (e.g. @v1.18.0) once that release is cut. + update-plugin-index: + needs: + - publish-plugin if: github.event_name == 'release' runs-on: ubuntu-latest - outputs: - token: ${{ steps.app-token.outputs.token }} steps: - name: Mint catalog token from the milo-os GitHub App id: app-token @@ -103,26 +114,10 @@ jobs: owner: milo-os repositories: cli-plugins - # After goreleaser attaches the archives + checksums.txt to the release, open - # a PR against the datumctl plugin catalog (milo-os/cli-plugins) bumping - # plugins/ipam.yaml to this release: new version, per-platform download URLs, - # and refreshed sha256s pulled from checksums.txt. Gated on the release event - # (matching publish-plugin) so it runs exactly once, and depends on - # publish-plugin so checksums.txt exists before it reads it. - update-plugin-index: - needs: - - publish-plugin - - generate-index-token - if: github.event_name == 'release' - permissions: - contents: read - uses: datum-cloud/actions/.github/workflows/update-plugin-index.yaml@v1.17.0 - with: - index-repo: milo-os/cli-plugins - plugin-name: ipam - version: ${{ github.event.release.tag_name }} - secrets: - # Cross-repo PR auth: a repo-scoped installation token minted above from - # the milo-os GitHub App. The built-in GITHUB_TOKEN cannot push to - # milo-os/cli-plugins, and this avoids a long-lived PAT. - PLUGIN_INDEX_TOKEN: ${{ needs.generate-index-token.outputs.token }} + - name: Open the catalog PR + uses: datum-cloud/actions/update-plugin-index@feat/update-plugin-index-composite + with: + index-repo: milo-os/cli-plugins + plugin-name: ipam + version: ${{ github.event.release.tag_name }} + token: ${{ steps.app-token.outputs.token }} From d930781867bf60c6d727005017156b21b608d6fc Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 1 Jul 2026 09:17:17 -0500 Subject: [PATCH 5/5] Pin update-plugin-index to datum-cloud/actions@v1.18.0 --- .github/workflows/release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de6deff..ad5a495 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,9 +96,6 @@ jobs: # once, and depends on publish-plugin so checksums.txt exists before it reads # it. The App's ID and private key are stored as secrets (org-level, shared by # every milo plugin repo); the minted token is repo-scoped and expires ~1h. - # - # NOTE: pinned to the datum-cloud/actions branch that introduces the composite - # action; switch to a version tag (e.g. @v1.18.0) once that release is cut. update-plugin-index: needs: - publish-plugin @@ -115,7 +112,7 @@ jobs: repositories: cli-plugins - name: Open the catalog PR - uses: datum-cloud/actions/update-plugin-index@feat/update-plugin-index-composite + uses: datum-cloud/actions/update-plugin-index@v1.18.0 with: index-repo: milo-os/cli-plugins plugin-name: ipam