源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
name: Publish Claude Code Binaries
|
||||
|
||||
# Publishes zstd-compressed Claude Code native binaries to the app-runtime
|
||||
# CDN bucket. The desktop bundle no longer vendors the ~230MB-per-platform
|
||||
# @anthropic-ai/claude-agent-sdk-<platform> packages; tuttid downloads them at
|
||||
# runtime from <cdn>/claude-code/<claudeVersion>/claude-<platform>.zst (see
|
||||
# services/tuttid/service/agentstatus/claude_binary.go). Runs automatically
|
||||
# whenever the sidecar bumps its pinned SDK version; uploads are immutable and
|
||||
# skipped when the version is already published, so re-runs are safe.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- packages/agent/claude-sdk-sidecar/package.json
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
aws_region:
|
||||
description: AWS region. Defaults to runtime vars, then release vars.
|
||||
required: false
|
||||
type: string
|
||||
aws_role_arn:
|
||||
description: AWS role ARN. Defaults to runtime vars, then release vars.
|
||||
required: false
|
||||
type: string
|
||||
s3_bucket:
|
||||
description: S3 bucket. Defaults to runtime vars, then release vars.
|
||||
required: false
|
||||
type: string
|
||||
s3_prefix:
|
||||
description: S3 prefix. Defaults to claude-code.
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Stage and publish binaries
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AWS_REGION_VALUE: ${{ inputs.aws_region || vars.TUTTI_APP_RUNTIME_AWS_REGION || vars.TUTTI_APP_RELEASES_AWS_REGION }}
|
||||
AWS_ROLE_ARN_VALUE: ${{ inputs.aws_role_arn || vars.TUTTI_APP_RUNTIME_AWS_ROLE_ARN || vars.TUTTI_APP_RELEASES_AWS_ROLE_ARN }}
|
||||
S3_BUCKET_VALUE: ${{ inputs.s3_bucket || vars.TUTTI_APP_RUNTIME_S3_BUCKET || vars.TUTTI_APP_RELEASES_S3_BUCKET }}
|
||||
S3_PREFIX_VALUE: ${{ inputs.s3_prefix || 'claude-code' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Validate AWS configuration
|
||||
shell: bash
|
||||
run: |
|
||||
missing=()
|
||||
for name in AWS_REGION_VALUE AWS_ROLE_ARN_VALUE S3_BUCKET_VALUE; do
|
||||
if [ -z "${!name:-}" ]; then
|
||||
missing+=("${name}")
|
||||
fi
|
||||
done
|
||||
if [ "${#missing[@]}" -gt 0 ]; then
|
||||
printf 'Missing required AWS configuration: %s\n' "${missing[*]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Stage claude binaries
|
||||
id: stage
|
||||
shell: bash
|
||||
run: |
|
||||
node tools/scripts/stage-claude-code-binaries.mjs \
|
||||
--out "${GITHUB_WORKSPACE}/dist/claude-code" | tee stage-summary.json
|
||||
claude_version="$(node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('stage-summary.json','utf8')).claudeVersion)")"
|
||||
# The version becomes an S3 key segment; reject anything that is not
|
||||
# a plain semver so a malformed manifest cannot steer paths.
|
||||
if ! [[ "${claude_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "staged claude version is not a plain semver: ${claude_version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "claude_version=${claude_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-region: ${{ env.AWS_REGION_VALUE }}
|
||||
role-to-assume: ${{ env.AWS_ROLE_ARN_VALUE }}
|
||||
|
||||
- name: Upload binaries to S3
|
||||
shell: bash
|
||||
env:
|
||||
CLAUDE_VERSION: ${{ steps.stage.outputs.claude_version }}
|
||||
run: |
|
||||
prefix="${S3_PREFIX_VALUE%/}"
|
||||
version_dir="dist/claude-code/${CLAUDE_VERSION}"
|
||||
uploaded=0
|
||||
skipped=0
|
||||
while IFS= read -r artifact; do
|
||||
relative_path="${artifact#dist/claude-code/}"
|
||||
key="${prefix}/${relative_path}"
|
||||
local_size="$(wc -c < "${artifact}" | tr -d ' ')"
|
||||
local_sha256="$(sha256sum "${artifact}" | awk '{print $1}')"
|
||||
content_type="application/zstd"
|
||||
case "${artifact}" in
|
||||
*.json) content_type="application/json" ;;
|
||||
esac
|
||||
# Keys are immutable. A conditional create (If-None-Match: *) has
|
||||
# S3 itself enforce that: the object is written only when the key
|
||||
# does not exist yet, atomically even across racing publishers. It
|
||||
# also avoids probing missing keys with head-object, which reports
|
||||
# 403 instead of 404 when the role lacks s3:ListBucket and would
|
||||
# abort the job on a key that simply is not published yet.
|
||||
outcome=""
|
||||
for attempt in 1 2 3; do
|
||||
if aws s3api put-object \
|
||||
--bucket "${S3_BUCKET_VALUE}" \
|
||||
--key "${key}" \
|
||||
--body "${artifact}" \
|
||||
--if-none-match '*' \
|
||||
--content-type "${content_type}" \
|
||||
--cache-control "public, max-age=31536000, immutable" \
|
||||
--metadata "claude-sha256=${local_sha256}" \
|
||||
>/dev/null 2>put-error.txt; then
|
||||
outcome="uploaded"
|
||||
break
|
||||
fi
|
||||
if grep -qiE 'PreconditionFailed' put-error.txt; then
|
||||
outcome="exists"
|
||||
break
|
||||
fi
|
||||
# 409 ConditionalRequestConflict: a concurrent publisher is
|
||||
# writing this key right now. Wait for its create to land, then
|
||||
# re-probe — the retry turns into a plain 412 once it exists.
|
||||
if grep -qiE 'ConditionalRequestConflict' put-error.txt && [ "${attempt}" -lt 3 ]; then
|
||||
sleep 15
|
||||
continue
|
||||
fi
|
||||
echo "put-object failed for s3://${S3_BUCKET_VALUE}/${key}:" >&2
|
||||
cat put-error.txt >&2
|
||||
exit 1
|
||||
done
|
||||
if [ "${outcome}" = "uploaded" ]; then
|
||||
echo "uploaded: s3://${S3_BUCKET_VALUE}/${key}"
|
||||
uploaded=$((uploaded + 1))
|
||||
continue
|
||||
fi
|
||||
# The key already exists: only treat it as published when it holds
|
||||
# the staged bytes — a mismatch means a corrupted or foreign
|
||||
# publish and must stop the job. head-object is reliable here
|
||||
# regardless of s3:ListBucket because the object exists.
|
||||
if ! head_json="$(aws s3api head-object --bucket "${S3_BUCKET_VALUE}" --key "${key}" \
|
||||
--output json 2>head-error.txt)"; then
|
||||
echo "existing object could not be verified for s3://${S3_BUCKET_VALUE}/${key} (does the role have s3:GetObject on this prefix?):" >&2
|
||||
cat head-error.txt >&2
|
||||
exit 1
|
||||
fi
|
||||
remote_size="$(node -e "process.stdout.write(String(JSON.parse(process.argv[1]).ContentLength))" "${head_json}")"
|
||||
remote_sha256="$(node -e "process.stdout.write(JSON.parse(process.argv[1]).Metadata?.['claude-sha256'] ?? '')" "${head_json}")"
|
||||
# The size check is unconditional: the sha256 metadata is
|
||||
# caller-supplied at upload time, not derived from the stored
|
||||
# body, so matching metadata alone cannot vouch for the bytes.
|
||||
if [ "${remote_size}" != "${local_size}" ]; then
|
||||
echo "immutable key already exists with different content: s3://${S3_BUCKET_VALUE}/${key} (remote ${remote_size} bytes, staged ${local_size} bytes)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "${remote_sha256}" ] && [ "${remote_sha256}" != "${local_sha256}" ]; then
|
||||
echo "immutable key already exists with different content: s3://${S3_BUCKET_VALUE}/${key} (remote sha256 ${remote_sha256}, staged ${local_sha256})" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "exists with matching content, skipping: s3://${S3_BUCKET_VALUE}/${key}"
|
||||
skipped=$((skipped + 1))
|
||||
done < <(find "${version_dir}" -type f | sort)
|
||||
echo "uploaded=${uploaded} skipped=${skipped} version=${CLAUDE_VERSION}"
|
||||
Reference in New Issue
Block a user