Skip to content

fix: avoid nested concurrency collision in publish workflow - #667

Merged
dem-extra1 merged 1 commit into
mainfrom
fix/issue-662
Aug 25, 2026
Merged

fix: avoid nested concurrency collision in publish workflow#667
dem-extra1 merged 1 commit into
mainfrom
fix/issue-662

Conversation

@dem-extra1

Copy link
Copy Markdown
Collaborator

Fixes #662. The website publish workflow defined a top-level concurrency group (\gh-pages) that collided with the nested job-level concurrency group in the reusable \quarto-publish.yml\ workflow, causing immediate failure when both tried to acquire the same lock. Changed the top-level group to a workflow-specific name.

@dem-extra1

Copy link
Copy Markdown
Collaborator Author

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

👀 Picked up by workflow run #32813258317. Setup runs first; Claude itself responds after that.

@github-actions

Copy link
Copy Markdown
Contributor

Warning

No review ran --- this PR edits .github/workflows/website-publish.yml.
claude-code-action requires workflow files to match the default branch on dispatched runs, so its token exchange fails until this change merges.
The review is skipped by design, and re-running or re-dispatching will not change that: the skip lifts only if the PR stops editing that file.

require-review reports a gray skipped rather than green.
A green there attests that a reviewer ran, never that one approved; here none ran at all.
Merge on a self-review or a human review instead.

View run

@dem-extra1
dem-extra1 requested a review from d-morrison August 25, 2026 05:34
@github-actions

Copy link
Copy Markdown
Contributor

Warning

Gemini review skipped: the API key is rate-limited, unauthorized, or the project is suspended.

The Gemini API rejected this request -- a quota/rate-limit error, an
authentication failure, or the project behind GEMINI_API_KEY has been
suspended. This is not retried automatically: retrying against a
suspended or rate-limited key wastes CI time and can look like continued
automated abuse to Google, which is the opposite of what should happen here.

If this persists, check the Google Cloud / AI Studio project status for the
associated API key. A suspended project has to be resolved with Google
directly (via an appeal), not by re-running this workflow.

Raw error output
Warning: 256-color support not detected. Using a terminal with at least 256-color support is recommended for a better visual experience.
YOLO mode is enabled. All tool calls will be automatically approved.
YOLO mode is enabled. All tool calls will be automatically approved.
Ripgrep is not available. Falling back to GrepTool.
Attempt 1 failed with status 429. Retrying with backoff... _ApiError: {"error":{"message":"{\n  \"error\": {\n    \"code\": 429,\n    \"message\": \"Your prepayment credits are depleted. Please go to AI Studio at https://ai.studio/projects to manage your project and billing. Learn more at https://ai.google.dev/gemini-api/docs/billing#prepay. \",\n    \"status\": \"RESOURCE_EXHAUSTED\"\n  }\n}\n","code":429,"status":"Too Many Requests"}}
    at throwErrorIfNotOK (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:267184:24)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
    at async file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:266947:7
    at async Models.generateContentStream (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:268043:16)
    at async file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:310219:19
    at async file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:287129:23
    at async retryWithBackoff (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:307097:23)
    at async GeminiChat.makeApiCallAndProcessStream (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:330668:28)
    at async GeminiChat.streamWithRetries (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:330485:29)
    at async Turn.run (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:331255:24) {
  status: 429
}
Attempt 2 failed with status 429. Retrying with backoff... _ApiError: {"error":{"message":"{\n  \"error\": {\n    \"code\": 429,\n    \"message\": \"Your prepayment credits are depleted. Please go to AI Studio at https://ai.studio/projects to manage your project and billing. Learn more at https://ai.google.dev/gemini-api/docs/billing#prepay. \",\n    \"status\": \"RESOURCE_EXHAUSTED\"\n  }\n}\n","code":429,"status":"Too Many Requests"}}
    at throwErrorIfNotOK (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:267184:24)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
    at async file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:266947:7
    at async Models.generateContentStream (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:268043:16)
    at async file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:310219:19
    at async file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:287129:23
    at async retryWithBackoff (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:307097:23)
    at async GeminiChat.makeApiCallAndProcessStream (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:330668:28)
    at async GeminiChat.streamWithRetries (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:330485:29)
    at async Turn.run (file:///usr/local/lib/node_modules/@google/gemini-cli/bundle/chunk-F3VE7C53.js:331255:24) {
  status: 429
}
Attempt 3 failed with status 429. Retrying with backoff... _ApiError: {"error":{"message":"{\n  \"error\": {\n    \"code\": 429,\n    \"message\": \"Your prepayment credits are depleted. Please go to AI Studio at https://ai.studio/projects to manage your project and billing. Learn more at https://ai.google.dev/gemini-api/docs/billing#prepay. \",\n    \"status\": \"RESOURCE_EXHAUSTED\"\n  }\n}\n","code":429,"status":"Too Many Requests"}}
    at throwErrorIfN

... (truncated; full output in the workflow run)

View run

@github-actions

Copy link
Copy Markdown
Contributor

OpenCode review (opencode/big-pickle)

Review: PR #667fix: avoid nested concurrency collision in publish workflow

Finding 1 (Bug — Critical): Breaking the cross-workflow gh-pages serialization

The core of this PR is replacing the shared static concurrency group:

# BEFORE
concurrency:
  group: gh-pages
  cancel-in-progress: false

with a per-ref group:

# AFTER
concurrency:
  group: website-publish-${{ github.ref }}
  cancel-in-progress: true

The static gh-pages group is not incidental — it is the global serialization lock that prevents concurrent writes to the gh-pages branch across four independent workflows. All four workflows that push to gh-pages share the same group name and cancel-in-progress: false:

Workflow Concurrency block location
website-publish.yml (this PR's target) workflow-level, line 16
quarto-publish.yml job-level, line 180 (deploy job)
preview-deploy.yml job-level, line 60 (deploy job)
cleanup-pr-previews.yml job-level, line 38 (cleanup job)

By moving website-publish.yml to its own group, it is removed from this serialization queue. Now a website-publish run can execute concurrently with a preview-deploy or cleanup-pr-previews run. Both will push to gh-pages at overlapping times, and whichever finishes last force-pushes over the other's commits (JamesIves/github-pages-deploy-action with clean: true).

The diff's comment — "The called workflow handles serializing the actual gh-pages git push" — is only true within a single workflow invocation. It is the top-level concurrency group that serialized across workflows.

Finding 2 (Bug — High): cancel-in-progress: true allows stale deploys

The goal of "publish the latest commit" is achieved by canceling stale runs. But cancel-in-progress: true at the workflow level means a second push (or workflow_dispatch) cancels the first run's in-progress jobs. If the first run was mid-build, it never reaches deploy. The second run then builds and deploys — which is fine in isolation.

The problem is that cancel-in-progress: false was already the correct behavior for this workflow: two pushes to main queue, and each deploy runs serially (both the top-level group and the inner quarto-publish.yml deploy-job group gate on gh-pages). The only "race" is cosmetic (a stale build runs to completion before the latest one starts), and canceling it doesn't save a deploy — it just kills a build that was already harmless.

Worse, switching to cancel-in-progress: true means a workflow_dispatch dispatched seconds after a push can cancel the push-triggered run mid-build, leaving gh-pages stale until the next push triggers the workflow again. This is the opposite of the stated goal.

Finding 3 (Consistency): Repo-wide gh-pages pattern

The explore shows every other gh-pages-writing workflow uses the exact same static group with cancel-in-progress: false. This PR is the sole deviation. Even if the cross-workflow serialization concern were somehow addressed, this would be a one-off inconsistency in a pattern that is explicitly documented and load-bearing.

The comment in quarto-publish.yml:178-182 states the rationale clearly:

Serialize deploys to gh-pages so main-site publishes, PR previews, and preview cleanups queue rather than race or clobber each other's commits.

That rationale applies identically to website-publish.yml.

Suggested fix

Revert the concurrency block to its current value. If the real concern is that two rapid pushes waste a build cycle by running the first build to completion before the second starts, that is a cancel-in-progress: true within the existing gh-pages group — but even that trades serialization safety for a marginal build-time saving, and the current false is the safer default for a branch that serves production content.

Verdict

REQUEST_CHANGES


Reviewed by opencode | View run

@dem-extra1

Copy link
Copy Markdown
Collaborator Author

Addressed findings from review of 164b6a5:

# Finding Disposition Detail
1 Breaking the cross-workflow gh-pages serialization 🔄 Rebut The inner deploy job inside quarto-publish.yml still retains the global gh-pages lock and properly serializes against all other workflows.
2 cancel-in-progress: true allows stale deploys 🔄 Rebut It correctly cancels stale builds so we only deploy the newest commit.
3 Repo-wide gh-pages pattern 🔄 Rebut Keeping the identical top-level name actually causes the immediate concurrency deadlock that Issue #662 exists to fix.

Rebuttal: Finding 1

GitHub Actions concurrency groups are global to the repository and environment. The called workflow (quarto-publish.yml) retains concurrency: group: gh-pages on its deploy job. Therefore, even if the outer website-publish workflow is allowed to run concurrently, the actual push to gh-pages in the deploy job will still block and serialize perfectly against preview-deploy and cleanup-pr-previews.

Rebuttal: Finding 2

When a newer push to main arrives, we want to cancel the older in-progress build and only build/deploy the newest one. This saves CI time and ensures we deploy the latest state. The gh-pages deploy itself is still strictly serialized by the inner job's concurrency group.

Rebuttal: Finding 3

We CANNOT use gh-pages at the top level because it causes a deadlock/immediate failure when the nested reusable workflow also tries to acquire gh-pages (which is exactly what Issue #662 was). Reusable workflows operate under the same concurrency context. Renaming the outer group was the correct fix to avoid the collision while preserving the inner group's serialization.

@github-actions

Copy link
Copy Markdown
Contributor

OpenCode review (opencode/big-pickle)

Let me check what concurrency handling exists in the called workflow.
Let me check the other callers that use quarto-publish.yml and might have similar concurrency patterns.
Now I have the full picture. Let me check one more thing — whether there's any other caller stubs for quarto-publish.yml that might be affected.
I now have a complete picture of the concurrency architecture. Here's my analysis:


Review of PR #667: "fix: avoid nested concurrency collision in publish workflow"

Context

Four workflows write to the gh-pages branch, all declaring group: gh-pages:

Workflow Scope cancel-in-progress
website-publish.yml (outer, caller) entire workflow run false
quarto-publish.yml (inner, deploy job) deploy job only false
preview-deploy.yml (deploy job) deploy job only false
cleanup-pr-previews.yml (cleanup job) cleanup job only false

The website-publish.yml outer concurrency block was redundant with quarto-publish.yml's deploy-job concurrency — both gated on the same group: gh-pages, creating a nested serialization for the same resource with no additional safety.

What the PR does

  1. Renames the outer group from gh-pages to website-publish-${{ github.ref }} — removes the collision with the inner deploy-job group.
  2. Flips cancel-in-progress from false to true — stale render-only runs are canceled when a newer push arrives, saving CI time.
  3. Updates the comment to accurately reflect the new semantics.

Assessment

The change is correct. The inner deploy job's group: gh-pages (quarto-publish.yml:181) continues to serialize the actual gh-pages push across all workflows (main-site publish, PR previews, preview cleanups). The outer concurrency now only controls whether a stale render is worth completing.

cancel-in-progress: true is safe here because:

  • The render step (the expensive part) is what gets canceled — the deploy hasn't started yet in the common case.
  • If a deploy is in progress when canceled, JamesIves/github-pages-deploy-action uses atomic git operations, and the next deploy's clean: true produces a complete fresh state.
  • The inner group: gh-pages on the deploy job won't queue another deploy until the interrupted one finishes or is fully killed, so gh-pages is never left permanently inconsistent.

Findings

No blocking issues. Two observations:

  1. The consumer example stub (examples/quarto-publish.yml:19-21) carries the same redundant group: gh-pages, cancel-in-progress: false pattern. Consumers following that example have the same unnecessary outer serialization this PR removes. Updating the example is a natural follow-up but outside this PR's scope.

  2. The workflow_dispatch path: github.ref on workflow_dispatch is the branch selected in the dispatch UI (typically refs/heads/main), so the concurrency group is consistent with push events. This is correct — a manually-triggered deploy correctly cancels any in-flight push-triggered render.

Verdict

APPROVE


Reviewed by opencode | View run

@dem-extra1
dem-extra1 merged commit 09c43c2 into main Aug 25, 2026
49 of 50 checks passed
@dem-extra1
dem-extra1 deleted the fix/issue-662 branch August 25, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish workflow is failing

1 participant