Repository navigation
[ML] Pass explicit vault-role for the Backstage-registered backport token policy #695
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Backport | |
| on: | |
| pull_request_target: | |
| types: ["labeled", "closed"] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| 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 | |
| # head.ref fallback keys off the minor-freeze topic branch created by | |
| # dev-tools/bump_main_minor_freeze.sh (ci/ml-cpp-minor-freeze-main-*) so it cannot | |
| # accidentally skip an unrelated PR whose title merely mentions a minor freeze. | |
| if: | | |
| github.event.pull_request.merged == true && | |
| !(contains(github.event.pull_request.labels.*.name, 'backport')) && | |
| !(contains(github.event.pull_request.labels.*.name, 'no-backport')) && | |
| !startsWith(github.event.pull_request.head.ref, 'ci/ml-cpp-minor-freeze-main-') | |
| steps: | |
| - name: Check for version labels | |
| id: check-labels | |
| env: | |
| LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }} | |
| run: | | |
| for label in $LABELS; do | |
| if echo "$label" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then | |
| echo "Found version label: $label" | |
| echo "has_version_label=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| done | |
| 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" | |
| # The Token Policy was registered via the Backstage self-service template, | |
| # which names the Vault role after the AppRole name (token-policy-ml-cpp-backport) | |
| # rather than the SHA-256 hash of the workflow ref that fetch-github-token | |
| # derives by default. Pass the role explicitly so the lookup matches the | |
| # registered policy (github/token/token-policy-ml-cpp-backport). | |
| vault-role: "token-policy-ml-cpp-backport" | |
| - 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: ${{ steps.ephemeral_token.outputs.token || secrets.GITHUB_TOKEN }} | |
| - name: Info log | |
| if: steps.backport.outcome == 'success' | |
| run: cat ~/.backport/backport.info.log | |
| # When version labels were detected we always run the Backport Action. If it fails with | |
| # no-branches-exception, backport PRs were not created — fail CI so this is not silent, | |
| # UNLESS the label resolved only to the source branch (current-version label, e.g. | |
| # v9.6.0 -> main), which legitimately has nothing to backport (see step logic). | |
| - name: Check for real failures | |
| if: steps.backport.outcome == 'failure' | |
| env: | |
| HAS_VERSION_LABEL: ${{ steps.check-labels.outputs.has_version_label }} | |
| run: | | |
| echo "::group::backport.debug.log" | |
| cat ~/.backport/backport.debug.log 2>/dev/null || echo "(missing ~/.backport/backport.debug.log)" | |
| echo "::endgroup::" | |
| echo "::group::backport.info.log" | |
| cat ~/.backport/backport.info.log 2>/dev/null || echo "(missing ~/.backport/backport.info.log)" | |
| echo "::endgroup::" | |
| if grep -q '"code":"no-branches-exception"' ~/.backport/backport.debug.log 2>/dev/null; then | |
| if [ "${HAS_VERSION_LABEL}" = "true" ]; then | |
| # A version label for the current main version (e.g. v9.6.0 -> main via | |
| # branchLabelMapping) resolves only to the source branch, so there is | |
| # legitimately nothing to backport. The CLI still raises | |
| # no-branches-exception in that case; treat it as success, but ONLY when | |
| # every resolved target is the source branch (isSourceBranch:true and no | |
| # isSourceBranch:false). A genuine misconfiguration (label mapped to a | |
| # real, different, or missing branch) still fails loudly below. | |
| if grep -Ehq '"isSourceBranch":[[:space:]]*true' ~/.backport/backport.info.log ~/.backport/backport.debug.log 2>/dev/null \ | |
| && ! grep -Ehq '"isSourceBranch":[[:space:]]*false' ~/.backport/backport.info.log ~/.backport/backport.debug.log 2>/dev/null; then | |
| echo "Version label resolved only to the source branch — nothing to backport (expected for current-version labels)." | |
| exit 0 | |
| fi | |
| echo "::error::Backport CLI reported no-branches-exception while this PR had version labels. No backport PRs were opened — check branchLabelMapping / targetBranchChoices in .backportrc.json, review logs above, or open backports manually." | |
| exit 1 | |
| fi | |
| echo "No target branches matched (no version labels from workflow check) — nothing to backport." | |
| exit 0 | |
| fi | |
| echo "::error::Backport failed — see logs above." | |
| exit 1 | |
| # 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 }} | |
| # '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. | |
| 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 approval / auto-merge." | |
| exit 0 | |
| fi | |
| for pr in $PR_NUMBERS; do | |
| 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" | |
| else | |
| 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" | |
| gh pr merge "$pr" --repo "$REPO" --auto --squash || \ | |
| echo "::warning::Could not enable auto-merge on #$pr (is auto-merge enabled in repo settings?)" | |
| done | |
| remove-backport-pending: | |
| name: Remove backport-pending label | |
| runs-on: ubuntu-latest | |
| # Run when a backport PR is merged or closed. | |
| if: | | |
| github.event.pull_request.merged == true && | |
| contains(github.event.pull_request.labels.*.name, 'backport') | |
| steps: | |
| - name: Check if all backports are complete | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| # Backport PR titles follow the pattern: [9.2] Original title (#1234) | |
| ORIGINAL_PR=$(echo "$PR_TITLE" | grep -oE '\(#[0-9]+\)' | tail -1 | tr -d '(#)') | |
| if [ -z "$ORIGINAL_PR" ]; then | |
| echo "Could not extract original PR number from title: $PR_TITLE" | |
| exit 0 | |
| fi | |
| echo "Original PR: #$ORIGINAL_PR" | |
| echo "Just-merged backport PR: #$PR_NUMBER" | |
| HAS_LABEL=$(gh pr view "$ORIGINAL_PR" --repo "$REPO" --json labels \ | |
| --jq '[.labels[].name] | if index("backport-pending") then "true" else "false" end') | |
| if [ "$HAS_LABEL" != "true" ]; then | |
| echo "Original PR #$ORIGINAL_PR does not have backport-pending label. Nothing to do." | |
| exit 0 | |
| fi | |
| # Count open backport PRs, excluding the just-merged PR (API eventual consistency). | |
| OPEN_BACKPORTS=$(gh pr list --repo "$REPO" --label backport --state open \ | |
| --json number,title \ | |
| --jq "[.[] | select(.number != ${PR_NUMBER}) | select(.title | test(\"\\\\(#${ORIGINAL_PR}\\\\)\"))] | length") | |
| echo "Open backport PRs remaining: $OPEN_BACKPORTS" | |
| if [ "$OPEN_BACKPORTS" -eq 0 ]; then | |
| echo "All backport PRs are merged/closed. Removing backport-pending label from #$ORIGINAL_PR." | |
| gh pr edit "$ORIGINAL_PR" --repo "$REPO" --remove-label "backport-pending" | |
| else | |
| echo "Still $OPEN_BACKPORTS open backport PR(s). Keeping backport-pending label on #$ORIGINAL_PR." | |
| fi |