From 0fecc5290e0e2e31450a00e6531e70e9a0d7c851 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Apr 2026 10:54:35 +0000 Subject: [PATCH 1/7] feat(workflow): restore repo variable gate with dedicated poller app Bring back the CI_POLLER_HAS_PENDING variable gate so the cron is truly zero-cost when idle (no runner provisioned). Use a dedicated GitHub App (CI_POLLER_APP_CLIENT_ID) for variable access since neither GITHUB_TOKEN nor sentry-internal-app has the actions_variables:write permission. The app secret is stored in the production environment for branch protection. Flow: - ci-pending.yml: adds ci-pending label (sentry-internal-app) and sets CI_POLLER_HAS_PENDING=true (poller app) - ci-poller.yml: gated by if: vars.CI_POLLER_HAS_PENDING == "true", resets to "false" when all issues resolved (poller app) --- .github/workflows/ci-pending.yml | 14 +++++++++ .github/workflows/ci-poller.yml | 51 ++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-pending.yml b/.github/workflows/ci-pending.yml index 090e33a..1a911a9 100644 --- a/.github/workflows/ci-pending.yml +++ b/.github/workflows/ci-pending.yml @@ -11,6 +11,7 @@ permissions: jobs: mark-pending: runs-on: ubuntu-latest + environment: production if: "startsWith(github.event.issue.title, 'publish: ')" steps: - name: Get auth token @@ -20,6 +21,13 @@ jobs: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }} private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} + - name: Get poller app token + id: poller-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} + private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} + - name: Add ci-pending label env: # Use the app token so the label event triggers publish.yml @@ -29,3 +37,9 @@ jobs: gh issue edit "${{ github.event.issue.number }}" \ -R "$GITHUB_REPOSITORY" \ --add-label "ci-pending" + + - name: Enable cron poller + env: + GH_TOKEN: ${{ steps.poller-token.outputs.token }} + run: | + gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "true" diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index cd83bdd..0f8003a 100644 --- a/.github/workflows/ci-poller.yml +++ b/.github/workflows/ci-poller.yml @@ -11,37 +11,31 @@ permissions: jobs: check-ci: runs-on: ubuntu-latest + environment: production + # Skip entirely (no runner provisioned) when there's nothing to check. + # Set to "true" by ci-pending.yml, reset to "false" here when done. + if: vars.CI_POLLER_HAS_PENDING == 'true' concurrency: group: ci-status-poller cancel-in-progress: false steps: - # Cheap first check with GITHUB_TOKEN — if no ci-pending issues exist, - # exit immediately without generating an app token (~5s idle cost). - - name: Check for pending issues - id: pending - env: - GH_TOKEN: ${{ github.token }} - run: | - count=$(gh issue list -R "$GITHUB_REPOSITORY" \ - --state open \ - --label ci-pending \ - --limit 1 \ - --json number -q 'length') - echo "count=${count}" >> "$GITHUB_OUTPUT" - if [[ "$count" == "0" ]]; then - echo "No ci-pending issues. Nothing to do." - fi - - name: Get auth token - if: steps.pending.outputs.count != '0' id: token uses: actions/create-github-app-token@v3 with: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }} private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} + # Separate app token for writing repo variables — the sentry-internal + # app lacks the actions_variables permission. + - name: Get poller app token + id: poller-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} + private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} + - name: Check CI status for ci-pending issues - if: steps.pending.outputs.count != '0' env: # Use the app token so label changes trigger publish.yml # (GITHUB_TOKEN events are suppressed by GitHub). @@ -55,6 +49,10 @@ jobs: --json number,title,labels,body) count=$(echo "$issues" | jq length) + if [[ "$count" == "0" ]]; then + echo "No ci-pending issues found." + exit 0 + fi echo "Found ${count} ci-pending issue(s)." # Check each issue's CI status @@ -182,3 +180,18 @@ jobs: gh issue comment "$number" -R "$GITHUB_REPOSITORY" --body "$comment" fi done + + # Disable the poller if no ci-pending issues remain. + - name: Disable poller if no pending issues remain + env: + GH_TOKEN: ${{ steps.poller-token.outputs.token }} + run: | + remaining=$(gh issue list -R "$GITHUB_REPOSITORY" \ + --state open \ + --label ci-pending \ + --limit 1 \ + --json number -q 'length') + if [[ "$remaining" == "0" ]]; then + echo "All ci-pending issues resolved. Disabling poller." + gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "false" + fi From aede05e8ae3e2135a58ece181a9ebc4e06fd64e8 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Apr 2026 10:58:42 +0000 Subject: [PATCH 2/7] temp: add workflow_dispatch for manual testing --- .github/workflows/ci-poller.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index 0f8003a..1c0f49a 100644 --- a/.github/workflows/ci-poller.yml +++ b/.github/workflows/ci-poller.yml @@ -3,6 +3,7 @@ name: CI Status Poller on: schedule: - cron: "*/5 * * * *" + workflow_dispatch: permissions: contents: read From acd6ac403d80066adb49906661537755207ac0f1 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Apr 2026 11:19:33 +0000 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20address=20bot=20review=20=E2=80=94?= =?UTF-8?q?=20token=20scoping=20and=20step=20ordering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci-poller.yml: split "disable poller" into two steps — GITHUB_TOKEN for gh issue list (read), poller token only for gh variable set (write). The poller app may only have actions_variables permission. - ci-pending.yml: reorder so label addition (critical) runs before poller token generation (non-critical). If the poller token step fails, the label is already applied. --- .github/workflows/ci-pending.yml | 17 ++++++++++------- .github/workflows/ci-poller.yml | 24 ++++++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci-pending.yml b/.github/workflows/ci-pending.yml index 1a911a9..7ee593d 100644 --- a/.github/workflows/ci-pending.yml +++ b/.github/workflows/ci-pending.yml @@ -21,13 +21,9 @@ jobs: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }} private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} - - name: Get poller app token - id: poller-token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} - private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} - + # Add the label first — this is the critical step. The poller + # token and variable set below are best-effort optimizations + # that must not block label addition. - name: Add ci-pending label env: # Use the app token so the label event triggers publish.yml @@ -38,6 +34,13 @@ jobs: -R "$GITHUB_REPOSITORY" \ --add-label "ci-pending" + - name: Get poller app token + id: poller-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} + private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} + - name: Enable cron poller env: GH_TOKEN: ${{ steps.poller-token.outputs.token }} diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index 1c0f49a..ad662c8 100644 --- a/.github/workflows/ci-poller.yml +++ b/.github/workflows/ci-poller.yml @@ -182,17 +182,25 @@ jobs: fi done - # Disable the poller if no ci-pending issues remain. - - name: Disable poller if no pending issues remain + # Check if any ci-pending issues remain (using GITHUB_TOKEN for + # read access — the poller app may only have actions_variables). + - name: Check for remaining pending issues + id: remaining env: - GH_TOKEN: ${{ steps.poller-token.outputs.token }} + GH_TOKEN: ${{ github.token }} run: | - remaining=$(gh issue list -R "$GITHUB_REPOSITORY" \ + count=$(gh issue list -R "$GITHUB_REPOSITORY" \ --state open \ --label ci-pending \ --limit 1 \ --json number -q 'length') - if [[ "$remaining" == "0" ]]; then - echo "All ci-pending issues resolved. Disabling poller." - gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "false" - fi + echo "count=${count}" >> "$GITHUB_OUTPUT" + + # Disable the poller if no ci-pending issues remain. + - name: Disable poller if no pending issues remain + if: steps.remaining.outputs.count == '0' + env: + GH_TOKEN: ${{ steps.poller-token.outputs.token }} + run: | + echo "All ci-pending issues resolved. Disabling poller." + gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "false" From 5b6a635eb2d558735152d8db41678410b29d2163 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Apr 2026 11:31:09 +0000 Subject: [PATCH 4/7] fix: allow workflow_dispatch to bypass variable gate and document race condition --- .github/workflows/ci-poller.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index ad662c8..80f48ce 100644 --- a/.github/workflows/ci-poller.yml +++ b/.github/workflows/ci-poller.yml @@ -15,7 +15,8 @@ jobs: environment: production # Skip entirely (no runner provisioned) when there's nothing to check. # Set to "true" by ci-pending.yml, reset to "false" here when done. - if: vars.CI_POLLER_HAS_PENDING == 'true' + # Always allow workflow_dispatch for manual recovery. + if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == 'workflow_dispatch' concurrency: group: ci-status-poller cancel-in-progress: false @@ -197,6 +198,9 @@ jobs: echo "count=${count}" >> "$GITHUB_OUTPUT" # Disable the poller if no ci-pending issues remain. + # Race window: ci-pending.yml could set the variable to "true" between + # our check and our set-to-false. Worst case: the new issue waits one + # cron tick (5 min). ci-pending.yml will re-enable on the next opened event. - name: Disable poller if no pending issues remain if: steps.remaining.outputs.count == '0' env: From eb7f2e99b1233f536d10a8f7153d2a102937a0f1 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Apr 2026 11:45:01 +0000 Subject: [PATCH 5/7] fix: move poller token after CI check, add if: always() to cleanup - Poller token step moved after CI check step so a token failure cannot block CI status checking - Cleanup steps (remaining check + disable) use if: always() so CI_POLLER_HAS_PENDING does not get stuck on "true" if the CI check step fails - Poller token only generated when needed (count == 0) --- .github/workflows/ci-poller.yml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index 80f48ce..6c7431d 100644 --- a/.github/workflows/ci-poller.yml +++ b/.github/workflows/ci-poller.yml @@ -28,15 +28,6 @@ jobs: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }} private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} - # Separate app token for writing repo variables — the sentry-internal - # app lacks the actions_variables permission. - - name: Get poller app token - id: poller-token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} - private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} - - name: Check CI status for ci-pending issues env: # Use the app token so label changes trigger publish.yml @@ -183,9 +174,11 @@ jobs: fi done - # Check if any ci-pending issues remain (using GITHUB_TOKEN for - # read access — the poller app may only have actions_variables). + # Cleanup: check if we should disable the poller. Runs even if the + # CI check step above failed, so CI_POLLER_HAS_PENDING doesn't get + # stuck on "true" permanently. - name: Check for remaining pending issues + if: always() id: remaining env: GH_TOKEN: ${{ github.token }} @@ -197,12 +190,22 @@ jobs: --json number -q 'length') echo "count=${count}" >> "$GITHUB_OUTPUT" + # Get the poller app token only when we need to disable the variable. + # Placed here (after CI check) so a token failure can't block CI checking. + - name: Get poller app token + if: always() && steps.remaining.outputs.count == '0' + id: poller-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} + private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} + # Disable the poller if no ci-pending issues remain. # Race window: ci-pending.yml could set the variable to "true" between # our check and our set-to-false. Worst case: the new issue waits one # cron tick (5 min). ci-pending.yml will re-enable on the next opened event. - name: Disable poller if no pending issues remain - if: steps.remaining.outputs.count == '0' + if: always() && steps.remaining.outputs.count == '0' env: GH_TOKEN: ${{ steps.poller-token.outputs.token }} run: | From c4f7d014dc0855311cc81308fd8e3b4eb7558363 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Apr 2026 12:03:06 +0000 Subject: [PATCH 6/7] fix: guard disable step on token success, add continue-on-error for best-effort steps - ci-poller.yml: disable step checks steps.poller-token.outcome == success so it does not run (and fail) when token generation failed - ci-pending.yml: poller token + variable set steps use continue-on-error since they are best-effort optimizations that must not block the critical label addition --- .github/workflows/ci-pending.yml | 5 +++++ .github/workflows/ci-poller.yml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-pending.yml b/.github/workflows/ci-pending.yml index 7ee593d..2b05274 100644 --- a/.github/workflows/ci-pending.yml +++ b/.github/workflows/ci-pending.yml @@ -34,14 +34,19 @@ jobs: -R "$GITHUB_REPOSITORY" \ --add-label "ci-pending" + # Best-effort: enable the cron poller variable. If this fails, the + # poller's cron will still pick up the issue (it just won't skip the + # runner provisioning on idle ticks until the variable is set). - name: Get poller app token id: poller-token + continue-on-error: true uses: actions/create-github-app-token@v3 with: client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} - name: Enable cron poller + if: steps.poller-token.outcome == 'success' env: GH_TOKEN: ${{ steps.poller-token.outputs.token }} run: | diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index 6c7431d..28355d1 100644 --- a/.github/workflows/ci-poller.yml +++ b/.github/workflows/ci-poller.yml @@ -205,7 +205,7 @@ jobs: # our check and our set-to-false. Worst case: the new issue waits one # cron tick (5 min). ci-pending.yml will re-enable on the next opened event. - name: Disable poller if no pending issues remain - if: always() && steps.remaining.outputs.count == '0' + if: always() && steps.remaining.outputs.count == '0' && steps.poller-token.outcome == 'success' env: GH_TOKEN: ${{ steps.poller-token.outputs.token }} run: | From ba6dd68759dee5342afd17eac35f0ee4aa79f049 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Apr 2026 13:58:39 +0000 Subject: [PATCH 7/7] fix: sync poller variable with actual pending issue state When workflow_dispatch bypasses the variable gate (variable is "false") and finds pending issues that CI has not finished yet, the variable must be set back to "true" so subsequent cron ticks continue checking. Previously only the count==0 path was handled (disable). Now both paths are covered: enable when issues remain, disable when resolved. --- .github/workflows/ci-poller.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index 28355d1..4d8e3fa 100644 --- a/.github/workflows/ci-poller.yml +++ b/.github/workflows/ci-poller.yml @@ -190,24 +190,28 @@ jobs: --json number -q 'length') echo "count=${count}" >> "$GITHUB_OUTPUT" - # Get the poller app token only when we need to disable the variable. - # Placed here (after CI check) so a token failure can't block CI checking. + # Update the poller variable to match reality: + # - Remaining issues → ensure variable is "true" (important when + # workflow_dispatch bypassed the gate while variable was "false") + # - No remaining issues → set to "false" to stop the cron + # Placed after CI check so a token failure can't block CI checking. - name: Get poller app token - if: always() && steps.remaining.outputs.count == '0' + if: always() id: poller-token uses: actions/create-github-app-token@v3 with: client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} - # Disable the poller if no ci-pending issues remain. - # Race window: ci-pending.yml could set the variable to "true" between - # our check and our set-to-false. Worst case: the new issue waits one - # cron tick (5 min). ci-pending.yml will re-enable on the next opened event. - - name: Disable poller if no pending issues remain - if: always() && steps.remaining.outputs.count == '0' && steps.poller-token.outcome == 'success' + - name: Sync poller variable with pending issue state + if: always() && steps.poller-token.outcome == 'success' env: GH_TOKEN: ${{ steps.poller-token.outputs.token }} run: | - echo "All ci-pending issues resolved. Disabling poller." - gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "false" + if [[ "${{ steps.remaining.outputs.count }}" == "0" ]]; then + echo "All ci-pending issues resolved. Disabling poller." + gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "false" + else + echo "Still pending issues. Ensuring poller stays enabled." + gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "true" + fi