Skip to content

feat: add daily workflow syncing Claude workflows with upstream action docs - #227

Merged
morisil merged 4 commits into
mainfrom
claude-action-docs-sync-workflow
Aug 15, 2026
Merged

feat: add daily workflow syncing Claude workflows with upstream action docs#227
morisil merged 4 commits into
mainfrom
claude-action-docs-sync-workflow

Conversation

@morisil

@morisil morisil commented Aug 14, 2026

Copy link
Copy Markdown
Member

Adds claude-action-docs-sync.yml, a daily scheduled workflow that runs Claude Code in automation mode to:

  • read the upstream anthropics/claude-code-action documentation (README, setup, usage, configuration, FAQ, migration guide, examples) and the code.claude.com GitHub Actions page,
  • compare the current recommendations against this repository's Claude workflows (claude-code.yml, claude-code-review.yml, claude.yml, review.yml),
  • and, only when a real upstream recommendation change is found, open a PR with the minimal edits (branch prefix claude-action-docs-sync-, deduplicated against already-open proposals).

The prompt encodes the CLAUDE.md constraints: reusable workflow inputs/secrets/jobs must not be renamed (called from other org repos), and changes go through a PR since main is ruleset-protected. Checkout and gh use WORKFLOW_SECRET because pushing edits under .github/workflows/ requires a workflow-scoped token.

🤖 Generated with Claude Code

…n docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review: claude-action-docs-sync.yml

Nice addition — closes a real gap (upstream claude-code-action recommendations silently drifting from this repo's workflows), and the prompt correctly encodes the CLAUDE.md constraints (no renaming reusable-workflow inputs/secrets/jobs, PR-only since main is ruleset-protected).

Security concern: unscoped Bash tool + privileged token + untrusted external content

claude_args: '--allowed-tools "Bash,WebFetch,WebSearch"' grants unrestricted Bash, while GH_TOKEN/checkout token is secrets.WORKFLOW_SECRET — a token with workflow scope, powerful enough to push under .github/workflows/ and open PRs.

Every other Claude workflow in this repo scopes Bash down to specific subcommands instead of granting it wholesale, e.g. claude-code-review.yml:

--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),WebSearch,WebFetch"

This workflow is the first to combine unrestricted Bash with a WORKFLOW_SECRET-level credential and content fetched from external URLs (raw.githubusercontent.com, code.claude.com) via WebFetch. If any of those upstream pages were compromised or crafted to contain injected instructions, the agent has both the tool surface (arbitrary Bash) and the credential (workflow-scoped PAT) to act on them — e.g. pushing to arbitrary branches or running commands beyond git/gh. Since this runs unattended on a daily schedule (no human trigger like the @claude comment gate in claude-code.yml), there's no human-in-the-loop checkpoint before execution.

Worth considering scoping this to what the task actually needs, e.g.:

Bash(git:*),Bash(gh pr list:*),Bash(gh pr create:*),Bash(date:*)

Even if that list ends up fairly broad (this task legitimately needs more git/gh surface than the review workflow), an explicit scope is safer than bare Bash, and keeps the pattern consistent with the rest of the repo. Worth at least a comment explaining the reasoning if unrestricted Bash is kept intentionally.

Minor: no concurrency guard

The proposal branch name is claude-action-docs-sync-<YYYYMMDD> (day granularity). If workflow_dispatch is triggered manually the same day the schedule also fires, both runs could race to create/push the same branch name. A concurrency: group: claude-action-docs-sync block would make overlapping runs safe rather than relying on same-day timing being unlikely.

Minor: self-reference gap

claude-action-docs-sync.yml itself calls anthropics/claude-code-action@v1, but isn't in the list of files the job compares against upstream recommendations (only claude-code.yml, claude-code-review.yml, claude.yml, review.yml are). Might be intentional (avoids the job editing itself mid-run), but worth confirming that's the reasoning rather than an oversight.

Positive notes

  • permissions: { contents: read, id-token: write } is nicely minimal — correctly relies on WORKFLOW_SECRET for all writes/PR-creation rather than over-granting the default GITHUB_TOKEN, better than some sibling workflows here.
  • The prompt's step 4 (stop cleanly with no branch/PR if nothing changed) and step 5 (dedupe against an existing open proposal branch) are good design choices that avoid PR spam.
  • timeout-minutes: 30 is a sensible bound given the number of upstream docs fetched.

No test coverage is applicable here beyond a manual workflow_dispatch dry run, given this is a scheduled automation workflow rather than application code.

…ompare

- Scope --allowed-tools to the exact git/gh/date commands the task needs
  instead of unrestricted Bash, matching the pattern of sibling workflows.
- Add a concurrency group so a manual dispatch cannot race a same-day
  scheduled run for the same proposal branch.
- Include claude-action-docs-sync.yml itself in the files compared
  against upstream recommendations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@morisil

morisil commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

Addressed the review in 2508031:

  • Bash scoping: replaced unrestricted Bash with Bash(git:*),Bash(gh pr list:*),Bash(gh pr create:*),Bash(date:*) — the exact surface steps 5–6 of the prompt use — plus a comment explaining why the scope matters for an unattended job holding a workflow-scoped token while reading external content.
  • Concurrency guard: added concurrency: group: claude-action-docs-sync so a manual dispatch cannot race a same-day scheduled run for the same proposal branch.
  • Self-reference: the gap was an oversight, not intentional — editing the file on a branch does not affect the running job, so claude-action-docs-sync.yml is now included in the list of compared files.

🤖 Generated with Claude Code

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review

Nicely scoped workflow — it correctly encodes the CLAUDE.md constraints (no renaming reusable-workflow inputs/secrets/jobs, PR-only changes since main is ruleset-protected) directly into the prompt, dedups against already-open proposal branches, and only opens a PR when something actually changed. A few notes:

Security

  • Broad Bash(git:*) combined with untrusted fetched content and a workflow-scoped token is the main risk surface. The agent runs unattended, calls WebFetch/WebSearch against external content (upstream README/docs, code.claude.com), and holds WORKFLOW_SECRET (a token broad enough to push and open PRs touching .github/workflows/). If any fetched page ever contained an injected instruction, the blanket git:* grant lets it run any git subcommand (e.g. git config, git remote, git push --force, git reset --hard) rather than just the ones this task needs. Since the commit message for the second commit already shows intent to scope tools tightly ("Scope --allowed-tools to the exact git/gh/date commands the task needs instead of unrestricted Bash"), it'd be consistent to go one step further and enumerate the actual git subcommands needed — e.g. Bash(git checkout:*),Bash(git branch:*),Bash(git add:*),Bash(git commit:*),Bash(git push:*),Bash(git config:*) — instead of the wildcard Bash(git:*). Defense-in-depth given the token scope and unattended execution.

Potential bugs / edge cases

  • Docs are fetched from anthropics/claude-code-action's main branch rather than the latest released tag. If upstream docs on main describe not-yet-released behavior, the sync job could propose changes ahead of an actual release. Worth confirming this is intentional (freshness) vs. pinning to the latest tag.
  • If a previous run pushes the claude-action-docs-sync-<YYYYMMDD> branch but fails before gh pr create (e.g. transient network error), a same-day rerun's dedup check (step 5, which only looks at open PRs) won't catch the orphaned branch, and git checkout -b / git push could hit a naming collision or non-fast-forward push later that day. Low-probability, and it would fail loudly rather than silently misbehave, but worth being aware of.

Code quality / consistency

  • Good alignment with the existing code-statistics.yml pattern (scheduled + workflow_dispatch, WORKFLOW_SECRET for pushing to protected main, gh pr list/gh pr create dedup logic).
  • The self-referential inclusion of claude-action-docs-sync.yml itself in the compared files (added in the second commit) is a nice touch — otherwise this workflow's own --allowed-tools usage would drift silently.

Test coverage

  • No automated test, but that matches the repo's existing convention for scheduled workflows (none of the other cron-triggered workflows have tests either), and workflow_dispatch gives a way to manually verify behavior before the first scheduled run.

Overall this is a solid, well-scoped addition — the main actionable item is tightening Bash(git:*) to the specific subcommands actually used, given the combination of unattended execution, external content ingestion, and a workflow-scoped push token.

…ighter git scope

- Fetch upstream docs at the v1 release tag instead of main, so
  recommendations match the action version the workflows actually run.
- Extend the dedup step to detect and delete orphaned proposal branches
  left by a run that failed between push and PR creation.
- Enumerate git subcommands in --allowed-tools (including read-only
  status/diff/log and ls-remote) instead of a blanket git:* grant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@morisil

morisil commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

Addressed the second review in e86fe85:

  • Docs vs. released tag: the prompt now fetches upstream docs at the v1 release tag — the floating tag these workflows actually run — instead of main, so the job can no longer propose changes based on not-yet-released behavior. The code.claude.com page stays as-is since it cannot be versioned and documents the current release.
  • Orphaned branch edge case: step 5 now also runs git ls-remote --heads origin 'claude-action-docs-sync-*' and deletes any branch with the prefix that has no open PR — a leftover from a run that failed between push and gh pr create. Safe because the concurrency group guarantees no parallel run owns it.
  • Bash(git:*) scope: replaced with enumerated subcommands (config, checkout, branch, add, commit, push, ls-remote, plus read-only status/diff/log so the agent does not stall mid-task).

🤖 Generated with Claude Code

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review: claude-action-docs-sync.yml

Nice design overall — self-referential inclusion in the compared files, orphan-branch cleanup, and pinning the upstream docs fetch to the v1 tag (matching the @v1 action reference the workflows actually run, rather than main which may describe unreleased behavior) are all thoughtful touches that address real edge cases.

Security / permissions

  • id-token: write — I don't see anything in this workflow that performs OIDC-based auth (no cloud provider login, no attestation step). Since auth to Claude goes through claude_code_oauth_token, this permission looks unused. Worth double-checking whether claude-code-action@v1 actually requires it; if not, dropping it keeps the token's blast radius as small as possible for an unattended job holding a workflow-scoped PAT.
  • WebSearch in --allowed-tools — the prompt hardcodes every upstream URL it needs (raw.githubusercontent.com docs + the code.claude.com page). Since nothing in the task requires discovering additional pages, granting WebSearch widens the egress surface of an unattended job without a clear use case. Consider dropping it unless open-ended discovery is intentional.

Destructive operation via prompt reasoning

The orphan-branch deletion (step 5: git ls-remotegit push origin --delete <branch>) is a destructive git operation whose correctness depends entirely on the LLM's interpretation of "orphan" at prompt-execution time, rather than a scripted/code-enforced check. The concurrency group protects against a same-repo scheduled/dispatch race, but doesn't protect against, e.g., a human or another process having just created a same-prefixed branch moments before the run starts. Given this job runs unattended with a workflow-scoped token, it might be worth tightening this to a narrower, non-LLM-judged condition (e.g., only delete branches whose HEAD commit is older than N hours) rather than relying purely on natural-language instructions for a delete.

Minor / nit

  • The trailing comment # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md links to main, while the fetch URLs earlier in the prompt deliberately pin to v1 for the stated reason that main may describe not-yet-released behavior. Just a human-facing comment, but pinning it too would keep the rationale consistent.
  • No CI (actionlint/yamllint) appears to run on workflow YAML changes in this repo (only claude-review shows in the PR's status checks) — not a blocker, just flagging that a YAML/schema typo here wouldn't be caught before merge.

What's solid

  • Scoping --allowed-tools to exact git/gh/date subcommands instead of blanket Bash or git:* is good unattended-job hygiene.
  • Dedup against open proposal PRs before creating a new one avoids duplicate-PR spam.
  • Correctly follows the CLAUDE.md constraints: never renames reusable workflow inputs/secrets/jobs, and routes changes through a PR since main is ruleset-protected.

🤖 Generated with Claude Code

…doc pins

- Move orphaned-branch deletion out of the agent prompt into a
  deterministic workflow step: delete only prefix-matching branches with
  no open PR whose tip is older than one hour, so a destructive git
  operation never depends on LLM judgement and recently pushed human
  branches are protected. The prompt now forbids the agent from
  force-pushing or deleting branches.
- Drop WebSearch and Bash(git ls-remote:*) from --allowed-tools —
  every URL the task needs is hardcoded and the agent no longer
  inspects remote branches.
- Comment why id-token: write is required (claude-code-action fetches
  a GitHub OIDC token for its GitHub App authentication).
- Pin the trailing usage.md comment link to v1, matching the fetch URLs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@morisil

morisil commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

Addressed the third review in be07752:

  • id-token: write: kept, but now with an explaining comment — it is not unused. Upstream docs/setup.md states the action's default GitHub App authentication path requires id-token: write so it can fetch a GitHub OIDC token; both sibling Claude workflows here grant it for the same reason.
  • WebSearch: dropped from --allowed-tools, with a comment noting that every URL the task needs is hardcoded in the prompt.
  • Destructive operation via prompt reasoning: agreed — orphan-branch deletion is now a deterministic scripted step (Delete orphaned proposal branches) that runs before the agent: it deletes a prefix-matching branch only when it has no open PR and its tip commit is older than one hour, protecting branches a human may have just pushed. The prompt no longer instructs any deletion — it now explicitly forbids the agent from force-pushing or deleting branches, and Bash(git ls-remote:*) was removed from the allowed tools since the agent no longer needs it.
  • Nit: the trailing usage.md comment link is now pinned to v1, consistent with the fetch URLs.

On the actionlint observation: noted — adding workflow linting to CI would be a separate change beyond this PR's scope.

🤖 Generated with Claude Code

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review: claude-action-docs-sync.yml

This is a well thought-out workflow, and the commit history shows real hardening over four rounds (scoped --allowed-tools, deterministic non-LLM branch cleanup, tag-pinned docs, dropping WebSearch). A few things worth a look before merge:

Potential bug — orphan-cleanup step can fail the whole run on a single transient error

git ls-remote --heads origin 'claude-action-docs-sync-*' | while read -r sha ref; do
  ...
  committed=$(gh api "repos/$GITHUB_REPOSITORY/commits/$sha" --jq '.commit.committer.date')
  age=$(( $(date +%s) - $(date -d "$committed" +%s) ))
  ...
done

GitHub Actions runs run: blocks under bash --noprofile --norc -eo pipefail {0} by default. If gh api .../commits/$sha ever fails for one dangling branch (rate limit, transient network blip, GC'd commit), committed comes back empty, date -d "" +%s fails, and the $(( ... )) arithmetic throws a syntax error. Because the while loop is the read side of a pipe, it runs in a subshell — so -e only kills that subshell early, but its non-zero exit status then propagates back to the outer pipeline command, which under the outer script's -e aborts the entire step (and therefore the job, since Run Claude Code never runs). One flaky gh api call on any leftover branch would silently skip the day's sync entirely. Consider guarding the per-branch body (e.g. || continue around the gh api/date calls, or check $? before the arithmetic) so one bad lookup doesn't cancel the whole run.

Minor — age guard is based on commit date, not push date

The comment says the one-hour guard "protects branches someone may have just pushed outside this workflow," but committed is the commit's committer date, not when it was pushed. A branch created from an older existing commit (rebase/cherry-pick) would be immediately eligible for deletion even though it was pushed seconds ago. Narrow edge case given the branch-name filter is scoped to this workflow's own prefix, but the comment slightly overstates what the check actually guards against.

Security — docs pinned to the v1 tag, not a SHA

Fetching upstream docs at v1 rather than a commit SHA means the read content isn't fully immutable (tags can be moved by upstream maintainers). This mirrors the action's own floating @v1 pin used elsewhere in the repo and is a reasonable, intentional tradeoff — just flagging it as the accepted-risk source if a future sync run behaves unexpectedly. The --allowed-tools scoping (no unrestricted Bash, no WebSearch, hardcoded URLs) plus the ruleset-enforced PR review already do most of the work of containing an autonomous agent that ingests external content and proposes code changes.

Test coverage

No automated test exists (expected for a workflow-only change), but this combines shell scripting with LLM-driven git/PR operations, which is easy to get subtly wrong in ways CI won't catch. Worth a manual workflow_dispatch run after merge to confirm both paths end-to-end: the normal fetch → compare → PR-create flow, and the orphan-branch deletion path (e.g. by deliberately leaving a stale claude-action-docs-sync-* branch with no open PR).

Nits

  • permissions: contents: read / id-token: write at the job level correctly reflects that all privileged git/gh operations go through the WORKFLOW_SECRET PAT rather than the default GITHUB_TOKEN — nice attention to least privilege.
  • Consistent with CLAUDE.md's noted gotcha that main is ruleset-protected and reusable workflow inputs/secrets/jobs must not be renamed — the prompt explicitly encodes both constraints.

Nothing here is blocking; the orphan-cleanup robustness point is the one I'd actually want addressed before relying on the daily cron.

@morisil
morisil merged commit 9f0f478 into main Aug 15, 2026
1 check passed
@morisil
morisil deleted the claude-action-docs-sync-workflow branch August 15, 2026 11:59
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.

1 participant