diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 367113123..bd0ab6ff0 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -41,7 +41,7 @@ jobs: # pull_request_target runs with base-repo secrets, so keep the # workspace on trusted base code. Argus reads the untrusted PR via # GitHub APIs (`gh pr diff/view`) and never checks out or executes it. - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: ref: ${{ github.event.pull_request.base.sha }} fetch-depth: 0 @@ -54,10 +54,12 @@ jobs: # ───────────────────────────────────────────────────────────────────── - name: Mint App token id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.IPR_APP_ID }} private-key: ${{ secrets.IPR_APP_PRIVATE_KEY }} + permission-pull-requests: write + permission-issues: write - name: Build Argus review prompt id: build-prompt @@ -86,7 +88,7 @@ jobs: - name: Run Argus PR Review id: ai-review continue-on-error: true - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@9db594c7a0e82298c121c18b7f08aa1579ce7341 # v1 with: prompt: ${{ steps.build-prompt.outputs.ARGUS_PROMPT }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} @@ -132,7 +134,7 @@ jobs: - name: Comment on PR if Argus review failed if: steps.ai-review.outcome == 'failure' || steps.verify.outputs.review_posted != 'true' - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e05af3b89..f975b4136 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,37 @@ concurrency: cancel-in-progress: true jobs: + workflow-security: + name: Workflow security + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - name: Lint workflows + uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 + with: + version: 1.7.7 + shellcheck: false + - name: Audit workflows + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + inputs: >- + .github/workflows/claude-issue-triage.yml + .github/workflows/ipr-agreement.yml + .github/workflows/sync-agent-roles.yml + .github/workflows/slash-command-dispatch.yml + .github/workflows/ai-review.yml + .github/workflows/release-please.yml + version: 1.29.0 + min-severity: high + min-confidence: high + advanced-security: false + annotations: true + test: name: Test Python ${{ matrix.python-version }} runs-on: ubuntu-latest diff --git a/.github/workflows/claude-issue-triage.yml b/.github/workflows/claude-issue-triage.yml index ddb62166e..657586e63 100644 --- a/.github/workflows/claude-issue-triage.yml +++ b/.github/workflows/claude-issue-triage.yml @@ -5,9 +5,9 @@ name: Claude Issue Triage # invokes `/triage` via comment (dispatched by # `.github/workflows/slash-command-dispatch.yml`). # -# Both issue comments and PR comments are routed to the same routine; the -# payload's `is_pr` flag and `pr` block tell the routine which context it's -# in so it can pick the right response (issue triage vs PR-feedback fix). +# Issue comments are routed to the routine. PR comments are routed only after +# the workflow verifies that their author has write, maintain, or admin access, +# because the PR-feedback mode may push a follow-up commit to the PR head branch. # # The issue/PR body is fetched fresh and passed as *data* (fenced, size- # capped) — the routine's prompt treats anything inside the fence as @@ -16,10 +16,13 @@ name: Claude Issue Triage # Required repo secrets: # CLAUDE_ROUTINE_TRIAGE_URL — full /fire URL including routine ID # CLAUDE_ROUTINE_TRIAGE_TOKEN — bearer token for that routine -# TRIAGE_DISPATCH_PAT — PAT for reaction on the /triage-triggering -# comment (same secret as slash-command-dispatch.yml) +# TRIAGE_DISPATCH_PAT — PAT for /triage dispatch authorization, +# permission lookup, and trigger-comment reaction +# (same secret as slash-command-dispatch.yml) # # Token last rotated: 2026-04-23 — rotate every 90 days. +# Action refs are pinned to immutable SHAs; verify each SHA against its +# upstream release tag and run actionlint/zizmor when updating a pin. on: issues: @@ -51,12 +54,12 @@ jobs: # by slash-command-dispatch path); # skip `no-triage` issues (same reason # as above). - # Both issue and PR comments fire — the - # routine receives `is_pr` to branch on. - # - repository_dispatch → always allow (slash-dispatch already - # gated by member-association check; - # manual /triage intentionally overrides - # the `no-triage` label). + # Both issue and PR comments fire; PR + # commenters are permission-checked below. + # - repository_dispatch → start the job, then independently verify + # the commenter's current repository write + # permission below. Manual /triage + # intentionally overrides `no-triage`. if: >- ( github.event_name == 'issues' && @@ -82,30 +85,92 @@ jobs: steps: - name: Resolve issue number + event kind id: ctx + env: + # Event and client-payload fields are untrusted. Passing them via env + # keeps expression expansion out of the shell program itself. + EVENT_NAME: ${{ github.event_name }} + EVENT_ACTION: ${{ github.event.action || '' }} + SOURCE_NUMBER: ${{ github.event.issue.number || github.event.client_payload.github.payload.issue.number || '' }} + SOURCE_COMMENTER: ${{ github.event.comment.user.login || github.event.client_payload.github.payload.comment.user.login || '' }} + SOURCE_COMMENT_ID: ${{ github.event.comment.id || github.event.client_payload.github.payload.comment.id || '' }} + DISPATCH_ARGS: ${{ github.event.client_payload.slash_command.args.all || '' }} run: | set -euo pipefail - if [ "${{ github.event_name }}" = "issues" ]; then - echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" - echo "kind=auto" >> "$GITHUB_OUTPUT" - echo "action=${{ github.event.action }}" >> "$GITHUB_OUTPUT" - echo "commenter=" >> "$GITHUB_OUTPUT" - echo "args=" >> "$GITHUB_OUTPUT" - echo "comment_id=" >> "$GITHUB_OUTPUT" - elif [ "${{ github.event_name }}" = "issue_comment" ]; then - echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" - echo "kind=comment" >> "$GITHUB_OUTPUT" - echo "action=created" >> "$GITHUB_OUTPUT" - echo "commenter=${{ github.event.comment.user.login }}" >> "$GITHUB_OUTPUT" - echo "args=" >> "$GITHUB_OUTPUT" - echo "comment_id=${{ github.event.comment.id }}" >> "$GITHUB_OUTPUT" + export LC_ALL=C + if [ "$EVENT_NAME" = "issues" ]; then + number=$SOURCE_NUMBER + kind=auto + action=$EVENT_ACTION + commenter='' + args='' + comment_id='' + elif [ "$EVENT_NAME" = "issue_comment" ]; then + number=$SOURCE_NUMBER + kind=comment + action=created + commenter=$SOURCE_COMMENTER + args='' + comment_id=$SOURCE_COMMENT_ID else - echo "number=${{ github.event.client_payload.github.payload.issue.number }}" >> "$GITHUB_OUTPUT" - echo "kind=manual" >> "$GITHUB_OUTPUT" - echo "action=triage" >> "$GITHUB_OUTPUT" - echo "commenter=${{ github.event.client_payload.github.payload.comment.user.login }}" >> "$GITHUB_OUTPUT" - echo "args=${{ github.event.client_payload.slash_command.args.all }}" >> "$GITHUB_OUTPUT" - echo "comment_id=${{ github.event.client_payload.github.payload.comment.id }}" >> "$GITHUB_OUTPUT" + number=$SOURCE_NUMBER + kind=manual + action=triage + commenter=$SOURCE_COMMENTER + args=$DISPATCH_ARGS + comment_id=$SOURCE_COMMENT_ID + fi + + if [[ ! "$number" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid issue number in event payload." + exit 1 + fi + if [[ -n "$commenter" && ! "$commenter" =~ ^[A-Za-z0-9-]{1,39}$ ]]; then + echo "::error::Invalid commenter login in event payload." + exit 1 fi + if [[ -n "$comment_id" && ! "$comment_id" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid comment ID in event payload." + exit 1 + fi + + delimiter="args-$(openssl rand -hex 16)" + { + echo "number=$number" + echo "kind=$kind" + echo "action=$action" + echo "commenter=$commenter" + echo "comment_id=$comment_id" + echo "args<<$delimiter" + printf '%s\n' "$args" + echo "$delimiter" + } >> "$GITHUB_OUTPUT" + + - name: Authorize mutation-capable trigger + if: >- + github.event_name == 'repository_dispatch' || + (github.event_name == 'issue_comment' && github.event.issue.pull_request) + env: + GH_TOKEN: ${{ secrets.TRIAGE_DISPATCH_PAT }} + REPO: ${{ github.repository }} + COMMENTER: ${{ steps.ctx.outputs.commenter }} + run: | + set -euo pipefail + if [ -z "$COMMENTER" ]; then + echo "::error::Cannot authorize an empty commenter." + exit 1 + fi + error_file=$(mktemp) + if ! push=$(gh api "repos/$REPO/collaborators/$COMMENTER/permission" \ + --jq '.user.permissions.push' 2>"$error_file"); then + echo "::error::Permission lookup for @$COMMENTER failed (token scope or API error)." + sed 's/^/ /' "$error_file" + exit 1 + fi + if [ "$push" != "true" ]; then + echo "::error::Refusing mutation-capable triage from @$COMMENTER (no push access)." + exit 1 + fi + echo "Authorized @$COMMENTER with repository push permission." - name: POST to routine /fire id: fire @@ -122,6 +187,7 @@ jobs: COMMENT_ID: ${{ steps.ctx.outputs.comment_id }} run: | set -euo pipefail + export LC_ALL=C if [ -z "${ROUTINE_URL:-}" ] || [ -z "${ROUTINE_TOKEN:-}" ]; then echo "::warning::CLAUDE_ROUTINE_TRIAGE_URL or _TOKEN not set — skipping." @@ -141,7 +207,7 @@ jobs: # routine can branch on context (head/base ref, draft status, etc.). is_pr=$(echo "$issue" | jq -r 'if .pull_request then "true" else "false" end') - body_safe=$(printf '%s' "$body" | tr -d '\000' | head -c 8192) + body_safe=${body:0:8192} pr_block="" if [ "$is_pr" = "true" ]; then @@ -169,12 +235,12 @@ jobs: comment_body=$(echo "$comment" | jq -r '.body // ""') comment_author=$(echo "$comment" | jq -r '.user.login') comment_assoc=$(echo "$comment" | jq -r '.author_association // "NONE"') - comment_body_safe=$(printf '%s' "$comment_body" | tr -d '\000' | head -c 4096) + comment_body_safe=${comment_body:0:4096} fi nudge_note="" if [ "$EVENT_KIND" = "manual" ]; then - nudge_note="MANUAL NUDGE: @${COMMENTER} requested triage via /triage ${ARGS}. Treat as an explicit request; skip already-engaged check. Honor any modifier (execute / clarify / defer) in the args." + nudge_note="MANUAL NUDGE: @${COMMENTER} requested triage via /triage. Treat as an explicit request; skip already-engaged check." fi payload=$(jq -n \ @@ -189,6 +255,7 @@ jobs: --argjson labels "$labels" \ --arg body "$body_safe" \ --arg nudge "$nudge_note" \ + --arg triage_args "${ARGS:0:512}" \ --arg comment_body "$comment_body_safe" \ --arg comment_author "$comment_author" \ --arg comment_assoc "$comment_assoc" \ @@ -210,6 +277,10 @@ jobs: "is_pr: false\n" end) + (if $nudge == "" then "" else $nudge + "\n" end) + + (if $triage_args == "" then "" else + "<<>>\n" + + $triage_args + "\n<<>>\n" + end) + (if $comment_body == "" then "" else "\nNew comment by @" + $comment_author + " (association: " + $comment_assoc + "):\n" + @@ -240,9 +311,6 @@ jobs: fi echo "HTTP $http_code" - sed 's/[Bb]earer [A-Za-z0-9._-]*/Bearer [REDACTED]/g' /tmp/fire-response.json - echo - if [ "${http_code:-000}" -ge 400 ]; then echo "::error::Failed to fire routine (HTTP $http_code) for issue #${ISSUE_NUMBER}" exit 1 @@ -252,18 +320,18 @@ jobs: - name: React +1 on manual-nudge comment (success) if: steps.ctx.outputs.kind == 'manual' && success() && steps.ctx.outputs.comment_id != '' - uses: peter-evans/create-or-update-comment@v5 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: token: ${{ secrets.TRIAGE_DISPATCH_PAT }} - repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + repository: ${{ github.repository }} comment-id: ${{ steps.ctx.outputs.comment_id }} reactions: "+1" - name: React -1 on manual-nudge comment (failure) if: steps.ctx.outputs.kind == 'manual' && failure() && steps.ctx.outputs.comment_id != '' - uses: peter-evans/create-or-update-comment@v5 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: token: ${{ secrets.TRIAGE_DISPATCH_PAT }} - repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + repository: ${{ github.repository }} comment-id: ${{ steps.ctx.outputs.comment_id }} reactions: "-1" diff --git a/.github/workflows/ipr-agreement.yml b/.github/workflows/ipr-agreement.yml index e65bca72b..f997b034d 100644 --- a/.github/workflows/ipr-agreement.yml +++ b/.github/workflows/ipr-agreement.yml @@ -1,9 +1,18 @@ name: IPR Agreement -# Delegates to the reusable workflow in adcontextprotocol/adcp. Signatures -# land in the central ledger at adcontextprotocol/adcp:signatures/ipr-signatures.json. -# See https://github.com/adcontextprotocol/adcp/blob/main/governance/ipr-bot-setup.md -# for the GitHub App configuration and how to rotate / revoke credentials. +# Checks signatures against the central ledger in adcontextprotocol/adcp. +# Executable code is checked out at an immutable reviewed commit, separately +# from the mutable main-branch ledger that receives signature records. +# +# Action refs are pinned to immutable SHAs. Update them in a dedicated PR by +# verifying the SHA against the upstream release tag, reviewing release notes, +# and running actionlint. Update the pinned IPR code commit only after reviewing +# the callable implementation and scripts at that adcontextprotocol/adcp commit. +# +# Required repo secrets: +# IPR_APP_ID — AAO IPR Bot GitHub App ID +# IPR_APP_PRIVATE_KEY — GitHub App private key PEM +# Rotation/setup: governance/ipr-bot-setup.md on: issue_comment: @@ -11,13 +20,68 @@ on: pull_request_target: types: [opened, synchronize, reopened] -permissions: - pull-requests: write - statuses: write +concurrency: + group: adcp-ipr-signature-write + cancel-in-progress: false jobs: check: - uses: adcontextprotocol/adcp/.github/workflows/ipr-check-callable.yml@main - secrets: - IPR_APP_ID: ${{ secrets.IPR_APP_ID }} - IPR_APP_PRIVATE_KEY: ${{ secrets.IPR_APP_PRIVATE_KEY }} + permissions: + contents: read + pull-requests: write + statuses: write + runs-on: ubuntu-latest + timeout-minutes: 5 + if: >- + github.event_name == 'pull_request_target' || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request != null && + contains(github.event.comment.body, 'I have read the IPR Policy')) + steps: + # SECURITY: do not add a checkout of the caller repo to this job. The + # App token + caller's GITHUB_TOKEN both live in this job's env; a step + # that runs PR-head code (npm scripts, builds, or any caller workspace + # executable) would expose them to attacker-controlled code. + - name: Mint AAO IPR Bot installation token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.IPR_APP_ID }} + private-key: ${{ secrets.IPR_APP_PRIVATE_KEY }} + owner: adcontextprotocol + repositories: adcp + permission-contents: write + + - name: Checkout reviewed IPR executable code + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + repository: adcontextprotocol/adcp + ref: 82a671607c92945f0fec513c4375af583fdea914 + token: ${{ github.token }} + path: .ipr-code + fetch-depth: 1 + persist-credentials: false + + - name: Checkout mutable central IPR ledger + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + repository: adcontextprotocol/adcp + ref: main + token: ${{ steps.app-token.outputs.token }} + path: .ipr-ledger + fetch-depth: 1 + persist-credentials: true + + - name: Setup Node + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: '22' + + - name: Check and record IPR signature + env: + GITHUB_TOKEN: ${{ github.token }} + LEDGER_DIR: ${{ github.workspace }}/.ipr-ledger + CODE_DIR: ${{ github.workspace }}/.ipr-code + run: | + set -euo pipefail + node "$CODE_DIR/scripts/ipr/check-and-record.mjs" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 98cffca5a..8eadcb623 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -11,12 +11,11 @@ on: branches: - main -permissions: - contents: write - pull-requests: write - jobs: release-please: + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest steps: # Release Please must write release PR branches with an installation token, @@ -25,12 +24,14 @@ jobs: # blocked waiting for CI/IPR contexts that never start. - name: Mint App token id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.IPR_APP_ID }} private-key: ${{ secrets.IPR_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write - - uses: googleapis/release-please-action@v5 + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5 id: release if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish != true }} with: @@ -40,17 +41,19 @@ jobs: - name: Checkout release PR if: ${{ steps.release.outputs.prs_created == 'true' }} - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} - name: Normalize release PR pyproject prerelease version if: ${{ steps.release.outputs.prs_created == 'true' }} + env: + REF_NAME: ${{ github.ref_name }} run: | set -euo pipefail - BASE_RELEASE_PR_BRANCH="release-please--branches--${{ github.ref_name }}" + BASE_RELEASE_PR_BRANCH="release-please--branches--${REF_NAME}" COMPONENT_RELEASE_PR_BRANCH="${BASE_RELEASE_PR_BRANCH}--components--adcp" if git ls-remote --exit-code --heads origin "${COMPONENT_RELEASE_PR_BRANCH}" >/dev/null 2>&1; then @@ -81,11 +84,11 @@ jobs: # Publish to PyPI when a release is created - name: Checkout if: ${{ steps.release.outputs.release_created == 'true' || (github.event_name == 'workflow_dispatch' && inputs.publish == true) }} - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Setup Python if: ${{ steps.release.outputs.release_created == 'true' || (github.event_name == 'workflow_dispatch' && inputs.publish == true) }} - uses: actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: '3.10' diff --git a/.github/workflows/slash-command-dispatch.yml b/.github/workflows/slash-command-dispatch.yml index ad715b5fe..e51270f4f 100644 --- a/.github/workflows/slash-command-dispatch.yml +++ b/.github/workflows/slash-command-dispatch.yml @@ -35,7 +35,7 @@ jobs: !endsWith(github.event.comment.user.login, '[bot]') steps: - name: Slash command dispatch - uses: peter-evans/slash-command-dispatch@v5 + uses: peter-evans/slash-command-dispatch@9bdcd7914ec1b75590b790b844aa3b8eee7c683a # v5.0.2 with: token: ${{ secrets.TRIAGE_DISPATCH_PAT }} commands: | diff --git a/.github/workflows/sync-agent-roles.yml b/.github/workflows/sync-agent-roles.yml index 72cc2e6e5..09bd4326b 100644 --- a/.github/workflows/sync-agent-roles.yml +++ b/.github/workflows/sync-agent-roles.yml @@ -1,42 +1,55 @@ name: Sync agent roles from adcp -# Mirrors `.agents/roles/` + `scripts/import-claude-agents.mjs` from -# the canonical source in adcontextprotocol/adcp. Opens a PR if drift -# is detected. Runs weekly on Mondays and on-demand. +# Mirrors `.agents/roles/` from the canonical source in +# adcontextprotocol/adcp. The importer remains the reviewed local copy: code +# fetched from a mutable upstream branch must never execute with this +# workflow's write token. Opens a PR if drift is detected. Runs weekly on +# Mondays and on-demand. +# +# Action refs are pinned to immutable SHAs. Update them in a dedicated PR by +# verifying each SHA against its upstream release tag, reviewing release notes, +# and running actionlint before merge. on: schedule: - cron: '17 6 * * 1' # Mondays, 06:17 UTC (off-peak) workflow_dispatch: -permissions: - contents: write - pull-requests: write - jobs: sync: + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false - - uses: actions/setup-node@v6 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: '22' - - name: Fetch canonical .agents/roles/ and sync script from adcp + - name: Checkout canonical role data from adcp + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + repository: adcontextprotocol/adcp + ref: main + path: .adcp-upstream + fetch-depth: 1 + persist-credentials: false + + - name: Import canonical .agents/roles/ from adcp + id: upstream run: | set -euo pipefail - tmp=$(mktemp -d) - curl -fsSL https://github.com/adcontextprotocol/adcp/archive/refs/heads/main.tar.gz \ - | tar -xz -C "$tmp" --strip-components=1 \ - adcp-main/.agents/roles \ - adcp-main/scripts/import-claude-agents.mjs rm -rf .agents/roles - mkdir -p .agents scripts - cp -r "$tmp/.agents/roles" .agents/roles - cp "$tmp/scripts/import-claude-agents.mjs" scripts/import-claude-agents.mjs - rm -rf "$tmp" + mkdir -p .agents + cp -r .adcp-upstream/.agents/roles .agents/roles + upstream_sha=$(git -C .adcp-upstream rev-parse HEAD) + echo "sha=$upstream_sha" >> "$GITHUB_OUTPUT" + rm -rf .adcp-upstream - name: Regenerate .claude/agents/ from synced roles run: node scripts/import-claude-agents.mjs @@ -64,7 +77,7 @@ jobs: Sync `.agents/roles/` from canonical source in `adcontextprotocol/adcp`. EOF - - uses: peter-evans/create-pull-request@v8 + - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 if: steps.diff.outputs.changed == 'true' with: branch: claude-routine/sync-agent-roles @@ -73,8 +86,11 @@ jobs: commit-message: 'chore(agents): sync .agents/roles/ from adcp' delete-branch: true body: | - Automated weekly sync from `adcontextprotocol/adcp:.agents/roles/` - and `scripts/import-claude-agents.mjs`. + Automated weekly sync from `adcontextprotocol/adcp:.agents/roles/`. + Upstream commit: `${{ steps.upstream.outputs.sha }}` + + The generated `.claude/agents/` files use this repository's + reviewed local `scripts/import-claude-agents.mjs` importer. Run by `.github/workflows/sync-agent-roles.yml`. diff --git a/.github/workflows/triage-webhook-miss-sweep.yml b/.github/workflows/triage-webhook-miss-sweep.yml index 97e205371..dde2afbeb 100644 --- a/.github/workflows/triage-webhook-miss-sweep.yml +++ b/.github/workflows/triage-webhook-miss-sweep.yml @@ -36,6 +36,7 @@ jobs: ROUTINE_TOKEN: ${{ secrets.CLAUDE_ROUTINE_TRIAGE_TOKEN }} run: | set -euo pipefail + export LC_ALL=C if [ -z "${ROUTINE_URL:-}" ] || [ -z "${ROUTINE_TOKEN:-}" ]; then echo "::warning::CLAUDE_ROUTINE_TRIAGE_URL or _TOKEN not set — skipping." @@ -99,7 +100,7 @@ jobs: labels=$(echo "$issue" | jq -c '[.labels[].name]') html_url=$(echo "$issue" | jq -r '.html_url') - body_safe=$(printf '%s' "$body" | tr -d '\000' | head -c 8192) + body_safe=${body:0:8192} payload=$(jq -n \ --arg repo "$REPO" \