- Add command execution rules to prompt (no env prefixes, no 2>&1) - Document pre-existing test failures so Claude doesn't waste turns - Add Bash(echo:*) and Bash(cat:*) to allowed_tools - Delete stale remote branches before creating new ones Addresses 24 permission denials in last run where Claude spent ~15 of 47 turns on blocked commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
399 lines
18 KiB
YAML
399 lines
18 KiB
YAML
# Generated by KCL v1.37.1 - DO NOT EDIT
|
|
# Source: .forgejo/kcl-ci/main.k
|
|
# kcl-ci local: ~/stuff/gitrepos/wxs/actions/kcl-ci
|
|
# kcl-ci remote: https://git.wxs.ro/actions/kcl-ci
|
|
|
|
name: Upstream Sync Check
|
|
'on':
|
|
schedule:
|
|
- cron: '0 8 * * 1'
|
|
workflow_dispatch: {}
|
|
env:
|
|
BUN_VERSION: '1.2.21'
|
|
jobs:
|
|
check-upstream:
|
|
name: Check for upstream updates
|
|
runs-on: grunt
|
|
outputs:
|
|
needs_sync: ${{ steps.compare.outputs.needs_sync }}
|
|
behind: ${{ steps.compare.outputs.behind }}
|
|
ahead: ${{ steps.compare.outputs.ahead }}
|
|
issue_number: ${{ steps.sync-issue.outputs.issue_number }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2
|
|
with:
|
|
fetch-depth: '0'
|
|
- name: Configure upstream remote
|
|
run: |-
|
|
if ! git remote get-url upstream &> /dev/null; then
|
|
git remote add upstream https://github.com/anthropics/claude-code-action.git
|
|
fi
|
|
- name: Fetch upstream
|
|
run: git fetch upstream main --quiet
|
|
- name: Compare with upstream
|
|
id: compare
|
|
run: |-
|
|
BEHIND_COUNT=$(git rev-list main..upstream/main --count)
|
|
AHEAD_COUNT=$(git rev-list upstream/main..main --count)
|
|
|
|
echo "behind=$BEHIND_COUNT" >> "$GITHUB_OUTPUT"
|
|
echo "ahead=$AHEAD_COUNT" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "Our main is $BEHIND_COUNT commits behind and $AHEAD_COUNT commits ahead of upstream"
|
|
|
|
if [ "$BEHIND_COUNT" -gt 0 ]; then
|
|
echo "needs_sync=true" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "--- New upstream commits ---"
|
|
git log main..upstream/main --oneline | head -30
|
|
|
|
echo ""
|
|
echo "--- Files changed ---"
|
|
DIFF_STAT=$(git diff --stat main...upstream/main)
|
|
echo "$DIFF_STAT"
|
|
|
|
{
|
|
echo "diff_stat<<DIFF_EOF"
|
|
echo "$DIFF_STAT"
|
|
echo "DIFF_EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
{
|
|
echo "commit_log<<LOG_EOF"
|
|
git log main..upstream/main --oneline | head -30
|
|
echo "LOG_EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "needs_sync=false" >> "$GITHUB_OUTPUT"
|
|
echo "Already up to date with upstream"
|
|
fi
|
|
- name: Check for existing sync issue
|
|
if: steps.compare.outputs.needs_sync == 'true'
|
|
id: existing
|
|
run: |-
|
|
RESPONSE=$(curl -s \
|
|
-H "Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
"${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}/issues?state=open&labels=upstream-sync&limit=1")
|
|
|
|
ISSUE_COUNT=$(echo "$RESPONSE" | jq 'length')
|
|
|
|
if [ "$ISSUE_COUNT" -gt 0 ]; then
|
|
ISSUE_NUMBER=$(echo "$RESPONSE" | jq '.[0].number')
|
|
echo "issue_exists=true" >> "$GITHUB_OUTPUT"
|
|
echo "issue_number=$ISSUE_NUMBER" >> "$GITHUB_OUTPUT"
|
|
echo "Found existing sync issue #$ISSUE_NUMBER"
|
|
else
|
|
echo "issue_exists=false" >> "$GITHUB_OUTPUT"
|
|
echo "No existing sync issue found"
|
|
fi
|
|
- name: Ensure upstream-sync label exists
|
|
if: steps.compare.outputs.needs_sync == 'true'
|
|
id: label
|
|
run: |
|
|
API="${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}"
|
|
AUTH="Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}"
|
|
|
|
# Try to create the label (409 = already exists, which is fine)
|
|
curl -s -o /dev/null -w "%{http_code}" \
|
|
-X POST -H "$AUTH" -H "Content-Type: application/json" \
|
|
-d '{"name":"upstream-sync","color":"#0075ca","description":"Upstream sync tracking"}' \
|
|
"$API/labels" || true
|
|
|
|
# Look up the label ID (Forgejo needs integer IDs, not names)
|
|
LABEL_ID=$(curl -s -H "$AUTH" "$API/labels" | jq -r '[.[] | select(.name == "upstream-sync")] | .[0].id // ""')
|
|
echo "label_id=$LABEL_ID" >> "$GITHUB_OUTPUT"
|
|
echo "upstream-sync label ID: $LABEL_ID"
|
|
- name: Create or update sync issue
|
|
if: steps.compare.outputs.needs_sync == 'true'
|
|
id: sync-issue
|
|
run: |-
|
|
API="${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}"
|
|
AUTH="Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}"
|
|
LABEL_ID="${{ steps.label.outputs.label_id }}"
|
|
|
|
BEHIND="${{ steps.compare.outputs.behind }}"
|
|
AHEAD="${{ steps.compare.outputs.ahead }}"
|
|
COMMIT_LOG=$(cat <<'LOG_EOF'
|
|
${{ steps.compare.outputs.commit_log }}
|
|
LOG_EOF
|
|
)
|
|
DIFF_STAT=$(cat <<'DIFF_EOF'
|
|
${{ steps.compare.outputs.diff_stat }}
|
|
DIFF_EOF
|
|
)
|
|
|
|
BODY=$(cat <<BODY_EOF
|
|
## Upstream Sync Required
|
|
|
|
Our \`main\` branch is **${BEHIND} commits behind** upstream (\`anthropics/claude-code-action\`).
|
|
We are **${AHEAD} commits ahead** (our Forgejo-specific changes).
|
|
|
|
### New upstream commits (latest ${BEHIND}, max 30 shown)
|
|
|
|
\`\`\`
|
|
${COMMIT_LOG}
|
|
\`\`\`
|
|
|
|
### Files changed
|
|
|
|
\`\`\`
|
|
${DIFF_STAT}
|
|
\`\`\`
|
|
|
|
### How to merge
|
|
|
|
\`\`\`bash
|
|
git fetch upstream main
|
|
git merge upstream/main
|
|
# Resolve any conflicts in the 3-4 modified files (run.ts, tag/index.ts, etc.)
|
|
bun test
|
|
git push origin main
|
|
\`\`\`
|
|
|
|
### Conflict-prone files
|
|
|
|
These files have Forgejo modifications and may conflict:
|
|
- \`src/entrypoints/run.ts\`
|
|
- \`src/modes/tag/index.ts\`
|
|
- \`src/mcp/install-mcp-server.ts\`
|
|
- \`action.yml\`
|
|
|
|
_Auto-generated by upstream-sync workflow on $(date -u +%Y-%m-%d)_
|
|
BODY_EOF
|
|
)
|
|
|
|
TITLE="Upstream sync: ${BEHIND} new commits from anthropics/claude-code-action"
|
|
|
|
if [ "${{ steps.existing.outputs.issue_exists }}" = "true" ]; then
|
|
ISSUE_NUMBER="${{ steps.existing.outputs.issue_number }}"
|
|
curl -s -o /dev/null \
|
|
-X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
|
-d "$(jq -n --arg title "$TITLE" --arg body "$BODY" '{title: $title, body: $body}')" \
|
|
"$API/issues/${ISSUE_NUMBER}"
|
|
echo "Updated existing issue #${ISSUE_NUMBER}"
|
|
echo "issue_number=${ISSUE_NUMBER}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
# Forgejo expects labels as integer IDs, not objects with names
|
|
RESPONSE=$(curl -s \
|
|
-X POST -H "$AUTH" -H "Content-Type: application/json" \
|
|
-d "$(jq -n --arg title "$TITLE" --arg body "$BODY" --argjson labels "[${LABEL_ID:-0}]" '{title: $title, body: $body, labels: $labels}')" \
|
|
"$API/issues")
|
|
ISSUE_NUMBER=$(echo "$RESPONSE" | jq '.number')
|
|
if [ "$ISSUE_NUMBER" = "null" ] || [ -z "$ISSUE_NUMBER" ]; then
|
|
echo "::error::Failed to create issue. API response:"
|
|
echo "$RESPONSE" | jq '.'
|
|
else
|
|
echo "Created new upstream sync issue #${ISSUE_NUMBER}"
|
|
fi
|
|
echo "issue_number=${ISSUE_NUMBER}" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
merge-upstream:
|
|
name: Auto-merge Upstream
|
|
runs-on: grunt
|
|
needs:
|
|
- check-upstream
|
|
if: needs.check-upstream.outputs.needs_sync == 'true'
|
|
steps:
|
|
- name: Checkout
|
|
uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2
|
|
with:
|
|
fetch-depth: '0'
|
|
token: ${{ secrets.CLAUDE_BOT_TOKEN }}
|
|
- name: Check for existing sync PR
|
|
id: existing-pr
|
|
run: |-
|
|
# Search for open PRs from chore/upstream-sync-* branches
|
|
RESPONSE=$(curl -s \
|
|
-H "Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
"${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}/pulls?state=open&limit=50")
|
|
|
|
PR_BRANCH=$(echo "$RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].head.ref // ""')
|
|
|
|
if [ -n "$PR_BRANCH" ]; then
|
|
PR_NUMBER=$(echo "$RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].number')
|
|
echo "Found existing sync PR #$PR_NUMBER on branch $PR_BRANCH"
|
|
echo "pr_exists=true" >> "$GITHUB_OUTPUT"
|
|
echo "pr_branch=$PR_BRANCH" >> "$GITHUB_OUTPUT"
|
|
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "No existing sync PR found"
|
|
echo "pr_exists=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
- name: Prepare upstream merge
|
|
id: prepare
|
|
run: |-
|
|
git config user.name "claude"
|
|
git config user.email "claude@wxs.ro"
|
|
|
|
git remote add upstream https://github.com/anthropics/claude-code-action.git
|
|
git fetch upstream main --quiet
|
|
|
|
# Check if existing PR branch is already up to date with upstream
|
|
if [ "${{ steps.existing-pr.outputs.pr_exists }}" = "true" ]; then
|
|
PR_BRANCH="${{ steps.existing-pr.outputs.pr_branch }}"
|
|
git checkout "$PR_BRANCH"
|
|
|
|
# Check if upstream/main is already merged into the PR branch
|
|
BEHIND=$(git rev-list "$PR_BRANCH"..upstream/main --count)
|
|
if [ "$BEHIND" -eq 0 ]; then
|
|
echo "Existing PR branch $PR_BRANCH is already up to date with upstream"
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
echo "Existing PR branch $PR_BRANCH is $BEHIND commits behind upstream — updating"
|
|
BRANCH="$PR_BRANCH"
|
|
else
|
|
BRANCH="chore/upstream-sync-$(date +%Y%m%d)"
|
|
|
|
# Delete stale remote branch if it exists (from a previous run with no PR)
|
|
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
|
|
echo "Deleting stale remote branch $BRANCH (no PR references it)"
|
|
git push origin --delete "$BRANCH" || true
|
|
fi
|
|
|
|
git checkout -b "$BRANCH"
|
|
fi
|
|
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
|
|
# Files our fork has modified vs upstream
|
|
FORK_FILES=$(git log upstream/main..main --name-only --pretty=format: | sort -u | grep -v '^$')
|
|
echo "=== Fork-modified files ==="
|
|
echo "$FORK_FILES"
|
|
|
|
# Files upstream has changed since we last synced
|
|
UPSTREAM_FILES=$(git diff --name-only main...upstream/main | sort -u)
|
|
echo ""
|
|
echo "=== Upstream-changed files ==="
|
|
echo "$UPSTREAM_FILES"
|
|
|
|
# Intersection: fork files that upstream also changed (conflict risk)
|
|
AFFECTED=$(comm -12 <(echo "$FORK_FILES") <(echo "$UPSTREAM_FILES"))
|
|
echo ""
|
|
echo "=== Affected fork files (overlap) ==="
|
|
if [ -z "$AFFECTED" ]; then
|
|
echo "(none — low risk merge)"
|
|
else
|
|
echo "$AFFECTED"
|
|
fi
|
|
|
|
# Export all lists as multiline outputs
|
|
echo "fork_files<<EOF" >> "$GITHUB_OUTPUT"
|
|
echo "$FORK_FILES" >> "$GITHUB_OUTPUT"
|
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "affected_files<<EOF" >> "$GITHUB_OUTPUT"
|
|
echo "${AFFECTED:-(none)}" >> "$GITHUB_OUTPUT"
|
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
|
|
|
if git merge upstream/main --no-edit; then
|
|
echo "merge_clean=true" >> "$GITHUB_OUTPUT"
|
|
echo "Merge completed cleanly"
|
|
else
|
|
echo "merge_clean=false" >> "$GITHUB_OUTPUT"
|
|
echo "Merge has conflicts — Claude will resolve them"
|
|
fi
|
|
- name: Install Bun
|
|
run: |-
|
|
if command -v bun &> /dev/null; then
|
|
INSTALLED=$(bun --version)
|
|
if [ "$INSTALLED" = "$BUN_VERSION" ]; then
|
|
echo "Bun $BUN_VERSION already installed"
|
|
else
|
|
echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling"
|
|
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"
|
|
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
|
fi
|
|
else
|
|
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"
|
|
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
|
fi
|
|
if: steps.prepare.outputs.skip != 'true'
|
|
- name: Install dependencies
|
|
run: bun install
|
|
if: steps.prepare.outputs.skip != 'true'
|
|
- name: 'Claude: review merge and create PR'
|
|
if: steps.prepare.outputs.skip != 'true'
|
|
uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/actions/claude-code-action@main
|
|
with:
|
|
prompt: |-
|
|
You are automating an upstream sync for our Forgejo fork of anthropics/claude-code-action.
|
|
|
|
## Current state
|
|
- Branch: ${{ steps.prepare.outputs.branch }}
|
|
- ${{ needs.check-upstream.outputs.behind }} commits behind upstream, ${{ needs.check-upstream.outputs.ahead }} commits ahead (our Forgejo changes)
|
|
- Tracking issue: #${{ needs.check-upstream.outputs.issue_number }}
|
|
- Existing PR: ${{ steps.existing-pr.outputs.pr_exists }} (if true, PR #${{ steps.existing-pr.outputs.pr_number }} already exists — update it instead of creating a new one)
|
|
- Merge was attempted. Status: ${{ steps.prepare.outputs.merge_clean }} (true = clean, false = has conflicts)
|
|
|
|
## Impact analysis
|
|
- $FORK_MODIFIED_FILES — all files our fork has changed vs upstream
|
|
- $AFFECTED_FORK_FILES — subset of fork files that upstream ALSO changed (overlap = conflict risk)
|
|
- If AFFECTED_FORK_FILES is empty, upstream only touched files we haven't modified — low risk, merge should be clean.
|
|
- If AFFECTED_FORK_FILES is non-empty, those files need careful review since both sides changed them.
|
|
|
|
## If merge has conflicts (merge_clean=false)
|
|
Resolve all conflicts. For files in $FORK_MODIFIED_FILES, our fork's changes take PRIORITY — they contain Forgejo-specific adaptations.
|
|
|
|
## Review ALL upstream changes for fork compatibility
|
|
Read CLAUDE.md for context on our Forgejo adapter pattern and platform-specific code.
|
|
Even files we haven't modified can break our fork. Check upstream changes for:
|
|
- Renamed/moved imports or exports that our Forgejo-specific files depend on
|
|
- Changed function signatures, types, or interfaces used by our adapter code
|
|
- New assumptions (e.g. GitHub-only APIs) that don't hold on Forgejo
|
|
- Removed or restructured code that our platform branches reference
|
|
If you find incompatibilities, fix them to maintain Forgejo support.
|
|
|
|
## Maintainability principle
|
|
When fixing incompatibilities, minimize fork divergence to keep future merges easy.
|
|
Prefer adapter patterns (separate Forgejo files) over inline modifications to upstream files.
|
|
The fewer upstream files we modify, the fewer conflicts in the next sync.
|
|
|
|
## IMPORTANT: Command execution rules
|
|
- GITHUB_SERVER_URL=https://github.com is ALREADY exported in your environment. Run commands directly WITHOUT env var prefixes.
|
|
CORRECT: `bun test`
|
|
WRONG: `GITHUB_SERVER_URL=https://github.com bun test` (this will be blocked by permissions)
|
|
- Do NOT use `2>&1` redirections, subshells (`sh -c`), or `export` commands — they will be blocked.
|
|
- For inspecting env vars, use: `bun -e "console.log(process.env.VAR_NAME)"`
|
|
- Available commands: git, bun, bunx, npx, curl, echo, and anything in ./node_modules/.bin/
|
|
|
|
## Known pre-existing test failures
|
|
Some tests in comment-logic.test.ts and create-prompt.test.ts have pre-existing failures related to Forgejo URL patterns.
|
|
These are NOT caused by the upstream merge. When running `bun test`:
|
|
- If only comment-logic.test.ts and/or create-prompt.test.ts fail, that's expected — proceed.
|
|
- If OTHER test files fail that weren't failing before, investigate those as potential merge regressions.
|
|
- Do NOT spend time trying to fix pre-existing failures or trying different env var approaches.
|
|
|
|
## After merge is resolved and reviewed
|
|
1. Run tests: `bun test` — see "Known pre-existing test failures" above
|
|
2. Run typecheck: `bun run typecheck`
|
|
3. Check formatting: `bun run format:check` — if issues, fix with `bun run format`
|
|
4. Commit any remaining changes and push the branch
|
|
5. Push the branch, then create or update the PR:
|
|
- If existing PR is "true": PR #${{ steps.existing-pr.outputs.pr_number }} already exists. Just push the branch — the PR updates automatically.
|
|
Optionally update the PR body via PATCH $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls/${{ steps.existing-pr.outputs.pr_number }}
|
|
- If existing PR is "false": Create a new PR via POST $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls
|
|
Auth: token $GITHUB_TOKEN
|
|
Title: "chore: merge ${{ needs.check-upstream.outputs.behind }} upstream commits from anthropics/claude-code-action"
|
|
Body should include:
|
|
- "Closes #${{ needs.check-upstream.outputs.issue_number }}" on its own line (links PR to the tracking issue)
|
|
- Summary of notable upstream changes
|
|
- Impact assessment: which fork files (if any) were affected
|
|
- List of conflicts resolved (if any)
|
|
- If no fork files were affected, note that this is a low-risk merge
|
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}
|
|
platform: forgejo
|
|
show_full_output: 'true'
|
|
allowed_tools: Bash(git:*),Bash(bun:*),Bash(bunx:*),Bash(npx:*),Bash(./node_modules/.bin/*:*),Bash(curl:*),Bash(echo:*),Bash(cat:*),Read,Write,Edit,MultiEdit,Glob,Grep,WebFetch,WebSearch
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.CLAUDE_BOT_TOKEN }}
|
|
GITHUB_SERVER_URL: https://github.com
|
|
FORK_MODIFIED_FILES: ${{ steps.prepare.outputs.fork_files }}
|
|
AFFECTED_FORK_FILES: ${{ steps.prepare.outputs.affected_files }}
|