From 3a49ce2019c5d006413fac75729b6b9a7571247b Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Thu, 23 Jul 2026 10:03:13 +1200 Subject: [PATCH 1/4] [ML] Auto-approve backport PRs so auto-merge can complete Backport PRs already have auto-merge enabled, but they never merge because the release branches require one approving review via an org-wide ruleset ("[org] Require a PR") that has no bypass actors. The bot that opens the backport (github-actions[bot]) cannot approve its own PR, so the review requirement is never satisfied. Mirror the Elasticsearch auto-backport-and-merge model: mint a short-lived token for a dedicated backport-approver GitHub App (a distinct, non-author identity) and have it approve each backport PR, after which the existing auto-merge step completes once required checks pass. The approval step is skipped when the App credentials are not configured, so this is a safe no-op until ML_BACKPORT_APPROVER_APP_ID (variable) and ML_BACKPORT_APPROVER_PRIVATE_KEY (secret) are set. Co-authored-by: Cursor --- .github/workflows/backport.yml | 35 ++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index c35387365..92c54f69c 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -91,22 +91,53 @@ jobs: echo "::error::Backport failed — see logs above." exit 1 - - name: Enable auto-merge on backport PRs + # Mint a short-lived token for the backport-approver GitHub App so its review + # counts as an approval from an identity distinct from the PR author + # (github-actions[bot]). The release branches require one approving review via an + # org-wide ruleset with no bypass actors, so auto-merge cannot complete without a + # real approval. This mirrors the Elasticsearch auto-backport-and-merge model. + # The step is skipped (leaving backports for manual approval) until the App + # credentials are configured, so the workflow is safe to run before provisioning. + - name: Generate backport-approver app token + id: approver_token + if: >- + steps.backport.outcome == 'success' && + contains(github.event.pull_request.labels.*.name, 'auto-backport') && + vars.ML_BACKPORT_APPROVER_APP_ID != '' + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.ML_BACKPORT_APPROVER_APP_ID }} + private-key: ${{ secrets.ML_BACKPORT_APPROVER_PRIVATE_KEY }} + + - name: Approve and enable auto-merge on backport PRs if: >- steps.backport.outcome == 'success' && contains(github.event.pull_request.labels.*.name, 'auto-backport') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APPROVE_TOKEN: ${{ steps.approver_token.outputs.token }} REPO: ${{ github.repository }} run: | # Extract created PR numbers from the backport info log. PR_NUMBERS=$(grep -oE '"pullRequestNumber":[0-9]+' ~/.backport/backport.info.log \ | grep -oE '[0-9]+' || true) if [ -z "$PR_NUMBERS" ]; then - echo "No backport PRs found in log. Skipping auto-merge." + echo "No backport PRs found in log. Skipping approval / auto-merge." exit 0 fi for pr in $PR_NUMBERS; do + # Auto-approve with the backport-approver App (a distinct, non-author identity) + # so the required review is satisfied. The backport is a clean cherry-pick of a + # change already reviewed on the source branch. + if [ -n "$APPROVE_TOKEN" ]; then + echo "Approving backport PR #$pr as the backport-approver App" + GH_TOKEN="$APPROVE_TOKEN" gh pr review "$pr" --repo "$REPO" --approve \ + --body "Automated approval: clean backport of an already-reviewed change." || \ + echo "::warning::Could not approve #$pr with the backport-approver App" + else + echo "::warning::No backport-approver App token configured (set the ML_BACKPORT_APPROVER_APP_ID variable and ML_BACKPORT_APPROVER_PRIVATE_KEY secret); #$pr will need a manual approval to merge." + fi + echo "Enabling auto-merge (squash) on PR #$pr" gh pr merge "$pr" --repo "$REPO" --auto --squash || \ echo "::warning::Could not enable auto-merge on #$pr (is auto-merge enabled in repo settings?)" From c583f7f613ef151b8e999d7294d4446ca5b74c9d Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Thu, 23 Jul 2026 10:27:01 +1200 Subject: [PATCH 2/4] [ML] Approve backport PRs via Elastic's ephemeral-token service Rework the auto-approve step to mint a short-lived token from Elastic's central ephemeral-token GitHub App (elastic/ci-gh-actions/fetch-github-token, OIDC -> Vault) instead of a bespoke, self-owned GitHub App, which we cannot get approved to create. The ephemeral-token identity approves each clean, bot-authored backport so the org-wide "[org] Require a PR" review requirement is satisfied and the existing auto-merge can complete. The token fetch uses continue-on-error so the workflow stays safe until the backport Token Policy is registered in elastic/catalog-info: the approval is skipped with a warning and backports simply await a manual approval. Requires: a TokenPolicy (pull_requests: write, scoped to elastic/ml-cpp, bound to this workflow ref) registered in elastic/catalog-info, and confirmation from Platform Engineering Productivity that an ephemeral-token approval counts toward the required review. Co-authored-by: Cursor --- .github/workflows/backport.yml | 48 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 92c54f69c..100d1e928 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -12,6 +12,12 @@ jobs: backport: name: Backport PR runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + # Required by elastic/ci-gh-actions/fetch-github-token to authenticate to Vault + # via OIDC and mint the ephemeral approval token. + id-token: write # Only run for merged PRs that are not themselves backports (avoid loops). # Skip automation PRs (minor-freeze main bumps carry a release version label from # elasticsearchmachine but must not backport to the new release branch). The @@ -91,23 +97,27 @@ jobs: echo "::error::Backport failed — see logs above." exit 1 - # Mint a short-lived token for the backport-approver GitHub App so its review - # counts as an approval from an identity distinct from the PR author - # (github-actions[bot]). The release branches require one approving review via an - # org-wide ruleset with no bypass actors, so auto-merge cannot complete without a - # real approval. This mirrors the Elasticsearch auto-backport-and-merge model. - # The step is skipped (leaving backports for manual approval) until the App - # credentials are configured, so the workflow is safe to run before provisioning. - - name: Generate backport-approver app token + # Mint a short-lived token from Elastic's central ephemeral-token GitHub App + # (via OIDC -> Vault) so the backport PR can be approved by an identity distinct + # from the author (github-actions[bot]). The release branches require one + # approving review via the org-wide "[org] Require a PR" ruleset, which has no + # bypass actors, so auto-merge cannot complete without a real approval. This + # mirrors the Elasticsearch auto-backport-and-merge model but reuses the shared + # ephemeral-token service rather than a bespoke App or machine account. + # + # continue-on-error keeps the workflow safe before the Token Policy is registered + # (resources/github-token-policies in elastic/catalog-info): the fetch fails + # softly, the approval below is skipped with a warning, and the backport still + # opens and arms auto-merge to await a manual approval. + - name: Fetch ephemeral GitHub token for approval id: approver_token if: >- steps.backport.outcome == 'success' && - contains(github.event.pull_request.labels.*.name, 'auto-backport') && - vars.ML_BACKPORT_APPROVER_APP_ID != '' - uses: actions/create-github-app-token@v1 + contains(github.event.pull_request.labels.*.name, 'auto-backport') + continue-on-error: true + uses: elastic/ci-gh-actions/fetch-github-token@v1 with: - app-id: ${{ vars.ML_BACKPORT_APPROVER_APP_ID }} - private-key: ${{ secrets.ML_BACKPORT_APPROVER_PRIVATE_KEY }} + vault-instance: "ci-prod" - name: Approve and enable auto-merge on backport PRs if: >- @@ -126,16 +136,16 @@ jobs: exit 0 fi for pr in $PR_NUMBERS; do - # Auto-approve with the backport-approver App (a distinct, non-author identity) - # so the required review is satisfied. The backport is a clean cherry-pick of a - # change already reviewed on the source branch. + # Auto-approve with the ephemeral-token identity (a distinct, non-author + # identity) so the required review is satisfied. The backport is a clean + # cherry-pick of a change already reviewed on the source branch. if [ -n "$APPROVE_TOKEN" ]; then - echo "Approving backport PR #$pr as the backport-approver App" + echo "Approving backport PR #$pr with the ephemeral-token identity" GH_TOKEN="$APPROVE_TOKEN" gh pr review "$pr" --repo "$REPO" --approve \ --body "Automated approval: clean backport of an already-reviewed change." || \ - echo "::warning::Could not approve #$pr with the backport-approver App" + echo "::warning::Could not approve #$pr with the ephemeral token" else - echo "::warning::No backport-approver App token configured (set the ML_BACKPORT_APPROVER_APP_ID variable and ML_BACKPORT_APPROVER_PRIVATE_KEY secret); #$pr will need a manual approval to merge." + echo "::warning::No ephemeral approval token available (is the backport Token Policy registered in elastic/catalog-info?); #$pr will need a manual approval to merge." fi echo "Enabling auto-merge (squash) on PR #$pr" From 8af91727e74dfa42d0370be55bc099f3d3216abe Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Thu, 23 Jul 2026 15:57:56 +1200 Subject: [PATCH 3/4] [ML] Rewire auto-backport to author-with-ephemeral-token, approve-with-GITHUB_TOKEN Adopt the production-proven pattern used by elastic/cloud, elasticsearch-ruby and the ECP GitOps repos: mint the ephemeral token before the backport action and use it to author the backport PRs, then approve with GITHUB_TOKEN (github-actions[bot]). Because the PRs are authored by the ephemeral-token identity, github-actions[bot] is a distinct identity and its approval counts toward the org "[org] Require a PR" ruleset (verified against elastic/catalog-sync-config, which merges under the identical org-only ruleset). Authoring with a real token also makes CI actually run on the backport PRs. Falls back to GITHUB_TOKEN authoring (no auto-approval) when the Token Policy is not yet registered. Co-authored-by: Cursor --- .github/workflows/backport.yml | 74 +++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 100d1e928..cb183b1fc 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -45,13 +45,35 @@ jobs: echo "No version label matching vN.N.N found — nothing to backport." echo "has_version_label=false" >> "$GITHUB_OUTPUT" + # Mint a short-lived token from Elastic's central ephemeral-token GitHub App + # (via OIDC -> Vault) and use it to AUTHOR the backport PRs. This mirrors the + # pattern used by elastic/cloud, elastic/elasticsearch-ruby and the ECP GitOps + # repos. Authoring with a real token (not the built-in GITHUB_TOKEN) means: + # * the backport PR is opened by a distinct, non-github-actions identity, so + # github-actions[bot] can later approve it (a token cannot approve its own + # PR); and + # * CI actually runs on the backport PR (PRs opened with GITHUB_TOKEN do not + # trigger further workflow runs). + # + # continue-on-error keeps the workflow safe before the Token Policy is registered + # (resources/github-token-policies in elastic/catalog-info): the fetch fails + # softly and we fall back to GITHUB_TOKEN below, so backports still open — but CI + # is not triggered on them and they will need a manual approval to merge. + - name: Fetch ephemeral GitHub token + id: ephemeral_token + if: steps.check-labels.outputs.has_version_label == 'true' + continue-on-error: true + uses: elastic/ci-gh-actions/fetch-github-token@v1 + with: + vault-instance: "ci-prod" + - name: Backport Action id: backport if: steps.check-labels.outputs.has_version_label == 'true' uses: sorenlouv/backport-github-action@v10.2.0 continue-on-error: true with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ steps.ephemeral_token.outputs.token || secrets.GITHUB_TOKEN }} - name: Info log if: steps.backport.outcome == 'success' @@ -97,35 +119,24 @@ jobs: echo "::error::Backport failed — see logs above." exit 1 - # Mint a short-lived token from Elastic's central ephemeral-token GitHub App - # (via OIDC -> Vault) so the backport PR can be approved by an identity distinct - # from the author (github-actions[bot]). The release branches require one - # approving review via the org-wide "[org] Require a PR" ruleset, which has no - # bypass actors, so auto-merge cannot complete without a real approval. This - # mirrors the Elasticsearch auto-backport-and-merge model but reuses the shared - # ephemeral-token service rather than a bespoke App or machine account. - # - # continue-on-error keeps the workflow safe before the Token Policy is registered - # (resources/github-token-policies in elastic/catalog-info): the fetch fails - # softly, the approval below is skipped with a warning, and the backport still - # opens and arms auto-merge to await a manual approval. - - name: Fetch ephemeral GitHub token for approval - id: approver_token - if: >- - steps.backport.outcome == 'success' && - contains(github.event.pull_request.labels.*.name, 'auto-backport') - continue-on-error: true - uses: elastic/ci-gh-actions/fetch-github-token@v1 - with: - vault-instance: "ci-prod" - + # Approve the backport PRs with GITHUB_TOKEN (github-actions[bot]) and arm + # auto-merge. Because the PRs were authored by the ephemeral-token identity above, + # github-actions[bot] is a *different* identity and its approval counts (a token + # cannot approve its own PR). One approval satisfies the org-wide "[org] Require a + # PR" ruleset on the release branches (one review, no code-owner requirement); the + # repo has "Allow GitHub Actions to create and approve pull requests" enabled. This + # is the same author-with-app / approve-with-GITHUB_TOKEN pattern the ECP GitOps + # repos (e.g. elastic/catalog-sync-config) use under the identical ruleset. - name: Approve and enable auto-merge on backport PRs if: >- steps.backport.outcome == 'success' && contains(github.event.pull_request.labels.*.name, 'auto-backport') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - APPROVE_TOKEN: ${{ steps.approver_token.outputs.token }} + # 'true' only when the ephemeral token was minted (Token Policy registered), + # i.e. the backport PRs were authored by the ephemeral-token app identity and + # can therefore be approved by github-actions[bot]. + EPHEMERAL_AUTHORED: ${{ steps.ephemeral_token.outputs.token != '' }} REPO: ${{ github.repository }} run: | # Extract created PR numbers from the backport info log. @@ -136,16 +147,15 @@ jobs: exit 0 fi for pr in $PR_NUMBERS; do - # Auto-approve with the ephemeral-token identity (a distinct, non-author - # identity) so the required review is satisfied. The backport is a clean - # cherry-pick of a change already reviewed on the source branch. - if [ -n "$APPROVE_TOKEN" ]; then - echo "Approving backport PR #$pr with the ephemeral-token identity" - GH_TOKEN="$APPROVE_TOKEN" gh pr review "$pr" --repo "$REPO" --approve \ + if [ "$EPHEMERAL_AUTHORED" = "true" ]; then + # Authored by the ephemeral-token identity, so this github-actions[bot] + # approval is from a distinct identity and satisfies the required review. + echo "Approving backport PR #$pr as github-actions[bot]" + gh pr review "$pr" --repo "$REPO" --approve \ --body "Automated approval: clean backport of an already-reviewed change." || \ - echo "::warning::Could not approve #$pr with the ephemeral token" + echo "::warning::Could not approve #$pr" else - echo "::warning::No ephemeral approval token available (is the backport Token Policy registered in elastic/catalog-info?); #$pr will need a manual approval to merge." + echo "::warning::Ephemeral token was unavailable (is the backport Token Policy registered in elastic/catalog-info?); #$pr was authored by github-actions[bot], so it cannot be auto-approved and will need a manual approval to merge." fi echo "Enabling auto-merge (squash) on PR #$pr" From bd2ca130ea921aa0b82364549ecfa47c60d0b7b1 Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Fri, 24 Jul 2026 14:03:11 +1200 Subject: [PATCH 4/4] [ML] Allow elastic-vault-github-plugin-prod[bot] to trigger CI on ephemeral-authored backports Co-authored-by: Cursor --- .buildkite/pull-requests.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/pull-requests.json b/.buildkite/pull-requests.json index db3e56c5e..341e6eb19 100644 --- a/.buildkite/pull-requests.json +++ b/.buildkite/pull-requests.json @@ -5,7 +5,7 @@ "pipeline_slug": "ml-cpp-pr-builds", "allow_org_users": true, "allowed_repo_permissions": ["admin", "write"], - "allowed_list": ["github-actions[bot]"], + "allowed_list": ["github-actions[bot]", "elastic-vault-github-plugin-prod[bot]"], "set_commit_status": false, "commit_status_context": "ml-cpp-ci", "build_on_commit": true,