Repository navigation
fix(actions): run Claude Code directly in beta-notes #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3e96c7b
1a20923
7d71b48
8b49a96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| 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:*)" \ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In federation mode, the Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,9 +8,12 @@ source=fallback | |
| if [ -n "$NOTES" ]; then | ||
| notes="$NOTES" | ||
| source=manual | ||
| elif [ "$CONCLUSION" = success ] && [ -n "$STRUCTURED" ]; then | ||
| elif [ -s "$RESULT" ]; then | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this composite action is invoked more than once in the same job, every invocation uses the same Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
|
||
| 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} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a job invokes
beta-notesmore than once with different federation rule, service-account, or workspace inputs, every invocation reuses this directory and thedefaultprofile, so the second run can consume the still-validcredentials/default.jsonwritten 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 👍 / 👎.
There was a problem hiding this comment.
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.