Files
agent-pet-companion/.github/workflows/release.yml
T
2026-08-10 18:41:19 +08:00

470 lines
21 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: GitHub Release for macOS
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
tag:
description: Existing vX.Y.Z tag to publish
required: true
type: string
build:
description: Positive CFBundleVersion
required: true
default: "1"
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: Prove source once
runs-on: macos-15
timeout-minutes: 75
permissions:
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 }}
env:
APC_VALIDATE_HOST_UI: "0"
APC_VALIDATE_REAL_AGENT_CONNECTORS: "0"
APC_VALIDATE_REAL_APP_SERVER: "0"
CARGO_TERM_COLOR: always
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || 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)"
test "$(git rev-list -n 1 "$tag")" = "$commit"
git rev-parse --verify refs/remotes/origin/main >/dev/null
git merge-base --is-ancestor "$commit" refs/remotes/origin/main
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"
} >>"$GITHUB_OUTPUT"
- name: Prepare source validation host
run: |
test "$(uname -m)" = "arm64"
swift --version
test "$(swift --version | awk '/Apple Swift version/{split($4, version, "."); print version[1]}')" -ge 6
rustup show
if ! command -v rg >/dev/null 2>&1; then
brew install ripgrep
fi
- 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"
"$RUNNER_TEMP/apc-validation-python/bin/python" - <<'PY'
from PIL import Image, features
if Image.__version__ != "11.3.0" or not features.check("webp_anim"):
raise SystemExit("pinned Pillow lacks the required animated WebP support")
PY
echo "$RUNNER_TEMP/apc-validation-python/bin" >>"$GITHUB_PATH"
- name: Restore source/build cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
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') }}-${{ steps.release_identity.outputs.commit }}
restore-keys: |
macos-release-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'apps/macos/Package.swift', 'apps/macos/Package.resolved') }}-
- name: Run source, complete Swift interaction, integration, and stress gates
env:
APC_BUILD_ID: ${{ steps.release_identity.outputs.version }}.${{ steps.release_identity.outputs.build }}.${{ steps.release_identity.outputs.commit }}
APC_INTERACTION_ATTESTATION_OUTPUT: ${{ runner.temp }}/release-interaction-attestation.json
run: |
defaults write NSGlobalDomain AppleLanguages -array "zh-Hans" "en"
test "$(defaults read NSGlobalDomain AppleLanguages | sed -n '2s/[ ,\"]//gp')" = "zh-Hans"
./script/test_all.sh --source-only --include-stress
- name: Upload source-bound interaction proof
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-interaction-attestation
if-no-files-found: error
retention-days: 3
path: ${{ runner.temp }}/release-interaction-attestation.json
build_archives:
name: Build ${{ matrix.architecture }} archive
needs: prepare
strategy:
fail-fast: false
matrix:
architecture: [arm64, x86_64]
runs-on: macos-15
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@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ needs.prepare.outputs.commit }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-interaction-attestation
path: ${{ runner.temp }}/release-proof
- name: Prepare architecture build host
run: |
rustup show
rustup target add "${{ matrix.architecture == 'arm64' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}"
swift --version
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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
ref: ${{ needs.prepare.outputs.commit }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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 native Apple silicon
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@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
ref: ${{ needs.prepare.outputs.commit }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
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 native Intel
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@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
ref: ${{ needs.prepare.outputs.commit }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
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
publish:
name: Verify and publish GitHub Release
needs: [prepare, assemble, validate_arm64, validate_x86_64]
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@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
ref: ${{ needs.prepare.outputs.commit }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
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'
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'
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