Skip to content
Merged
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
28 changes: 13 additions & 15 deletions .github/actions/beta-notes/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,33 +81,31 @@ runs:
APP: ${{ inputs.app }}
BASE_REF: ${{ inputs.base }}
TAG_PATTERN: ${{ inputs.tag-pattern }}
RESULT: ${{ runner.temp }}/beta-notes-result.json
run: '"$GITHUB_ACTION_PATH/plan.sh"'
- name: Write notes with Claude
id: claude
if: steps.plan.outputs.run == 'true'
continue-on-error: true
uses: anthropics/claude-code-action/base-action@ae68417a745a5df4b2124873632bd73a2255e6f6 # v1.0.259
shell: bash
env:
PROMPT: ${{ steps.plan.outputs.prompt }}
RESULT: ${{ runner.temp }}/beta-notes-result.json
MODEL: ${{ inputs.model }}
MAX_TURNS: ${{ inputs.max-turns }}
GH_TOKEN: ${{ inputs.token }}
with:
prompt_file: ${{ steps.plan.outputs.prompt }}
claude_args: >-
--model ${{ inputs.model }}
--max-turns ${{ inputs.max-turns }}
--allowedTools Read Grep Glob "Bash(git log:*)" "Bash(git show:*)" "Bash(git diff:*)" "Bash(gh pr view:*)"
--json-schema '{"type":"object","properties":{"bullets":{"type":"array","maxItems":8,"items":{"type":"string","maxLength":160}}},"required":["bullets"],"additionalProperties":false}'
anthropic_federation_rule_id: ${{ inputs.anthropic-federation-rule-id }}
anthropic_organization_id: ${{ inputs.anthropic-organization-id }}
anthropic_service_account_id: ${{ inputs.anthropic-service-account-id }}
anthropic_workspace_id: ${{ inputs.anthropic-workspace-id }}
anthropic_api_key: ${{ inputs.anthropic-api-key }}
ANTHROPIC_FEDERATION_RULE_ID: ${{ inputs.anthropic-federation-rule-id }}
ANTHROPIC_ORGANIZATION_ID: ${{ inputs.anthropic-organization-id }}
ANTHROPIC_SERVICE_ACCOUNT_ID: ${{ inputs.anthropic-service-account-id }}
ANTHROPIC_WORKSPACE_ID: ${{ inputs.anthropic-workspace-id }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }}
run: '"$GITHUB_ACTION_PATH/claude.sh"'
- name: Fit the notes to the store
id: finish
shell: bash
env:
NOTES: ${{ inputs.notes }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
STRUCTURED: ${{ steps.claude.outputs.structured_output }}
RESULT: ${{ runner.temp }}/beta-notes-result.json
MAX_LENGTH: ${{ inputs.max-length }}
FALLBACK: ${{ inputs.fallback }}
run: '"$GITHUB_ACTION_PATH/finish.sh"'
76 changes: 76 additions & 0 deletions .github/actions/beta-notes/claude.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# Runs Claude Code headless on the prompt and writes its JSON result to RESULT.
# claude-code-action is avoided: its Agent SDK launch is rejected as
# "Credit balance is too low" (anthropics/claude-code-action#1901).
set -euo pipefail

# The binary is pinned by sha256 per platform; manifest.json under the
# release lists them.
version=2.1.309
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) platform=linux-x64 sha=4d704a8aecbe038affa3000f498ae17c1b4f753add8facb8764120affc99b658 ;;
Linux-aarch64 | Linux-arm64) platform=linux-arm64 sha=9796dd26899337f687739a0b39a7353b639f062a3436a7b941fcf19a1b757a9b ;;
Darwin-arm64) platform=darwin-arm64 sha=5eadc0009cdcdbc0d06e58110d7b2baf88ced6088b1c33a60a21217fe410619c ;;
Darwin-x86_64) platform=darwin-x64 sha=200e0d775ae55c040db3bb8db8de3e22efcddbd7e0abff5ab50dd10f6853425d ;;
*) echo "beta-notes: no Claude Code build for $(uname -s)-$(uname -m)" >&2; exit 1 ;;
esac
claude="$RUNNER_TEMP/beta-notes-claude-$version"
curl -fsSL --retry 3 "https://downloads.claude.ai/claude-code-releases/$version/$platform/claude" -o "$claude"
if command -v sha256sum > /dev/null; then
actual="$(sha256sum "$claude" | cut -d' ' -f1)"
else
actual="$(shasum -a 256 "$claude" | cut -d' ' -f1)"
fi
if [ "$actual" != "$sha" ]; then
echo "beta-notes: Claude Code $version $platform checksum mismatch" >&2
exit 1
fi
chmod +x "$claude"

refresh=""
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
unset ANTHROPIC_FEDERATION_RULE_ID ANTHROPIC_ORGANIZATION_ID ANTHROPIC_SERVICE_ACCOUNT_ID ANTHROPIC_WORKSPACE_ID
else
unset ANTHROPIC_API_KEY
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "beta-notes: federation needs id-token: write on the job" >&2
exit 1
fi
# Claude never sees the OIDC request credentials; only this shell can mint
# GitHub tokens.
request_url="$ACTIONS_ID_TOKEN_REQUEST_URL"
request_token="$ACTIONS_ID_TOKEN_REQUEST_TOKEN"
unset ACTIONS_ID_TOKEN_REQUEST_URL ACTIONS_ID_TOKEN_REQUEST_TOKEN

# Fresh per invocation, so no cached token from another rule carries over.
dir="$RUNNER_TEMP/beta-notes-federation"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Give each federation invocation its own cache

When a job invokes beta-notes more than once with different federation rule, service-account, or workspace inputs, every invocation reuses this directory and the default profile, so the second run can consume the still-valid credentials/default.json written by the first instead of exchanging its new identity token. Anthropic documents that this file stores the profile's cached access token and that a federated token is scoped to a workspace when exchanged (WIF reference); consequently, the second summary can run under the first invocation's identity or fail authorization. Use an invocation-specific config directory or clear the cached credential before launching Claude.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: each invocation removes the federation directory before writing its token and profile.

rm -rf "$dir"
(umask 077 && mkdir -p "$dir/configs")
export ANTHROPIC_IDENTITY_TOKEN_FILE="$dir/identity-token"
write_identity_token() {
curl -fsS -H "Authorization: Bearer $request_token" \
"$request_url&audience=https://api.anthropic.com" \
| jq -er .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE.next"
mv "$ANTHROPIC_IDENTITY_TOKEN_FILE.next" "$ANTHROPIC_IDENTITY_TOKEN_FILE"
}
write_identity_token
# GitHub's token lives five minutes and each one exchanges once, so a long
# run needs a fresh one before the CLI refreshes its Anthropic token.
(while sleep 240; do write_identity_token || true; done) &
refresh=$!
# A federation profile turns on the CLI's credential cache. Without it the
# CLI exchanges the single-use GitHub token more than once and the second
# exchange fails as jti_reused.
echo '{"version":"1.0","authentication":{"type":"oidc_federation"}}' > "$dir/configs/default.json"
export ANTHROPIC_CONFIG_DIR="$dir" ANTHROPIC_PROFILE=default
fi

status=0
"$claude" -p "$(cat "$PROMPT")" \
--model "$MODEL" \
--max-turns "$MAX_TURNS" \
--allowedTools Read Grep Glob "Bash(git log:*)" "Bash(git show:*)" "Bash(git diff:*)" "Bash(gh pr view:*)" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Strip OIDC request credentials before launching Claude

In federation mode, the claude subprocess inherits ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN, allowing any Claude-executed tool or project hook influenced by the repository or summarized PR content to mint fresh GitHub OIDC tokens for arbitrary audiences. The replaced base action explicitly removed these variables before starting the model process (pinned source); unset them after fetching the Anthropic identity token and before invoking Claude.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the OIDC request variables move into unexported shell variables and are unset before Claude starts.

--json-schema "$(cat "$GITHUB_ACTION_PATH/schema.json")" \
--output-format json < /dev/null > "$RESULT" || status=$?
[ -z "$refresh" ] || kill "$refresh" 2> /dev/null || true
exit "$status"
7 changes: 5 additions & 2 deletions .github/actions/beta-notes/finish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ source=fallback
if [ -n "$NOTES" ]; then
notes="$NOTES"
source=manual
elif [ "$CONCLUSION" = success ] && [ -n "$STRUCTURED" ]; then
elif [ -s "$RESULT" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use an invocation-specific result file

When this composite action is invoked more than once in the same job, every invocation uses the same $RUNNER_TEMP/beta-notes-result.json. If a later invocation skips the Claude step because its range has no qualifying commits, or exits before the final redirected command, this condition consumes the earlier invocation's still-populated file and emits stale notes with source=claude instead of the promised fallback. Clear the result before planning or give each invocation a unique result path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: plan.sh clears the result file on every invocation.

if jq -e '.is_error' "$RESULT" > /dev/null; then
echo "beta-notes: Claude failed: $(jq -r '.result // .subtype' "$RESULT" | head -c 300)" >&2
fi
# One line per bullet; drop any that carry a link or a line break.
bullets="$(jq -r '.bullets[]? | select(type == "string") | select(test("[\r\n]|://") | not) | sub("^[-*• ]+"; "") | sub("[. ]+$"; "") | select(length > 0) | "- " + .' <<< "$STRUCTURED")" || bullets=""
bullets="$(jq -r 'select(.is_error | not) | .structured_output.bullets[]? | select(type == "string") | select(test("[\r\n]|://") | not) | sub("^[-*• ]+"; "") | sub("[. ]+$"; "") | select(length > 0) | "- " + .' "$RESULT")" || bullets=""
while IFS= read -r line; do
[ -n "$line" ] || continue
candidate="${notes:+$notes$'\n'}$line"
Expand Down
3 changes: 3 additions & 0 deletions .github/actions/beta-notes/plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# needs to run. Writes base, head, and run to GITHUB_OUTPUT.
set -euo pipefail

# A result left by an earlier invocation in this job must not count.
rm -f "$RESULT"

if [ -n "$NOTES" ]; then
echo "run=false" >> "$GITHUB_OUTPUT"
exit 0
Expand Down
1 change: 1 addition & 0 deletions .github/actions/beta-notes/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"object","properties":{"bullets":{"type":"array","maxItems":8,"items":{"type":"string","maxLength":160}}},"required":["bullets"],"additionalProperties":false}
3 changes: 0 additions & 3 deletions .github/workflows/federation-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ jobs:
with:
app: put.io shared GitHub actions
base: v2.0.0
- name: Print Claude's result message
if: always()
run: jq -c '.[] | select(.type == "result" or .type == "assistant") | del(.message.usage)' "$RUNNER_TEMP/claude-execution-output.json" | cut -c1-1500
- name: Require notes from Claude
env:
SOURCE: ${{ steps.notes.outputs.source }}
Expand Down