## Development lane / 开发通道 - Lane: `direct-to-main` bootstrap - Base branch: `main` - Active train: `n/a` - Owning Agent/session: workflow coordinator ## Change / 改动 - Split authoritative CI into parallel static, contract, Rust lint/test shards, Swift interaction, stress, overlay, bundle, and stable `Required CI` jobs. - Make every ready PR to `main` a complete gate while keeping task PRs to `gd-ops/train/*` path-scoped. - Add direct/train worktree coordination, bounded changelog fragments, protected main/train ruleset automation, and safe same-repository squash auto-merge. - Let GitHub Release promote an exact successful main commit, reuse its source proof, and create a missing release tag only after proof validation. - Keep local pre-push validation bounded to basic syntax, formatting, contracts, and compilation; complete duplicate proof stays in GitHub CI. This is the one bootstrap PR: the auto-merge workflow and required rules do not exist on the base branch yet. After this PR passes and is merged once, the exact `main` push must pass `Required CI`; then the fail-closed applicator can activate both rulesets and all later ready `gd-ops/*` PRs follow the automated path. ## Validation / 验证 - `actionlint .github/workflows/ci.yml .github/workflows/release.yml .github/workflows/auto-merge.yml` - `./script/validate_source_syntax.sh` - `./script/validate_build_scripts_safety.sh --skip-source-syntax` - Exact staged diff was reapplied to a clean detached worktree before the final validation run. - 28 validation-tooling tests, 40 release-distribution tests, macOS build-contract tests, Release shell contracts, and test-isolation validation passed. ## Risk / 风险 - No product runtime or credential access is added. - `pull_request_target` never checks out or executes PR code and only enables auto-merge after API verification of active PR and `Required CI` rules. - Full ruleset activation fails closed until the exact remote main commit has a trusted successful GitHub Actions push run. - Reversible repository settings are already enabled: auto-merge, squash-only, update branch, and delete merged branches. --------- Co-authored-by: gd-ops <gd-ops@xmfunny.com>
581 lines
25 KiB
YAML
581 lines
25 KiB
YAML
name: GitHub Release for macOS
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- "v*.*.*"
|
||
workflow_dispatch:
|
||
inputs:
|
||
tag:
|
||
description: vX.Y.Z tag to create or reuse
|
||
required: true
|
||
type: string
|
||
build:
|
||
description: Positive CFBundleVersion
|
||
required: true
|
||
default: "1"
|
||
type: string
|
||
commit:
|
||
description: Successful main commit to promote (blank uses current main)
|
||
required: false
|
||
type: string
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
concurrency:
|
||
group: github-release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
||
cancel-in-progress: false
|
||
|
||
jobs:
|
||
prepare:
|
||
name: Reuse exact main source proof
|
||
runs-on: macos-26
|
||
timeout-minutes: 20
|
||
permissions:
|
||
actions: read
|
||
contents: read
|
||
outputs:
|
||
tag: ${{ steps.release_identity.outputs.tag }}
|
||
version: ${{ steps.release_identity.outputs.version }}
|
||
build: ${{ steps.release_identity.outputs.build }}
|
||
commit: ${{ steps.release_identity.outputs.commit }}
|
||
previous_tag: ${{ steps.release_identity.outputs.previous_tag }}
|
||
tag_exists: ${{ steps.release_identity.outputs.tag_exists }}
|
||
env:
|
||
APC_VALIDATE_HOST_UI: "0"
|
||
APC_VALIDATE_REAL_AGENT_CONNECTORS: "0"
|
||
APC_VALIDATE_REAL_APP_SERVER: "0"
|
||
GH_TOKEN: ${{ github.token }}
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
ref: ${{ github.event_name == 'workflow_dispatch' && (inputs.commit || 'main') || github.ref }}
|
||
|
||
- name: Resolve release identity
|
||
id: release_identity
|
||
env:
|
||
EVENT_NAME: ${{ github.event_name }}
|
||
DISPATCH_TAG: ${{ inputs.tag }}
|
||
DISPATCH_BUILD: ${{ inputs.build }}
|
||
PUSH_TAG: ${{ github.ref_name }}
|
||
PUSH_BUILD: ${{ github.run_number }}
|
||
run: |
|
||
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
|
||
tag="$DISPATCH_TAG"
|
||
build="$DISPATCH_BUILD"
|
||
else
|
||
tag="$PUSH_TAG"
|
||
build="$PUSH_BUILD"
|
||
fi
|
||
[[ "$tag" =~ ^v[0-9]+[.][0-9]+[.][0-9]+$ ]]
|
||
[[ "$build" =~ ^[1-9][0-9]*$ ]]
|
||
version="${tag#v}"
|
||
commit="$(git rev-parse HEAD)"
|
||
git rev-parse --verify refs/remotes/origin/main >/dev/null
|
||
git merge-base --is-ancestor "$commit" refs/remotes/origin/main
|
||
tag_exists=0
|
||
if git rev-parse --verify "refs/tags/$tag^{commit}" >/dev/null 2>&1; then
|
||
test "$(git rev-list -n 1 "$tag")" = "$commit"
|
||
tag_exists=1
|
||
elif [[ "$EVENT_NAME" != "workflow_dispatch" ]]; then
|
||
echo "tag push did not resolve to a local tag" >&2
|
||
exit 1
|
||
fi
|
||
source_version="$(awk -F'"' '/^version = / {print $2; exit}' crates/petcore/Cargo.toml)"
|
||
test "$version" = "$source_version"
|
||
grep -q "^## \\[$version\\] - [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$" CHANGELOG.md
|
||
previous_tag="$(
|
||
gh api \
|
||
-H 'Accept: application/vnd.github+json' \
|
||
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
||
"repos/$GITHUB_REPOSITORY/releases/latest" \
|
||
--jq '.tag_name'
|
||
)"
|
||
[[ "$previous_tag" =~ ^v[0-9]+[.][0-9]+[.][0-9]+$ ]]
|
||
test "$previous_tag" != "$tag"
|
||
previous_commit="$(git rev-parse --verify "$previous_tag^{commit}")"
|
||
git merge-base --is-ancestor "$previous_commit" "$commit^"
|
||
./script/validate_codex_plugin_version.py --base-ref "$previous_tag"
|
||
{
|
||
echo "tag=$tag"
|
||
echo "version=$version"
|
||
echo "build=$build"
|
||
echo "commit=$commit"
|
||
echo "previous_tag=$previous_tag"
|
||
echo "tag_exists=$tag_exists"
|
||
} >>"$GITHUB_OUTPUT"
|
||
|
||
- name: Prepare source-proof verification host
|
||
run: |
|
||
test "$(uname -m)" = "arm64"
|
||
./script/validate_macos_build_contract.py toolchain
|
||
|
||
- name: Resolve successful trusted main CI run
|
||
id: source_proof_run
|
||
run: |
|
||
runs_json="$RUNNER_TEMP/apc-ci-runs.json"
|
||
gh api \
|
||
-H 'Accept: application/vnd.github+json' \
|
||
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
||
"repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?branch=main&event=push&status=success&head_sha=${{ steps.release_identity.outputs.commit }}&per_page=100" \
|
||
>"$runs_json"
|
||
selection="$(
|
||
./script/resolve_release_source_proof.py run \
|
||
--runs-json "$runs_json" \
|
||
--repository "$GITHUB_REPOSITORY" \
|
||
--commit "${{ steps.release_identity.outputs.commit }}"
|
||
)"
|
||
run_id="$(jq -r '.run_id' <<<"$selection")"
|
||
run_attempt="$(jq -r '.run_attempt' <<<"$selection")"
|
||
[[ "$run_id" =~ ^[1-9][0-9]*$ ]]
|
||
[[ "$run_attempt" =~ ^[1-9][0-9]*$ ]]
|
||
|
||
artifacts_json="$RUNNER_TEMP/apc-ci-artifacts.json"
|
||
gh api \
|
||
-H 'Accept: application/vnd.github+json' \
|
||
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
||
"repos/$GITHUB_REPOSITORY/actions/runs/$run_id/artifacts?per_page=100" \
|
||
>"$artifacts_json"
|
||
artifact_name="release-source-proof-${{ steps.release_identity.outputs.commit }}"
|
||
./script/resolve_release_source_proof.py artifact \
|
||
--artifacts-json "$artifacts_json" \
|
||
--run-id "$run_id" \
|
||
--artifact-name "$artifact_name" >/dev/null
|
||
{
|
||
echo "run_id=$run_id"
|
||
echo "run_attempt=$run_attempt"
|
||
echo "artifact_name=$artifact_name"
|
||
} >>"$GITHUB_OUTPUT"
|
||
|
||
- name: Download exact-commit source proof from trusted CI run
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||
with:
|
||
name: ${{ steps.source_proof_run.outputs.artifact_name }}
|
||
path: ${{ runner.temp }}/source-proof
|
||
github-token: ${{ github.token }}
|
||
repository: ${{ github.repository }}
|
||
run-id: ${{ steps.source_proof_run.outputs.run_id }}
|
||
|
||
- name: Verify exact-commit source proof and rebind interaction attestation
|
||
env:
|
||
APC_BUILD_ID: ${{ steps.release_identity.outputs.version }}.${{ steps.release_identity.outputs.build }}.${{ steps.release_identity.outputs.commit }}
|
||
run: |
|
||
./script/release_source_proof.py validate \
|
||
--repository "$GITHUB_REPOSITORY" \
|
||
--commit "${{ steps.release_identity.outputs.commit }}" \
|
||
--previous-release-tag "${{ steps.release_identity.outputs.previous_tag }}" \
|
||
--run-id "${{ steps.source_proof_run.outputs.run_id }}" \
|
||
--run-attempt "${{ steps.source_proof_run.outputs.run_attempt }}" \
|
||
--proof "$RUNNER_TEMP/source-proof/source-proof.json" \
|
||
--attestation "$RUNNER_TEMP/source-proof/interaction-attestation.json"
|
||
./script/prepare_interaction_attestation.sh \
|
||
--output "$RUNNER_TEMP/release-interaction-attestation.json" \
|
||
--proof-in "$RUNNER_TEMP/source-proof/interaction-attestation.json"
|
||
|
||
- name: Upload release-bound interaction proof
|
||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||
with:
|
||
name: release-interaction-attestation
|
||
if-no-files-found: error
|
||
retention-days: 3
|
||
path: ${{ runner.temp }}/release-interaction-attestation.json
|
||
|
||
ensure_tag:
|
||
name: Bind release tag to proven main commit
|
||
needs: prepare
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 10
|
||
permissions:
|
||
contents: write
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
||
RELEASE_COMMIT: ${{ needs.prepare.outputs.commit }}
|
||
TAG_EXISTS: ${{ needs.prepare.outputs.tag_exists }}
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 1
|
||
persist-credentials: false
|
||
ref: ${{ needs.prepare.outputs.commit }}
|
||
|
||
- name: Create missing lightweight tag after source proof succeeds
|
||
if: env.TAG_EXISTS != '1'
|
||
run: |
|
||
gh api --method POST \
|
||
-H 'Accept: application/vnd.github+json' \
|
||
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
||
"repos/$GITHUB_REPOSITORY/git/refs" \
|
||
-f "ref=refs/tags/$RELEASE_TAG" \
|
||
-f "sha=$RELEASE_COMMIT" >/dev/null
|
||
|
||
- name: Verify immutable release identity
|
||
run: ./script/verify_remote_release_tag.sh --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT"
|
||
|
||
build_archives:
|
||
name: Build ${{ matrix.architecture }} archive
|
||
needs: [prepare, ensure_tag]
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
architecture: [arm64, x86_64]
|
||
runs-on: macos-26
|
||
timeout-minutes: 60
|
||
permissions:
|
||
contents: read
|
||
env:
|
||
APC_VALIDATE_HOST_UI: "0"
|
||
APC_VALIDATE_REAL_AGENT_CONNECTORS: "0"
|
||
APC_VALIDATE_REAL_APP_SERVER: "0"
|
||
CARGO_TERM_COLOR: always
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
ref: ${{ needs.prepare.outputs.commit }}
|
||
|
||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||
with:
|
||
name: release-interaction-attestation
|
||
path: ${{ runner.temp }}/release-proof
|
||
|
||
- name: Prepare architecture build host
|
||
run: |
|
||
test "$(uname -m)" = "arm64"
|
||
rustup show
|
||
rustup target add "${{ matrix.architecture == 'arm64' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}"
|
||
./script/validate_macos_build_contract.py toolchain
|
||
if ! command -v rg >/dev/null 2>&1; then
|
||
brew install ripgrep
|
||
fi
|
||
command -v codesign
|
||
command -v ditto
|
||
command -v lipo
|
||
command -v shasum
|
||
|
||
- name: Prepare pinned Python validation environment
|
||
run: |
|
||
python3 -m venv "$RUNNER_TEMP/apc-validation-python"
|
||
"$RUNNER_TEMP/apc-validation-python/bin/python" -m pip install \
|
||
--disable-pip-version-check --no-deps --only-binary=:all: "Pillow==11.3.0"
|
||
echo "$RUNNER_TEMP/apc-validation-python/bin" >>"$GITHUB_PATH"
|
||
|
||
- name: Restore source/build cache
|
||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||
with:
|
||
path: |
|
||
~/.cargo/registry
|
||
~/.cargo/git
|
||
target
|
||
apps/macos/.build
|
||
key: macos-release-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'apps/macos/Package.swift', 'apps/macos/Package.resolved') }}-${{ needs.prepare.outputs.commit }}
|
||
restore-keys: |
|
||
macos-release-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'apps/macos/Package.swift', 'apps/macos/Package.resolved') }}-
|
||
|
||
- name: Build one source-proven official component
|
||
env:
|
||
APC_RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||
APC_RELEASE_BUILD: ${{ needs.prepare.outputs.build }}
|
||
APC_PREVIOUS_RELEASE_TAG: ${{ needs.prepare.outputs.previous_tag }}
|
||
APC_INTERACTION_ATTESTATION_PATH: ${{ runner.temp }}/release-proof/release-interaction-attestation.json
|
||
run: ./script/build_release.sh --github-release --source-gate-proven --arch "${{ matrix.architecture }}"
|
||
|
||
- name: Upload architecture component
|
||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||
with:
|
||
name: release-component-${{ matrix.architecture }}
|
||
if-no-files-found: error
|
||
retention-days: 3
|
||
path: dist/AgentPetCompanion-${{ needs.prepare.outputs.version }}-macos-${{ matrix.architecture }}.zip
|
||
|
||
assemble:
|
||
name: Assemble exact release candidate
|
||
needs: [prepare, build_archives]
|
||
runs-on: macos-15
|
||
timeout-minutes: 15
|
||
permissions:
|
||
contents: read
|
||
outputs:
|
||
arm64_zip_sha256: ${{ steps.candidate_digests.outputs.arm64_zip_sha256 }}
|
||
x86_64_zip_sha256: ${{ steps.candidate_digests.outputs.x86_64_zip_sha256 }}
|
||
checksum_sha256: ${{ steps.candidate_digests.outputs.checksum_sha256 }}
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 1
|
||
persist-credentials: false
|
||
ref: ${{ needs.prepare.outputs.commit }}
|
||
|
||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||
with:
|
||
pattern: release-component-*
|
||
path: release-assets
|
||
merge-multiple: true
|
||
|
||
- name: Create and validate exact three-file candidate
|
||
env:
|
||
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||
run: |
|
||
(
|
||
cd release-assets
|
||
shasum -a 256 \
|
||
"AgentPetCompanion-$RELEASE_VERSION-macos-arm64.zip" \
|
||
"AgentPetCompanion-$RELEASE_VERSION-macos-x86_64.zip" \
|
||
>"AgentPetCompanion-$RELEASE_VERSION-SHA256SUMS.txt"
|
||
)
|
||
./script/validate_release_artifact_metadata.py \
|
||
--directory release-assets \
|
||
--version "$RELEASE_VERSION"
|
||
|
||
- name: Emit trusted digest for every candidate file
|
||
id: candidate_digests
|
||
env:
|
||
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||
run: |
|
||
digest() { shasum -a 256 "$1" | awk '{print $1}'; }
|
||
{
|
||
echo "arm64_zip_sha256=$(digest "release-assets/AgentPetCompanion-$RELEASE_VERSION-macos-arm64.zip")"
|
||
echo "x86_64_zip_sha256=$(digest "release-assets/AgentPetCompanion-$RELEASE_VERSION-macos-x86_64.zip")"
|
||
echo "checksum_sha256=$(digest "release-assets/AgentPetCompanion-$RELEASE_VERSION-SHA256SUMS.txt")"
|
||
} >>"$GITHUB_OUTPUT"
|
||
|
||
- name: Upload exact release candidate
|
||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||
with:
|
||
name: AgentPetCompanion-${{ needs.prepare.outputs.version }}-github-release
|
||
if-no-files-found: error
|
||
retention-days: 14
|
||
path: release-assets
|
||
|
||
validate_arm64:
|
||
name: Validate packaged arm64 App on macOS 15 compatibility host
|
||
needs: [prepare, assemble]
|
||
runs-on: macos-15
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: read
|
||
steps:
|
||
- name: Prove native validation architecture
|
||
run: test "$(uname -m)" = "arm64"
|
||
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 1
|
||
persist-credentials: false
|
||
ref: ${{ needs.prepare.outputs.commit }}
|
||
|
||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||
with:
|
||
name: AgentPetCompanion-${{ needs.prepare.outputs.version }}-github-release
|
||
path: release-assets
|
||
|
||
- name: Compare trusted digests and run only native arm64 packaged validation
|
||
run: |
|
||
./script/verify_release_candidate_digests.sh \
|
||
--directory release-assets \
|
||
--version "${{ needs.prepare.outputs.version }}" \
|
||
--arm64-zip-sha256 "${{ needs.assemble.outputs.arm64_zip_sha256 }}" \
|
||
--x86_64-zip-sha256 "${{ needs.assemble.outputs.x86_64_zip_sha256 }}" \
|
||
--checksum-sha256 "${{ needs.assemble.outputs.checksum_sha256 }}"
|
||
./script/validate_github_release_artifacts.sh \
|
||
--directory release-assets \
|
||
--version "${{ needs.prepare.outputs.version }}" \
|
||
--build "${{ needs.prepare.outputs.build }}" \
|
||
--commit "${{ needs.prepare.outputs.commit }}" \
|
||
--require-native-architecture arm64
|
||
|
||
validate_x86_64:
|
||
name: Validate packaged x86_64 App on macOS 15 Intel compatibility host
|
||
needs: [prepare, assemble]
|
||
runs-on: macos-15-intel
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: read
|
||
steps:
|
||
- name: Prove native validation architecture
|
||
run: test "$(uname -m)" = "x86_64"
|
||
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 1
|
||
persist-credentials: false
|
||
ref: ${{ needs.prepare.outputs.commit }}
|
||
|
||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||
with:
|
||
name: AgentPetCompanion-${{ needs.prepare.outputs.version }}-github-release
|
||
path: release-assets
|
||
|
||
- name: Compare trusted digests and run only native x86_64 packaged validation
|
||
run: |
|
||
./script/verify_release_candidate_digests.sh \
|
||
--directory release-assets \
|
||
--version "${{ needs.prepare.outputs.version }}" \
|
||
--arm64-zip-sha256 "${{ needs.assemble.outputs.arm64_zip_sha256 }}" \
|
||
--x86_64-zip-sha256 "${{ needs.assemble.outputs.x86_64_zip_sha256 }}" \
|
||
--checksum-sha256 "${{ needs.assemble.outputs.checksum_sha256 }}"
|
||
./script/validate_github_release_artifacts.sh \
|
||
--directory release-assets \
|
||
--version "${{ needs.prepare.outputs.version }}" \
|
||
--build "${{ needs.prepare.outputs.build }}" \
|
||
--commit "${{ needs.prepare.outputs.commit }}" \
|
||
--require-native-architecture x86_64
|
||
|
||
validate_macos26:
|
||
name: Validate packaged Liquid Glass path on macOS 26
|
||
needs: [prepare, assemble]
|
||
runs-on: macos-26
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: read
|
||
steps:
|
||
- name: Prove native macOS 26 validation host
|
||
run: |
|
||
test "$(uname -m)" = "arm64"
|
||
test "$(sw_vers -productVersion | cut -d. -f1)" -ge 26
|
||
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 1
|
||
persist-credentials: false
|
||
ref: ${{ needs.prepare.outputs.commit }}
|
||
|
||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||
with:
|
||
name: AgentPetCompanion-${{ needs.prepare.outputs.version }}-github-release
|
||
path: release-assets
|
||
|
||
- name: Compare trusted digests and run packaged macOS 26 validation
|
||
run: |
|
||
./script/verify_release_candidate_digests.sh \
|
||
--directory release-assets \
|
||
--version "${{ needs.prepare.outputs.version }}" \
|
||
--arm64-zip-sha256 "${{ needs.assemble.outputs.arm64_zip_sha256 }}" \
|
||
--x86_64-zip-sha256 "${{ needs.assemble.outputs.x86_64_zip_sha256 }}" \
|
||
--checksum-sha256 "${{ needs.assemble.outputs.checksum_sha256 }}"
|
||
./script/validate_github_release_artifacts.sh \
|
||
--directory release-assets \
|
||
--version "${{ needs.prepare.outputs.version }}" \
|
||
--build "${{ needs.prepare.outputs.build }}" \
|
||
--commit "${{ needs.prepare.outputs.commit }}" \
|
||
--require-native-architecture arm64
|
||
|
||
publish:
|
||
name: Verify and publish GitHub Release
|
||
needs: [prepare, assemble, validate_arm64, validate_x86_64, validate_macos26]
|
||
runs-on: macos-15
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: write
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
||
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||
RELEASE_BUILD: ${{ needs.prepare.outputs.build }}
|
||
RELEASE_COMMIT: ${{ needs.prepare.outputs.commit }}
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 1
|
||
persist-credentials: false
|
||
ref: ${{ needs.prepare.outputs.commit }}
|
||
|
||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||
with:
|
||
name: AgentPetCompanion-${{ needs.prepare.outputs.version }}-github-release
|
||
path: release-assets
|
||
|
||
- name: Compare exact inventory and trusted build digests
|
||
run: |
|
||
./script/verify_release_candidate_digests.sh \
|
||
--directory release-assets \
|
||
--version "$RELEASE_VERSION" \
|
||
--arm64-zip-sha256 "${{ needs.assemble.outputs.arm64_zip_sha256 }}" \
|
||
--x86_64-zip-sha256 "${{ needs.assemble.outputs.x86_64_zip_sha256 }}" \
|
||
--checksum-sha256 "${{ needs.assemble.outputs.checksum_sha256 }}"
|
||
|
||
- name: Prove remote release tag still targets the candidate
|
||
run: ./script/verify_remote_release_tag.sh --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT"
|
||
|
||
- name: Create draft, verify downloaded bytes, and publish latest stable Release
|
||
run: |
|
||
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
|
||
echo "GitHub Release already exists for $RELEASE_TAG; refusing replacement" >&2
|
||
exit 1
|
||
fi
|
||
|
||
notes_file="$RUNNER_TEMP/apc-release-notes.md"
|
||
{
|
||
printf '# Agent Pet Companion %s\n\n' "$RELEASE_VERSION"
|
||
printf '## Update in three steps / 三步更新\n\n'
|
||
printf '1. Download and unzip the archive for this Mac.\n'
|
||
printf '2. Quit Agent Pet Companion, move the new App to Applications, and choose Replace.\n'
|
||
printf '3. Open the new App from Applications.\n\n'
|
||
printf '1. 下载并解压适用于这台 Mac 的归档。\n'
|
||
printf '2. 退出 Agent Pet Companion,将新版移入“应用程序”,并选择“替换”。\n'
|
||
printf '3. 从“应用程序”打开新版。\n\n'
|
||
printf 'Your pets, settings, history, and active work stay on this Mac and are preserved.\n\n'
|
||
printf '你的宠物、设置、历史和正在进行的工作会留在这台 Mac 上并保持不变。\n\n'
|
||
# shellcheck disable=SC2016
|
||
printf 'Apple silicon uses `macos-arm64`; Intel uses `macos-x86_64`. Download `SHA256SUMS.txt` with the ZIP and verify the selected archive before replacing the App.\n\n'
|
||
# shellcheck disable=SC2016
|
||
printf 'Apple 芯片使用 `macos-arm64`,Intel 使用 `macos-x86_64`。请同时下载 `SHA256SUMS.txt`,并在替换 App 前核对所选归档。\n\n'
|
||
printf '**First launch:** these Apps are ad-hoc signed, are not Developer ID signed, and are not Apple-notarized. If macOS blocks the App, Control-click it in Finder and choose **Open**, or use **System Settings → Privacy & Security → Open Anyway**. Only continue if you downloaded it from this GitHub Release and its SHA-256 matches the published checksum.\n\n'
|
||
printf '**首次打开:** 当前 App 使用 ad-hoc 签名,没有 Developer ID 签名,也未经过 Apple 公证。如果 macOS 阻止打开,请在 Finder 中按住 Control 点按 App 并选择“打开”,或前往“系统设置 → 隐私与安全性 → 仍要打开”。仅在文件来自本 GitHub Release 且 SHA-256 与校验和一致时继续。\n'
|
||
} >"$notes_file"
|
||
|
||
assets=(
|
||
"release-assets/AgentPetCompanion-$RELEASE_VERSION-macos-arm64.zip"
|
||
"release-assets/AgentPetCompanion-$RELEASE_VERSION-macos-x86_64.zip"
|
||
"release-assets/AgentPetCompanion-$RELEASE_VERSION-SHA256SUMS.txt"
|
||
)
|
||
gh release create "$RELEASE_TAG" "${assets[@]}" \
|
||
--verify-tag --draft \
|
||
--title "Agent Pet Companion $RELEASE_VERSION" \
|
||
--notes-file "$notes_file"
|
||
|
||
release_created=1
|
||
release_published=0
|
||
cleanup_draft() {
|
||
if [[ "$release_created" == "1" && "$release_published" == "0" ]]; then
|
||
gh release delete "$RELEASE_TAG" --yes >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
trap cleanup_draft EXIT
|
||
|
||
download_dir="$RUNNER_TEMP/apc-release-download"
|
||
rm -rf "$download_dir"
|
||
mkdir -p "$download_dir"
|
||
gh release download "$RELEASE_TAG" --dir "$download_dir"
|
||
./script/verify_release_candidate_digests.sh \
|
||
--directory "$download_dir" \
|
||
--version "$RELEASE_VERSION" \
|
||
--arm64-zip-sha256 "${{ needs.assemble.outputs.arm64_zip_sha256 }}" \
|
||
--x86_64-zip-sha256 "${{ needs.assemble.outputs.x86_64_zip_sha256 }}" \
|
||
--checksum-sha256 "${{ needs.assemble.outputs.checksum_sha256 }}"
|
||
|
||
./script/verify_remote_release_tag.sh --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT"
|
||
gh release edit "$RELEASE_TAG" --draft=false --latest
|
||
release_published=1
|
||
./script/verify_remote_release_tag.sh --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT"
|
||
|
||
published_json="$RUNNER_TEMP/apc-published-release.json"
|
||
latest_json="$RUNNER_TEMP/apc-latest-release.json"
|
||
gh api -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2026-03-10' \
|
||
"repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" >"$published_json"
|
||
gh api -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2026-03-10' \
|
||
"repos/$GITHUB_REPOSITORY/releases/latest" >"$latest_json"
|
||
./script/validate_github_release_api.py \
|
||
--release-json "$published_json" \
|
||
--latest-json "$latest_json" \
|
||
--repository "$GITHUB_REPOSITORY" \
|
||
--version "$RELEASE_VERSION" \
|
||
--arm64-zip-sha256 "${{ needs.assemble.outputs.arm64_zip_sha256 }}" \
|
||
--x86-64-zip-sha256 "${{ needs.assemble.outputs.x86_64_zip_sha256 }}" \
|
||
--checksum-sha256 "${{ needs.assemble.outputs.checksum_sha256 }}"
|
||
release_created=0
|