Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 96 additions & 38 deletions .github/workflows/claude-issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,12 +51,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' &&
Expand All @@ -82,31 +82,90 @@ 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"
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: ${{ github.token }}
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
permission=$(gh api "repos/$REPO/collaborators/$COMMENTER/permission" --jq '.permission')
case "$permission" in
admin|maintain|write)
echo "Authorized @$COMMENTER with repository permission: $permission"
;;
*)
echo "::error::Refusing mutation-capable triage from @$COMMENTER (permission: $permission)."
exit 1
;;
esac

- name: POST to routine /fire
id: fire
env:
Expand Down Expand Up @@ -141,7 +200,9 @@ 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)
# `head` intentionally closes the pipe at the cap; tolerate the
# resulting SIGPIPE from an upstream writer under `pipefail`.
body_safe=$(printf '%s' "$body" | tr -d '\000' | head -c 8192 || true)

pr_block=""
if [ "$is_pr" = "true" ]; then
Expand Down Expand Up @@ -169,7 +230,7 @@ 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=$(printf '%s' "$comment_body" | tr -d '\000' | head -c 4096 || true)
fi

nudge_note=""
Expand Down Expand Up @@ -240,9 +301,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
Expand All @@ -252,18 +310,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"
68 changes: 60 additions & 8 deletions .github/workflows/ipr-agreement.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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.

on:
issue_comment:
Expand All @@ -12,12 +16,60 @@ on:
types: [opened, synchronize, reopened]

permissions:
contents: read
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 }}
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:
- 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

- 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
run: node ${{ github.workspace }}/.ipr-code/scripts/ipr/check-and-record.mjs
31 changes: 19 additions & 12 deletions .github/workflows/sync-agent-roles.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
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:
Expand All @@ -18,24 +24,24 @@ jobs:
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: Fetch canonical .agents/roles/ from adcp
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
adcp-main/.agents/roles
rm -rf .agents/roles
mkdir -p .agents scripts
mkdir -p .agents
cp -r "$tmp/.agents/roles" .agents/roles
cp "$tmp/scripts/import-claude-agents.mjs" scripts/import-claude-agents.mjs
rm -rf "$tmp"

- name: Regenerate .claude/agents/ from synced roles
Expand Down Expand Up @@ -64,7 +70,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
Expand All @@ -74,7 +80,8 @@ jobs:
delete-branch: true
body: |
Automated weekly sync from `adcontextprotocol/adcp:.agents/roles/`
and `scripts/import-claude-agents.mjs`.
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`.

Expand Down
Loading