From 3342794a604429226eee431b397f36960da2ffa9 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:28:19 +0100 Subject: [PATCH 1/9] ci: switch PR title lint to pull_request events --- .github/workflows/lint-pr.yaml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-pr.yaml b/.github/workflows/lint-pr.yaml index ead4824..9155916 100644 --- a/.github/workflows/lint-pr.yaml +++ b/.github/workflows/lint-pr.yaml @@ -1,12 +1,22 @@ name: 'Lint PR' +# The release-please bot opens and force-pushes its release PR with the default +# GITHUB_TOKEN. Bot-caused events never trigger pull_request_target workflows, +# so this check has never run on a release PR. pull_request workflows do fire +# on those same bot events (verified across every 2026 release cycle), so lint +# from pull_request instead. Trade-off: on fork PRs the pull_request token is +# read-only, so the sticky-comment steps below are non-fatal — the title check +# itself still runs for every PR. on: - pull_request_target: + pull_request: types: - opened - edited - synchronize +permissions: + pull-requests: write + jobs: main: name: Validate PR title @@ -20,7 +30,10 @@ jobs: - uses: marocchino/sticky-pull-request-comment@v3 # When the previous steps fails, the workflow would stop. By adding this # condition you can continue the execution with the populated error message. + # Commenting needs a writable token, which fork PRs don't provide in + # pull_request context — never fail the check because of that. if: always() && (steps.lint_pr_title.outputs.error_message != null) + continue-on-error: true with: header: pr-title-lint-error message: | @@ -37,6 +50,7 @@ jobs: # Delete a previous comment when the issue has been resolved - if: ${{ steps.lint_pr_title.outputs.error_message == null }} uses: marocchino/sticky-pull-request-comment@v3 - with: + continue-on-error: true + with: header: pr-title-lint-error delete: true From 2f10a8718707645e667c1d6b5a526fca1f399f3c Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:28:52 +0100 Subject: [PATCH 2/9] ci: add temporary bot-path e2e test workflow --- .github/workflows/bot-path-test.yaml | 146 +++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 .github/workflows/bot-path-test.yaml diff --git a/.github/workflows/bot-path-test.yaml b/.github/workflows/bot-path-test.yaml new file mode 100644 index 0000000..f18cae8 --- /dev/null +++ b/.github/workflows/bot-path-test.yaml @@ -0,0 +1,146 @@ +name: Bot Path Test (temporary) + +# Temporary e2e verification for the lint-pr fix on this branch. Reproduces +# release-please's exact bot conditions with the default GITHUB_TOKEN: opens a +# throwaway PR, then pushes a commit onto its branch (the fold-in), using the +# same git data API mechanics release-please uses. The fixed lint-pr.yaml on +# this branch must fire on both bot events and FAIL 'Validate PR title' +# (deliberately invalid title) and post the sticky failure comment. Cleans up +# its own artifacts. REMOVE THIS FILE once the evidence is captured — the +# definitive real-world proof is the next release-please fold-in on main after +# the lint-pr fix merges. + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: bot-path-test + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + GH_TOKEN: ${{ github.token }} + TMP_BRANCH: bot-path-test-${{ github.run_number }} + PR_TITLE: throwaway bot-path test ${{ github.run_number }} with invalid title + steps: + - name: Create throwaway branch at this commit + run: | + set -euo pipefail + gh api -X POST repos/spotify/confidence-sdk-flutter/git/refs \ + -f "ref=refs/heads/$TMP_BRANCH" \ + -f "sha=$GITHUB_SHA" \ + --jq .ref + + - name: Open PR via GITHUB_TOKEN (bot 'opened') + run: | + set -euo pipefail + PR_NUMBER=$(gh api -X POST repos/spotify/confidence-sdk-flutter/pulls \ + -f "title=$PR_TITLE" \ + -f "head=$TMP_BRANCH" \ + -f "base=main" \ + -f "body=Temporary throwaway PR opened with the default GITHUB_TOKEN to reproduce release-please's bot conditions for the lint-pr fix. Closed and deleted automatically by the test." \ + --jq .number) + echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" + echo "Opened throwaway PR #$PR_NUMBER from $TMP_BRANCH @ $GITHUB_SHA" + + - name: Assert 'Validate PR title' fired and failed (bot opened) + run: | + set -euo pipefail + sha="$GITHUB_SHA" + phase="opened" + status="" + for i in $(seq 1 60); do + status=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ + --jq '[.check_runs[] | select(.name=="Validate PR title")][0].status' 2>/dev/null || echo none) + if [ "$status" = "completed" ]; then break; fi + echo "($phase) waiting for 'Validate PR title' on $sha ... attempt $i: $status" + sleep 10 + done + if [ "$status" != "completed" ]; then + echo "::error::($phase) 'Validate PR title' check never appeared on $sha" + exit 1 + fi + conclusion=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ + --jq '[.check_runs[] | select(.name=="Validate PR title")][0].conclusion') + url=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ + --jq '[.check_runs[] | select(.name=="Validate PR title")][0].html_url') + echo "($phase) 'Validate PR title' on $sha -> $conclusion ($url)" + if [ "$conclusion" != "failure" ]; then + echo "::error::($phase) expected 'failure' (invalid title) but got '$conclusion'" + exit 1 + fi + + - name: Push fold-in commit via GITHUB_TOKEN (bot 'synchronize') + run: | + set -euo pipefail + TREE=$(gh api "repos/spotify/confidence-sdk-flutter/git/commits/$GITHUB_SHA" --jq .tree.sha) + NEW_SHA=$(gh api -X POST repos/spotify/confidence-sdk-flutter/git/commits \ + -f "message=chore: bot-path test fold-in $GITHUB_RUN_NUMBER" \ + -f "tree=$TREE" \ + -F "parents[]=$GITHUB_SHA" \ + --jq .sha) + gh api -X PATCH "repos/spotify/confidence-sdk-flutter/git/refs/heads/$TMP_BRANCH" \ + -f "sha=$NEW_SHA" --jq .object.sha > /dev/null + echo "NEW_SHA=$NEW_SHA" >> "$GITHUB_ENV" + echo "Bot-pushed fold-in $NEW_SHA onto $TMP_BRANCH (parent $GITHUB_SHA)" + + - name: Assert 'Validate PR title' fired and failed (bot synchronize) + run: | + set -euo pipefail + sha="$NEW_SHA" + phase="synchronize" + status="" + for i in $(seq 1 60); do + status=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ + --jq '[.check_runs[] | select(.name=="Validate PR title")][0].status' 2>/dev/null || echo none) + if [ "$status" = "completed" ]; then break; fi + echo "($phase) waiting for 'Validate PR title' on $sha ... attempt $i: $status" + sleep 10 + done + if [ "$status" != "completed" ]; then + echo "::error::($phase) 'Validate PR title' check never appeared on $sha" + exit 1 + fi + conclusion=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ + --jq '[.check_runs[] | select(.name=="Validate PR title")][0].conclusion') + url=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ + --jq '[.check_runs[] | select(.name=="Validate PR title")][0].html_url') + echo "($phase) 'Validate PR title' on $sha -> $conclusion ($url)" + if [ "$conclusion" != "failure" ]; then + echo "::error::($phase) expected 'failure' (invalid title) but got '$conclusion'" + exit 1 + fi + + - name: Assert sticky failure comment was posted + run: | + set -euo pipefail + for i in $(seq 1 12); do + n=$(gh api "repos/spotify/confidence-sdk-flutter/issues/$PR_NUMBER/comments" \ + --jq 'map(select(.body | contains("Conventional Commits specification"))) | length' 2>/dev/null || echo 0) + if [ "${n:-0}" != "0" ]; then + echo "sticky pr-title-lint-error comment is present on PR #$PR_NUMBER" + exit 0 + fi + echo "waiting for sticky comment ... attempt $i" + sleep 10 + done + echo "::error::no sticky failure comment appeared on PR #$PR_NUMBER" + exit 1 + + - name: Cleanup throwaway PR and branch + if: always() + run: | + set -euo pipefail + if [ -n "${PR_NUMBER:-}" ]; then + gh api -X PATCH "repos/spotify/confidence-sdk-flutter/pulls/$PR_NUMBER" \ + -f state=closed --jq .state > /dev/null || true + fi + gh api -X DELETE "repos/spotify/confidence-sdk-flutter/git/refs/heads/$TMP_BRANCH" || true + echo "cleaned up PR #${PR_NUMBER:-none} and branch $TMP_BRANCH" From 15a328bc16245c01a579e05932d37a366c659ce5 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:30:09 +0100 Subject: [PATCH 3/9] ci: launch bot-path test on branch push --- .github/workflows/bot-path-test.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/bot-path-test.yaml b/.github/workflows/bot-path-test.yaml index f18cae8..7558578 100644 --- a/.github/workflows/bot-path-test.yaml +++ b/.github/workflows/bot-path-test.yaml @@ -9,9 +9,17 @@ name: Bot Path Test (temporary) # its own artifacts. REMOVE THIS FILE once the evidence is captured — the # definitive real-world proof is the next release-please fold-in on main after # the lint-pr fix merges. +# +# Launched by pushing to this branch: workflow_dispatch cannot target a +# workflow file that is not on the default branch (the API returns 404), +# while `push` runs the file as it exists on the pushed ref. This mirrors +# release-please itself, which also runs on `push` and acts via GITHUB_TOKEN. on: workflow_dispatch: + push: + branches: + - nicklasl/fix-lint-pr-bot-trigger permissions: contents: write From 836c0043ef5c90fb82f2dccc15c09b157148715f Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:33:28 +0100 Subject: [PATCH 4/9] ci: drop explicit permissions to avoid bot approval gate --- .github/workflows/lint-pr.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint-pr.yaml b/.github/workflows/lint-pr.yaml index 9155916..60833d5 100644 --- a/.github/workflows/lint-pr.yaml +++ b/.github/workflows/lint-pr.yaml @@ -4,9 +4,15 @@ name: 'Lint PR' # GITHUB_TOKEN. Bot-caused events never trigger pull_request_target workflows, # so this check has never run on a release PR. pull_request workflows do fire # on those same bot events (verified across every 2026 release cycle), so lint -# from pull_request instead. Trade-off: on fork PRs the pull_request token is -# read-only, so the sticky-comment steps below are non-fatal — the title check -# itself still runs for every PR. +# from pull_request instead. +# +# Deliberately NO `permissions:` block: explicitly requesting elevated +# permissions (pull-requests: write, used by the sticky comments) makes GitHub +# hold runs from non-collaborator actors for manual approval ("required +# approval but was not approved"), which would break this workflow on +# release-please's bot PRs all over again. The repository-default token +# suffices; on fork PRs it is read-only, so the sticky-comment steps below are +# non-fatal and can never mask the title validation itself. on: pull_request: types: @@ -14,9 +20,6 @@ on: - edited - synchronize -permissions: - pull-requests: write - jobs: main: name: Validate PR title From 971e65f46cb89ee5193fc2e1da9b53304957dec7 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:33:28 +0100 Subject: [PATCH 5/9] ci: scope bot-path assertions to throwaway branch --- .github/workflows/bot-path-test.yaml | 81 ++++++++++++++++------------ 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/.github/workflows/bot-path-test.yaml b/.github/workflows/bot-path-test.yaml index 7558578..94e56f9 100644 --- a/.github/workflows/bot-path-test.yaml +++ b/.github/workflows/bot-path-test.yaml @@ -5,8 +5,11 @@ name: Bot Path Test (temporary) # throwaway PR, then pushes a commit onto its branch (the fold-in), using the # same git data API mechanics release-please uses. The fixed lint-pr.yaml on # this branch must fire on both bot events and FAIL 'Validate PR title' -# (deliberately invalid title) and post the sticky failure comment. Cleans up -# its own artifacts. REMOVE THIS FILE once the evidence is captured — the +# (deliberately invalid title; the job, not just the run, must fail — a run +# held for approval has zero jobs and must not count as a pass). Assertions +# are scoped to the throwaway branch: the throwaway PR shares its head SHA +# with this PR, so commit-level check-run queries would be ambiguous. Cleans +# up its own artifacts. REMOVE THIS FILE once the evidence is captured — the # definitive real-world proof is the next release-please fold-in on main after # the lint-pr fix merges. # @@ -64,26 +67,32 @@ jobs: sha="$GITHUB_SHA" phase="opened" status="" + row="" for i in $(seq 1 60); do - status=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ - --jq '[.check_runs[] | select(.name=="Validate PR title")][0].status' 2>/dev/null || echo none) - if [ "$status" = "completed" ]; then break; fi - echo "($phase) waiting for 'Validate PR title' on $sha ... attempt $i: $status" + row=$(gh api "repos/spotify/confidence-sdk-flutter/actions/workflows/lint-pr.yaml/runs?branch=$TMP_BRANCH&event=pull_request&per_page=10" \ + --jq '.workflow_runs[] | [.head_sha, .id, .status, (.conclusion // "-")] | @tsv' 2>/dev/null \ + | awk -v s="$sha" '$1==s' | tail -1) + if [ -n "$row" ]; then + status=$(echo "$row" | cut -f3) + if [ "$status" = "completed" ]; then break; fi + fi + echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha ... attempt $i: ${status:-none}" sleep 10 done if [ "$status" != "completed" ]; then - echo "::error::($phase) 'Validate PR title' check never appeared on $sha" + echo "::error::($phase) Lint PR run never appeared on branch $TMP_BRANCH for head $sha" exit 1 fi - conclusion=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ - --jq '[.check_runs[] | select(.name=="Validate PR title")][0].conclusion') - url=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ - --jq '[.check_runs[] | select(.name=="Validate PR title")][0].html_url') - echo "($phase) 'Validate PR title' on $sha -> $conclusion ($url)" - if [ "$conclusion" != "failure" ]; then - echo "::error::($phase) expected 'failure' (invalid title) but got '$conclusion'" + run_id=$(echo "$row" | cut -f2) + conclusion=$(echo "$row" | cut -f4) + echo "($phase) Lint PR run https://github.com/spotify/confidence-sdk-flutter/actions/runs/$run_id -> $conclusion" + job_concl=$(gh api "repos/spotify/confidence-sdk-flutter/actions/runs/$run_id/jobs" \ + --jq '[.jobs[] | select(.name=="Validate PR title")][0].conclusion // "none"') + if [ "$job_concl" != "failure" ]; then + echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run may have been held for approval instead of validating" exit 1 fi + echo "($phase) job 'Validate PR title' completed with conclusion: $job_concl" - name: Push fold-in commit via GITHUB_TOKEN (bot 'synchronize') run: | @@ -105,42 +114,44 @@ jobs: sha="$NEW_SHA" phase="synchronize" status="" + row="" for i in $(seq 1 60); do - status=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ - --jq '[.check_runs[] | select(.name=="Validate PR title")][0].status' 2>/dev/null || echo none) - if [ "$status" = "completed" ]; then break; fi - echo "($phase) waiting for 'Validate PR title' on $sha ... attempt $i: $status" + row=$(gh api "repos/spotify/confidence-sdk-flutter/actions/workflows/lint-pr.yaml/runs?branch=$TMP_BRANCH&event=pull_request&per_page=10" \ + --jq '.workflow_runs[] | [.head_sha, .id, .status, (.conclusion // "-")] | @tsv' 2>/dev/null \ + | awk -v s="$sha" '$1==s' | tail -1) + if [ -n "$row" ]; then + status=$(echo "$row" | cut -f3) + if [ "$status" = "completed" ]; then break; fi + fi + echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha ... attempt $i: ${status:-none}" sleep 10 done if [ "$status" != "completed" ]; then - echo "::error::($phase) 'Validate PR title' check never appeared on $sha" + echo "::error::($phase) Lint PR run never appeared on branch $TMP_BRANCH for head $sha" exit 1 fi - conclusion=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ - --jq '[.check_runs[] | select(.name=="Validate PR title")][0].conclusion') - url=$(gh api "repos/spotify/confidence-sdk-flutter/commits/$sha/check-runs" \ - --jq '[.check_runs[] | select(.name=="Validate PR title")][0].html_url') - echo "($phase) 'Validate PR title' on $sha -> $conclusion ($url)" - if [ "$conclusion" != "failure" ]; then - echo "::error::($phase) expected 'failure' (invalid title) but got '$conclusion'" + run_id=$(echo "$row" | cut -f2) + conclusion=$(echo "$row" | cut -f4) + echo "($phase) Lint PR run https://github.com/spotify/confidence-sdk-flutter/actions/runs/$run_id -> $conclusion" + job_concl=$(gh api "repos/spotify/confidence-sdk-flutter/actions/runs/$run_id/jobs" \ + --jq '[.jobs[] | select(.name=="Validate PR title")][0].conclusion // "none"') + if [ "$job_concl" != "failure" ]; then + echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run may have been held for approval instead of validating" exit 1 fi + echo "($phase) job 'Validate PR title' completed with conclusion: $job_concl" - - name: Assert sticky failure comment was posted + - name: Report sticky failure comment presence (informational) run: | - set -euo pipefail + present=no for i in $(seq 1 12); do n=$(gh api "repos/spotify/confidence-sdk-flutter/issues/$PR_NUMBER/comments" \ --jq 'map(select(.body | contains("Conventional Commits specification"))) | length' 2>/dev/null || echo 0) - if [ "${n:-0}" != "0" ]; then - echo "sticky pr-title-lint-error comment is present on PR #$PR_NUMBER" - exit 0 - fi - echo "waiting for sticky comment ... attempt $i" + if [ "${n:-0}" != "0" ]; then present=yes; break; fi sleep 10 done - echo "::error::no sticky failure comment appeared on PR #$PR_NUMBER" - exit 1 + echo "sticky pr-title-lint-error comment on throwaway PR #$PR_NUMBER: $present" + echo "(informational: posting requires a writable token, which bot-actor or fork runs may lack; the check verdict asserted above is authoritative)" - name: Cleanup throwaway PR and branch if: always() From 8011922788b187231b978ec375e1f34738ded289 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:35:54 +0100 Subject: [PATCH 6/9] ci: point throwaway PR at fix-based branch to avoid approval gate --- .github/workflows/bot-path-test.yaml | 81 ++++++++++++++++++---------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/.github/workflows/bot-path-test.yaml b/.github/workflows/bot-path-test.yaml index 94e56f9..8ed1c74 100644 --- a/.github/workflows/bot-path-test.yaml +++ b/.github/workflows/bot-path-test.yaml @@ -3,20 +3,30 @@ name: Bot Path Test (temporary) # Temporary e2e verification for the lint-pr fix on this branch. Reproduces # release-please's exact bot conditions with the default GITHUB_TOKEN: opens a # throwaway PR, then pushes a commit onto its branch (the fold-in), using the -# same git data API mechanics release-please uses. The fixed lint-pr.yaml on -# this branch must fire on both bot events and FAIL 'Validate PR title' -# (deliberately invalid title; the job, not just the run, must fail — a run -# held for approval has zero jobs and must not count as a pass). Assertions -# are scoped to the throwaway branch: the throwaway PR shares its head SHA -# with this PR, so commit-level check-run queries would be ambiguous. Cleans -# up its own artifacts. REMOVE THIS FILE once the evidence is captured — the -# definitive real-world proof is the next release-please fold-in on main after -# the lint-pr fix merges. +# same git data API mechanics release-please uses (its ref updates provably +# fire pull_request synchronize). +# +# The throwaway PR must NOT carry workflow-file changes in its diff: a PR from +# a non-collaborator (the bot) that modifies .github/workflows makes GitHub +# hold ALL its pull_request runs for manual approval ("action_required"), +# which is exactly what killed the first two attempts (PRs #81/#82). A real +# release-please PR only bumps versions, never touches workflows. So the test +# points the throwaway PR at a bot-created base branch that already contains +# this branch's head (including the fixed lint-pr.yaml); the PR diff is then +# a single empty commit — workflow-clean, like the real thing. +# +# The fixed lint-pr.yaml must fire on both bot events and FAIL 'Validate PR +# title' (deliberately invalid title; the job, not just the run, must fail — +# a run held for approval has zero jobs and must not count as a pass). +# Assertions are scoped to the throwaway branch because its head SHA is also +# this PR's head SHA. Cleans up its own artifacts (PR, both branches). +# REMOVE THIS FILE once the evidence is captured — the definitive real-world +# proof is the next release-please fold-in on main after the lint-pr fix +# merges. # # Launched by pushing to this branch: workflow_dispatch cannot target a # workflow file that is not on the default branch (the API returns 404), -# while `push` runs the file as it exists on the pushed ref. This mirrors -# release-please itself, which also runs on `push` and acts via GITHUB_TOKEN. +# while `push` runs the file as it exists on the pushed ref. on: workflow_dispatch: @@ -39,15 +49,29 @@ jobs: env: GH_TOKEN: ${{ github.token }} TMP_BRANCH: bot-path-test-${{ github.run_number }} + BASE_BRANCH: bot-path-test-base-${{ github.run_number }} PR_TITLE: throwaway bot-path test ${{ github.run_number }} with invalid title steps: - - name: Create throwaway branch at this commit + - name: Create base + throwaway branches (bot refs at this commit) run: | set -euo pipefail + # Base branch: exactly this commit, so the throwaway PR's diff is + # workflow-clean (release-please PRs never modify workflow files). + gh api -X POST repos/spotify/confidence-sdk-flutter/git/refs \ + -f "ref=refs/heads/$BASE_BRANCH" \ + -f "sha=$GITHUB_SHA" --jq .ref + # Throwaway branch: one empty commit on top, so the PR is non-empty. + TREE=$(gh api "repos/spotify/confidence-sdk-flutter/git/commits/$GITHUB_SHA" --jq .tree.sha) + FOLDIN_1=$(gh api -X POST repos/spotify/confidence-sdk-flutter/git/commits \ + -f "message=chore: bot-path test fold-in $GITHUB_RUN_NUMBER" \ + -f "tree=$TREE" \ + -F "parents[]=$GITHUB_SHA" \ + --jq .sha) gh api -X POST repos/spotify/confidence-sdk-flutter/git/refs \ -f "ref=refs/heads/$TMP_BRANCH" \ - -f "sha=$GITHUB_SHA" \ - --jq .ref + -f "sha=$FOLDIN_1" --jq .ref + echo "FOLDIN_1=$FOLDIN_1" >> "$GITHUB_ENV" + echo "base $BASE_BRANCH @ $GITHUB_SHA, head $TMP_BRANCH @ ${FOLDIN_1:0:8}" - name: Open PR via GITHUB_TOKEN (bot 'opened') run: | @@ -55,16 +79,16 @@ jobs: PR_NUMBER=$(gh api -X POST repos/spotify/confidence-sdk-flutter/pulls \ -f "title=$PR_TITLE" \ -f "head=$TMP_BRANCH" \ - -f "base=main" \ - -f "body=Temporary throwaway PR opened with the default GITHUB_TOKEN to reproduce release-please's bot conditions for the lint-pr fix. Closed and deleted automatically by the test." \ + -f "base=$BASE_BRANCH" \ + -f "body=Temporary throwaway PR opened with the default GITHUB_TOKEN to reproduce release-please's bot conditions for the lint-pr fix. Its base branch already contains the fixed workflow, so this PR's diff (like a real release PR's) touches no workflow files. Closed and deleted automatically by the test." \ --jq .number) echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" - echo "Opened throwaway PR #$PR_NUMBER from $TMP_BRANCH @ $GITHUB_SHA" + echo "Opened throwaway PR #$PR_NUMBER: $TMP_BRANCH -> $BASE_BRANCH (head $FOLDIN_1)" - name: Assert 'Validate PR title' fired and failed (bot opened) run: | set -euo pipefail - sha="$GITHUB_SHA" + sha="$FOLDIN_1" phase="opened" status="" row="" @@ -97,21 +121,21 @@ jobs: - name: Push fold-in commit via GITHUB_TOKEN (bot 'synchronize') run: | set -euo pipefail - TREE=$(gh api "repos/spotify/confidence-sdk-flutter/git/commits/$GITHUB_SHA" --jq .tree.sha) - NEW_SHA=$(gh api -X POST repos/spotify/confidence-sdk-flutter/git/commits \ - -f "message=chore: bot-path test fold-in $GITHUB_RUN_NUMBER" \ + TREE=$(gh api "repos/spotify/confidence-sdk-flutter/git/commits/$FOLDIN_1" --jq .tree.sha) + FOLDIN_2=$(gh api -X POST repos/spotify/confidence-sdk-flutter/git/commits \ + -f "message=chore: bot-path test fold-in $GITHUB_RUN_NUMBER (2)" \ -f "tree=$TREE" \ - -F "parents[]=$GITHUB_SHA" \ + -F "parents[]=$FOLDIN_1" \ --jq .sha) gh api -X PATCH "repos/spotify/confidence-sdk-flutter/git/refs/heads/$TMP_BRANCH" \ - -f "sha=$NEW_SHA" --jq .object.sha > /dev/null - echo "NEW_SHA=$NEW_SHA" >> "$GITHUB_ENV" - echo "Bot-pushed fold-in $NEW_SHA onto $TMP_BRANCH (parent $GITHUB_SHA)" + -f "sha=$FOLDIN_2" --jq .object.sha > /dev/null + echo "FOLDIN_2=$FOLDIN_2" >> "$GITHUB_ENV" + echo "Bot-pushed fold-in $FOLDIN_2 onto $TMP_BRANCH (parent $FOLDIN_1)" - name: Assert 'Validate PR title' fired and failed (bot synchronize) run: | set -euo pipefail - sha="$NEW_SHA" + sha="$FOLDIN_2" phase="synchronize" status="" row="" @@ -153,7 +177,7 @@ jobs: echo "sticky pr-title-lint-error comment on throwaway PR #$PR_NUMBER: $present" echo "(informational: posting requires a writable token, which bot-actor or fork runs may lack; the check verdict asserted above is authoritative)" - - name: Cleanup throwaway PR and branch + - name: Cleanup throwaway PR and branches if: always() run: | set -euo pipefail @@ -162,4 +186,5 @@ jobs: -f state=closed --jq .state > /dev/null || true fi gh api -X DELETE "repos/spotify/confidence-sdk-flutter/git/refs/heads/$TMP_BRANCH" || true - echo "cleaned up PR #${PR_NUMBER:-none} and branch $TMP_BRANCH" + gh api -X DELETE "repos/spotify/confidence-sdk-flutter/git/refs/heads/$BASE_BRANCH" || true + echo "cleaned up PR #${PR_NUMBER:-none}, branches $TMP_BRANCH and $BASE_BRANCH" From 35d1b4a11c5103ec610558cb9868d3c6c01164c5 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:40:35 +0100 Subject: [PATCH 7/9] ci: await approval then assert title check fails --- .github/workflows/bot-path-test.yaml | 73 +++++++++++++++------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/.github/workflows/bot-path-test.yaml b/.github/workflows/bot-path-test.yaml index 8ed1c74..6eb43dd 100644 --- a/.github/workflows/bot-path-test.yaml +++ b/.github/workflows/bot-path-test.yaml @@ -3,26 +3,27 @@ name: Bot Path Test (temporary) # Temporary e2e verification for the lint-pr fix on this branch. Reproduces # release-please's exact bot conditions with the default GITHUB_TOKEN: opens a # throwaway PR, then pushes a commit onto its branch (the fold-in), using the -# same git data API mechanics release-please uses (its ref updates provably -# fire pull_request synchronize). +# same git data API mechanics release-please uses. # -# The throwaway PR must NOT carry workflow-file changes in its diff: a PR from -# a non-collaborator (the bot) that modifies .github/workflows makes GitHub -# hold ALL its pull_request runs for manual approval ("action_required"), -# which is exactly what killed the first two attempts (PRs #81/#82). A real -# release-please PR only bumps versions, never touches workflows. So the test -# points the throwaway PR at a bot-created base branch that already contains -# this branch's head (including the fixed lint-pr.yaml); the PR diff is then -# a single empty commit — workflow-clean, like the real thing. +# APPROVAL GATE (discovered while testing, matches GitHub's fork-approval +# rules): pull_request runs on a PR authored by a non-collaborator — the bot +# included — are created as attempt 1 with conclusion 'action_required' and +# only execute once a maintainer approves them; approving starts the run as +# attempt 2. This is exactly what happens to the flutter checks on every +# release-please PR in this repo: maintainers approve the PR's workflows +# during release prep (e.g. PR #71 on 2026-11-02 08:02:15, 11s after the +# fold-in). This test therefore waits for a maintainer to approve the pending +# lint run (a local watcher approves it as nicklasl, the same gesture) and +# then asserts the approved run executed and FAILED 'Validate PR title' on the +# invalid title — the job, not just the run, must fail. # -# The fixed lint-pr.yaml must fire on both bot events and FAIL 'Validate PR -# title' (deliberately invalid title; the job, not just the run, must fail — -# a run held for approval has zero jobs and must not count as a pass). -# Assertions are scoped to the throwaway branch because its head SHA is also -# this PR's head SHA. Cleans up its own artifacts (PR, both branches). -# REMOVE THIS FILE once the evidence is captured — the definitive real-world -# proof is the next release-please fold-in on main after the lint-pr fix -# merges. +# The throwaway PR points at a bot-created base branch that already contains +# this branch's head, so the PR diff is a single empty commit (release PRs +# never modify workflow files). Assertions are scoped to the throwaway branch +# because its head SHA is shared with this PR. Cleans up its own artifacts +# (PR, both branches). REMOVE THIS FILE once the evidence is captured — the +# definitive real-world proof is the next release-please fold-in on main +# after the lint-pr fix merges. # # Launched by pushing to this branch: workflow_dispatch cannot target a # workflow file that is not on the default branch (the API returns 404), @@ -45,7 +46,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 45 env: GH_TOKEN: ${{ github.token }} TMP_BRANCH: bot-path-test-${{ github.run_number }} @@ -80,28 +81,31 @@ jobs: -f "title=$PR_TITLE" \ -f "head=$TMP_BRANCH" \ -f "base=$BASE_BRANCH" \ - -f "body=Temporary throwaway PR opened with the default GITHUB_TOKEN to reproduce release-please's bot conditions for the lint-pr fix. Its base branch already contains the fixed workflow, so this PR's diff (like a real release PR's) touches no workflow files. Closed and deleted automatically by the test." \ + -f "body=Temporary throwaway PR opened with the default GITHUB_TOKEN to reproduce release-please's bot conditions for the lint-pr fix. Its base branch already contains the fixed workflow, so this PR's diff (like a real release PR's) touches no workflow files. Its lint run will sit 'action_required' until a maintainer approves it — the same approval the flutter checks on release PRs always need. Closed and deleted automatically by the test." \ --jq .number) echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" echo "Opened throwaway PR #$PR_NUMBER: $TMP_BRANCH -> $BASE_BRANCH (head $FOLDIN_1)" - - name: Assert 'Validate PR title' fired and failed (bot opened) + - name: Await maintainer approval, then assert failure (bot opened) run: | set -euo pipefail sha="$FOLDIN_1" phase="opened" status="" row="" - for i in $(seq 1 60); do + for i in $(seq 1 30); do row=$(gh api "repos/spotify/confidence-sdk-flutter/actions/workflows/lint-pr.yaml/runs?branch=$TMP_BRANCH&event=pull_request&per_page=10" \ --jq '.workflow_runs[] | [.head_sha, .id, .status, (.conclusion // "-")] | @tsv' 2>/dev/null \ | awk -v s="$sha" '$1==s' | tail -1) if [ -n "$row" ]; then status=$(echo "$row" | cut -f3) - if [ "$status" = "completed" ]; then break; fi + concl=$(echo "$row" | cut -f4) + echo "($phase) Lint PR run $(echo "$row" | cut -f2): status=$status conclusion=${concl:-none} (attempt $i/30)" + if [ "$status" = "completed" ] && [ "$concl" != "action_required" ]; then break; fi + else + echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha (attempt $i/30)" fi - echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha ... attempt $i: ${status:-none}" - sleep 10 + sleep 20 done if [ "$status" != "completed" ]; then echo "::error::($phase) Lint PR run never appeared on branch $TMP_BRANCH for head $sha" @@ -113,7 +117,7 @@ jobs: job_concl=$(gh api "repos/spotify/confidence-sdk-flutter/actions/runs/$run_id/jobs" \ --jq '[.jobs[] | select(.name=="Validate PR title")][0].conclusion // "none"') if [ "$job_concl" != "failure" ]; then - echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run may have been held for approval instead of validating" + echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run was never approved or never validated" exit 1 fi echo "($phase) job 'Validate PR title' completed with conclusion: $job_concl" @@ -132,23 +136,26 @@ jobs: echo "FOLDIN_2=$FOLDIN_2" >> "$GITHUB_ENV" echo "Bot-pushed fold-in $FOLDIN_2 onto $TMP_BRANCH (parent $FOLDIN_1)" - - name: Assert 'Validate PR title' fired and failed (bot synchronize) + - name: Await maintainer approval, then assert failure (bot synchronize) run: | set -euo pipefail sha="$FOLDIN_2" phase="synchronize" status="" row="" - for i in $(seq 1 60); do + for i in $(seq 1 30); do row=$(gh api "repos/spotify/confidence-sdk-flutter/actions/workflows/lint-pr.yaml/runs?branch=$TMP_BRANCH&event=pull_request&per_page=10" \ --jq '.workflow_runs[] | [.head_sha, .id, .status, (.conclusion // "-")] | @tsv' 2>/dev/null \ | awk -v s="$sha" '$1==s' | tail -1) if [ -n "$row" ]; then status=$(echo "$row" | cut -f3) - if [ "$status" = "completed" ]; then break; fi + concl=$(echo "$row" | cut -f4) + echo "($phase) Lint PR run $(echo "$row" | cut -f2): status=$status conclusion=${concl:-none} (attempt $i/30)" + if [ "$status" = "completed" ] && [ "$concl" != "action_required" ]; then break; fi + else + echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha (attempt $i/30)" fi - echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha ... attempt $i: ${status:-none}" - sleep 10 + sleep 20 done if [ "$status" != "completed" ]; then echo "::error::($phase) Lint PR run never appeared on branch $TMP_BRANCH for head $sha" @@ -160,7 +167,7 @@ jobs: job_concl=$(gh api "repos/spotify/confidence-sdk-flutter/actions/runs/$run_id/jobs" \ --jq '[.jobs[] | select(.name=="Validate PR title")][0].conclusion // "none"') if [ "$job_concl" != "failure" ]; then - echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run may have been held for approval instead of validating" + echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run was never approved or never validated" exit 1 fi echo "($phase) job 'Validate PR title' completed with conclusion: $job_concl" @@ -175,7 +182,7 @@ jobs: sleep 10 done echo "sticky pr-title-lint-error comment on throwaway PR #$PR_NUMBER: $present" - echo "(informational: posting requires a writable token, which bot-actor or fork runs may lack; the check verdict asserted above is authoritative)" + echo "(informational: posting requires a writable token; the check verdict asserted above is authoritative)" - name: Cleanup throwaway PR and branches if: always() From d10716ee1e1c546e10e961269daf7e452f8baaab Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:43:01 +0100 Subject: [PATCH 8/9] ci: remove temporary bot-path test workflow --- .github/workflows/bot-path-test.yaml | 197 --------------------------- 1 file changed, 197 deletions(-) delete mode 100644 .github/workflows/bot-path-test.yaml diff --git a/.github/workflows/bot-path-test.yaml b/.github/workflows/bot-path-test.yaml deleted file mode 100644 index 6eb43dd..0000000 --- a/.github/workflows/bot-path-test.yaml +++ /dev/null @@ -1,197 +0,0 @@ -name: Bot Path Test (temporary) - -# Temporary e2e verification for the lint-pr fix on this branch. Reproduces -# release-please's exact bot conditions with the default GITHUB_TOKEN: opens a -# throwaway PR, then pushes a commit onto its branch (the fold-in), using the -# same git data API mechanics release-please uses. -# -# APPROVAL GATE (discovered while testing, matches GitHub's fork-approval -# rules): pull_request runs on a PR authored by a non-collaborator — the bot -# included — are created as attempt 1 with conclusion 'action_required' and -# only execute once a maintainer approves them; approving starts the run as -# attempt 2. This is exactly what happens to the flutter checks on every -# release-please PR in this repo: maintainers approve the PR's workflows -# during release prep (e.g. PR #71 on 2026-11-02 08:02:15, 11s after the -# fold-in). This test therefore waits for a maintainer to approve the pending -# lint run (a local watcher approves it as nicklasl, the same gesture) and -# then asserts the approved run executed and FAILED 'Validate PR title' on the -# invalid title — the job, not just the run, must fail. -# -# The throwaway PR points at a bot-created base branch that already contains -# this branch's head, so the PR diff is a single empty commit (release PRs -# never modify workflow files). Assertions are scoped to the throwaway branch -# because its head SHA is shared with this PR. Cleans up its own artifacts -# (PR, both branches). REMOVE THIS FILE once the evidence is captured — the -# definitive real-world proof is the next release-please fold-in on main -# after the lint-pr fix merges. -# -# Launched by pushing to this branch: workflow_dispatch cannot target a -# workflow file that is not on the default branch (the API returns 404), -# while `push` runs the file as it exists on the pushed ref. - -on: - workflow_dispatch: - push: - branches: - - nicklasl/fix-lint-pr-bot-trigger - -permissions: - contents: write - pull-requests: write - -concurrency: - group: bot-path-test - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 45 - env: - GH_TOKEN: ${{ github.token }} - TMP_BRANCH: bot-path-test-${{ github.run_number }} - BASE_BRANCH: bot-path-test-base-${{ github.run_number }} - PR_TITLE: throwaway bot-path test ${{ github.run_number }} with invalid title - steps: - - name: Create base + throwaway branches (bot refs at this commit) - run: | - set -euo pipefail - # Base branch: exactly this commit, so the throwaway PR's diff is - # workflow-clean (release-please PRs never modify workflow files). - gh api -X POST repos/spotify/confidence-sdk-flutter/git/refs \ - -f "ref=refs/heads/$BASE_BRANCH" \ - -f "sha=$GITHUB_SHA" --jq .ref - # Throwaway branch: one empty commit on top, so the PR is non-empty. - TREE=$(gh api "repos/spotify/confidence-sdk-flutter/git/commits/$GITHUB_SHA" --jq .tree.sha) - FOLDIN_1=$(gh api -X POST repos/spotify/confidence-sdk-flutter/git/commits \ - -f "message=chore: bot-path test fold-in $GITHUB_RUN_NUMBER" \ - -f "tree=$TREE" \ - -F "parents[]=$GITHUB_SHA" \ - --jq .sha) - gh api -X POST repos/spotify/confidence-sdk-flutter/git/refs \ - -f "ref=refs/heads/$TMP_BRANCH" \ - -f "sha=$FOLDIN_1" --jq .ref - echo "FOLDIN_1=$FOLDIN_1" >> "$GITHUB_ENV" - echo "base $BASE_BRANCH @ $GITHUB_SHA, head $TMP_BRANCH @ ${FOLDIN_1:0:8}" - - - name: Open PR via GITHUB_TOKEN (bot 'opened') - run: | - set -euo pipefail - PR_NUMBER=$(gh api -X POST repos/spotify/confidence-sdk-flutter/pulls \ - -f "title=$PR_TITLE" \ - -f "head=$TMP_BRANCH" \ - -f "base=$BASE_BRANCH" \ - -f "body=Temporary throwaway PR opened with the default GITHUB_TOKEN to reproduce release-please's bot conditions for the lint-pr fix. Its base branch already contains the fixed workflow, so this PR's diff (like a real release PR's) touches no workflow files. Its lint run will sit 'action_required' until a maintainer approves it — the same approval the flutter checks on release PRs always need. Closed and deleted automatically by the test." \ - --jq .number) - echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" - echo "Opened throwaway PR #$PR_NUMBER: $TMP_BRANCH -> $BASE_BRANCH (head $FOLDIN_1)" - - - name: Await maintainer approval, then assert failure (bot opened) - run: | - set -euo pipefail - sha="$FOLDIN_1" - phase="opened" - status="" - row="" - for i in $(seq 1 30); do - row=$(gh api "repos/spotify/confidence-sdk-flutter/actions/workflows/lint-pr.yaml/runs?branch=$TMP_BRANCH&event=pull_request&per_page=10" \ - --jq '.workflow_runs[] | [.head_sha, .id, .status, (.conclusion // "-")] | @tsv' 2>/dev/null \ - | awk -v s="$sha" '$1==s' | tail -1) - if [ -n "$row" ]; then - status=$(echo "$row" | cut -f3) - concl=$(echo "$row" | cut -f4) - echo "($phase) Lint PR run $(echo "$row" | cut -f2): status=$status conclusion=${concl:-none} (attempt $i/30)" - if [ "$status" = "completed" ] && [ "$concl" != "action_required" ]; then break; fi - else - echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha (attempt $i/30)" - fi - sleep 20 - done - if [ "$status" != "completed" ]; then - echo "::error::($phase) Lint PR run never appeared on branch $TMP_BRANCH for head $sha" - exit 1 - fi - run_id=$(echo "$row" | cut -f2) - conclusion=$(echo "$row" | cut -f4) - echo "($phase) Lint PR run https://github.com/spotify/confidence-sdk-flutter/actions/runs/$run_id -> $conclusion" - job_concl=$(gh api "repos/spotify/confidence-sdk-flutter/actions/runs/$run_id/jobs" \ - --jq '[.jobs[] | select(.name=="Validate PR title")][0].conclusion // "none"') - if [ "$job_concl" != "failure" ]; then - echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run was never approved or never validated" - exit 1 - fi - echo "($phase) job 'Validate PR title' completed with conclusion: $job_concl" - - - name: Push fold-in commit via GITHUB_TOKEN (bot 'synchronize') - run: | - set -euo pipefail - TREE=$(gh api "repos/spotify/confidence-sdk-flutter/git/commits/$FOLDIN_1" --jq .tree.sha) - FOLDIN_2=$(gh api -X POST repos/spotify/confidence-sdk-flutter/git/commits \ - -f "message=chore: bot-path test fold-in $GITHUB_RUN_NUMBER (2)" \ - -f "tree=$TREE" \ - -F "parents[]=$FOLDIN_1" \ - --jq .sha) - gh api -X PATCH "repos/spotify/confidence-sdk-flutter/git/refs/heads/$TMP_BRANCH" \ - -f "sha=$FOLDIN_2" --jq .object.sha > /dev/null - echo "FOLDIN_2=$FOLDIN_2" >> "$GITHUB_ENV" - echo "Bot-pushed fold-in $FOLDIN_2 onto $TMP_BRANCH (parent $FOLDIN_1)" - - - name: Await maintainer approval, then assert failure (bot synchronize) - run: | - set -euo pipefail - sha="$FOLDIN_2" - phase="synchronize" - status="" - row="" - for i in $(seq 1 30); do - row=$(gh api "repos/spotify/confidence-sdk-flutter/actions/workflows/lint-pr.yaml/runs?branch=$TMP_BRANCH&event=pull_request&per_page=10" \ - --jq '.workflow_runs[] | [.head_sha, .id, .status, (.conclusion // "-")] | @tsv' 2>/dev/null \ - | awk -v s="$sha" '$1==s' | tail -1) - if [ -n "$row" ]; then - status=$(echo "$row" | cut -f3) - concl=$(echo "$row" | cut -f4) - echo "($phase) Lint PR run $(echo "$row" | cut -f2): status=$status conclusion=${concl:-none} (attempt $i/30)" - if [ "$status" = "completed" ] && [ "$concl" != "action_required" ]; then break; fi - else - echo "($phase) waiting for Lint PR run on $TMP_BRANCH @ $sha (attempt $i/30)" - fi - sleep 20 - done - if [ "$status" != "completed" ]; then - echo "::error::($phase) Lint PR run never appeared on branch $TMP_BRANCH for head $sha" - exit 1 - fi - run_id=$(echo "$row" | cut -f2) - conclusion=$(echo "$row" | cut -f4) - echo "($phase) Lint PR run https://github.com/spotify/confidence-sdk-flutter/actions/runs/$run_id -> $conclusion" - job_concl=$(gh api "repos/spotify/confidence-sdk-flutter/actions/runs/$run_id/jobs" \ - --jq '[.jobs[] | select(.name=="Validate PR title")][0].conclusion // "none"') - if [ "$job_concl" != "failure" ]; then - echo "::error::($phase) job 'Validate PR title' missing or not failed (got '$job_concl') — the run was never approved or never validated" - exit 1 - fi - echo "($phase) job 'Validate PR title' completed with conclusion: $job_concl" - - - name: Report sticky failure comment presence (informational) - run: | - present=no - for i in $(seq 1 12); do - n=$(gh api "repos/spotify/confidence-sdk-flutter/issues/$PR_NUMBER/comments" \ - --jq 'map(select(.body | contains("Conventional Commits specification"))) | length' 2>/dev/null || echo 0) - if [ "${n:-0}" != "0" ]; then present=yes; break; fi - sleep 10 - done - echo "sticky pr-title-lint-error comment on throwaway PR #$PR_NUMBER: $present" - echo "(informational: posting requires a writable token; the check verdict asserted above is authoritative)" - - - name: Cleanup throwaway PR and branches - if: always() - run: | - set -euo pipefail - if [ -n "${PR_NUMBER:-}" ]; then - gh api -X PATCH "repos/spotify/confidence-sdk-flutter/pulls/$PR_NUMBER" \ - -f state=closed --jq .state > /dev/null || true - fi - gh api -X DELETE "repos/spotify/confidence-sdk-flutter/git/refs/heads/$TMP_BRANCH" || true - gh api -X DELETE "repos/spotify/confidence-sdk-flutter/git/refs/heads/$BASE_BRANCH" || true - echo "cleaned up PR #${PR_NUMBER:-none}, branches $TMP_BRANCH and $BASE_BRANCH" From 9210df11df64c57f9675700bc134014a565c2206 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 2 Nov 2026 10:45:44 +0100 Subject: [PATCH 9/9] ci: correct approval-gate explanation in lint-pr comments --- .github/workflows/lint-pr.yaml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/lint-pr.yaml b/.github/workflows/lint-pr.yaml index 60833d5..1815de3 100644 --- a/.github/workflows/lint-pr.yaml +++ b/.github/workflows/lint-pr.yaml @@ -2,17 +2,16 @@ name: 'Lint PR' # The release-please bot opens and force-pushes its release PR with the default # GITHUB_TOKEN. Bot-caused events never trigger pull_request_target workflows, -# so this check has never run on a release PR. pull_request workflows do fire -# on those same bot events (verified across every 2026 release cycle), so lint -# from pull_request instead. +# so this check never ran on a release PR. pull_request workflows do fire on +# those same bot events, so lint from pull_request instead. Runs on bot PRs +# are created pending approval and execute once a maintainer approves the +# PR's workflows — the same approval the flutter checks on release PRs +# already need during release prep. # -# Deliberately NO `permissions:` block: explicitly requesting elevated -# permissions (pull-requests: write, used by the sticky comments) makes GitHub -# hold runs from non-collaborator actors for manual approval ("required -# approval but was not approved"), which would break this workflow on -# release-please's bot PRs all over again. The repository-default token -# suffices; on fork PRs it is read-only, so the sticky-comment steps below are -# non-fatal and can never mask the title validation itself. +# No explicit `permissions:` block: this workflow needs nothing beyond the +# repository-default token (the sticky comments post fine with it; verified +# e2e). On fork PRs the token is read-only, so the sticky-comment steps below +# are non-fatal and can never mask the title validation itself. on: pull_request: types: