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
42 changes: 42 additions & 0 deletions .github/release-inputs/v1.21.14.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"schema_version": 1,
"version": "1.21.14",
"release_tag": "v1.21.14",
"source_sha": "6161df6e550ead762da186b6beda36f0299eb4d7",
"staging_asset": "terraphim-clients-1.21.14-release-inputs.tar.gz",
"staging_sha256": "69457dde3e588f192a12b989db76705bb5c48a49a636d5506305a566565a7e41",
"builder": {
"build_checkout_sha": "f4afcdae653e476a1f6336b804323a080e2f313e",
"product_source_delta_from_tag": "none; post-tag commits changed release workflow and tests only",
"cargo_lock_sha256": "d3d0965ac068e1cc7a645f72edbd46c7a42bc932aa84942842223f85268324d1",
"apple_rust_toolchain": "rustc 1.98.1",
"windows_rust_toolchain": "rustc 1.98.1",
"cargo_xwin": "0.23.1",
"windows_sdk": "17",
"cross": "0.2.5 (65fe72b 2026-04-23)",
"cross_rust_toolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
"x86_64_unknown_linux_gnu_image": "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main@sha256:e3f7d4ee29f4198c22f84a8d05ab52ec209e7900bd394888b40ea81ca364ec6c",
"x86_64_unknown_linux_musl_image": "ghcr.io/cross-rs/x86_64-unknown-linux-musl:main@sha256:d54fdde7f1b680901a0bb21a2952e4921172b94c17e48603ccbbaeca8b5ef7e8",
"aarch64_unknown_linux_musl_image": "ghcr.io/cross-rs/aarch64-unknown-linux-musl:main@sha256:10304ec1a8b013544193a403a98b4547e959af1fbc22d1dad88e9ce2b3a9dde0"
},
"binaries": [
{"name": "terraphim-agent-aarch64-apple-darwin", "sha256": "441e3d0794d03b5fb14de714148e62d194b6da6aefe033acad2ca3434a8e3693"},
{"name": "terraphim-agent-aarch64-unknown-linux-musl", "sha256": "4894123f0e2c969ab5275806ab02fbc4078cc6231669c1cb810f2ce45f74dd96"},
{"name": "terraphim-agent-x86_64-apple-darwin", "sha256": "e9a958d0a8e342106575a90e239afcbf85466debe9691e958e4b8b8e87999f5c"},
{"name": "terraphim-agent-x86_64-pc-windows-msvc.exe", "sha256": "e121dacd978d781fb690b6b7c8acaf6b290f51ceaca4b77e0934b476098167f2"},
{"name": "terraphim-agent-x86_64-unknown-linux-gnu", "sha256": "4fa4a989fc8ce5be30ee5d83c73a8faa8752408a5e5875498772bad7ce402c17"},
{"name": "terraphim-agent-x86_64-unknown-linux-musl", "sha256": "142135e7be634c774b32f9197ed746868447531352a080b1f9ee7992869a2aab"},
{"name": "terraphim-cli-aarch64-apple-darwin", "sha256": "6089cd4ebb626ef00a62a4d49371134a99723de0fb784804c55e586d6dd8fde7"},
{"name": "terraphim-cli-aarch64-unknown-linux-musl", "sha256": "402aeaf1217008715ab7042221e29e65d4b069373bd56fe879622c9f3eab0565"},
{"name": "terraphim-cli-x86_64-apple-darwin", "sha256": "e16626f43cf6620c2d5acdb341c3c0d10b959952a832128535582940afa02c5c"},
{"name": "terraphim-cli-x86_64-pc-windows-msvc.exe", "sha256": "b284c3281df3ac41db2bbc9d9d96625ea0ba6e382d29e5208b708080fa8c5354"},
{"name": "terraphim-cli-x86_64-unknown-linux-gnu", "sha256": "090bc9798c449ddee38a219e9be3ced2b77dbff1944e135a4c12d4293771b218"},
{"name": "terraphim-cli-x86_64-unknown-linux-musl", "sha256": "4eec28f4d0c84af8270307df9ab24a48ff5689acfd21a5b6cc5bfd372fe0c4d0"},
{"name": "terraphim-grep-aarch64-apple-darwin", "sha256": "be5fb9eea90c2a5dc7d70875e891e2fe318d841fd42a2a196a14508c31841107"},
{"name": "terraphim-grep-aarch64-unknown-linux-musl", "sha256": "1c8cb493052f4b483c52163e81fb12412e23b8a961e5eb0763d490b86696a9c3"},
{"name": "terraphim-grep-x86_64-apple-darwin", "sha256": "33bbf7d0c632f069b130810e41238880bb4e1ac8e1b20b57b5ad2bb29aa9cf74"},
{"name": "terraphim-grep-x86_64-pc-windows-msvc.exe", "sha256": "66dd5350bbc6ac9bcac69d45f3f6b31c284eba5815ceb82951d7649de4df8f45"},
{"name": "terraphim-grep-x86_64-unknown-linux-gnu", "sha256": "f9896c54a95add5b915b6c79b99bb425b0ed7ae4d6677d624b4d6164679de96c"},
{"name": "terraphim-grep-x86_64-unknown-linux-musl", "sha256": "532696f1805a18243a811b57cc4f509623720f02cbf9de482c8e1d8c9ffe1934"}
]
}
1 change: 1 addition & 0 deletions .github/release-signing/zipsign-primary-public-key.base64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iW2sM72/09yfiQ3jMB2GBALCRN+1FLLgD5qBbISFfS0=
348 changes: 348 additions & 0 deletions .github/workflows/finalize-prebuilt-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
name: Finalize Prebuilt Client Release

on:
workflow_dispatch:
inputs:
version:
description: Reviewed release version without the v prefix
required: true
type: string

permissions:
contents: write

concurrency:
group: terraphim-client-release-${{ inputs.version }}
cancel-in-progress: false

jobs:
finalize:
name: Verify, Apple-sign, archive-sign, and publish
if: >-
github.repository == 'terraphim/terraphim-clients' &&
github.ref == 'refs/heads/main'
environment: tsm-production-release
runs-on: macos-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Load review-bound release contract
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
[ "$GITHUB_REF" = refs/heads/main ] || {
echo "ERROR: releases may run only from refs/heads/main" >&2
exit 1
}
[ "$(git rev-parse HEAD)" = "$GITHUB_SHA" ] || {
echo "ERROR: checkout does not match the reviewed workflow commit" >&2
exit 1
}
python3 - <<'PY'
import os, re, sys

version = os.environ["VERSION"]
if not re.fullmatch(r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)", version):
sys.exit(f"invalid stable version: {version!r}")
PY

contract=".github/release-inputs/v$VERSION.json"
[ -f "$contract" ] || {
echo "ERROR: no reviewed release contract for v$VERSION" >&2
exit 1
}
jq -e --arg version "$VERSION" '
.schema_version == 1 and
.version == $version and
.release_tag == ("v" + $version) and
(.source_sha | test("^[0-9a-f]{40}$")) and
(.staging_asset == ("terraphim-clients-" + $version + "-release-inputs.tar.gz")) and
(.staging_sha256 | test("^[0-9a-f]{64}$")) and
(.builder.cargo_lock_sha256 | test("^[0-9a-f]{64}$")) and
(.binaries | length == 18) and
([.binaries[].name] | unique | length == 18) and
all(.binaries[];
(.name | test("^terraphim-(agent|cli|grep)-(aarch64-apple-darwin|x86_64-apple-darwin|x86_64-unknown-linux-gnu|x86_64-unknown-linux-musl|aarch64-unknown-linux-musl)$|^terraphim-(agent|cli|grep)-x86_64-pc-windows-msvc\\.exe$")) and
(.sha256 | test("^[0-9a-f]{64}$"))
)
' "$contract" >/dev/null

{
echo "RELEASE_CONTRACT=$contract"
echo "RELEASE_TAG=$(jq -r '.release_tag' "$contract")"
echo "EXPECTED_SOURCE_SHA=$(jq -r '.source_sha' "$contract")"
echo "STAGING_ASSET=$(jq -r '.staging_asset' "$contract")"
echo "STAGING_SHA256=$(jq -r '.staging_sha256' "$contract")"
} >> "$GITHUB_ENV"

- name: Validate immutable source and draft release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
ref_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG")"
object_sha="$(jq -r '.object.sha' <<<"$ref_json")"
object_type="$(jq -r '.object.type' <<<"$ref_json")"
while [ "$object_type" != commit ]; do
[ "$object_type" = tag ] || {
echo "ERROR: unsupported tag object type: $object_type" >&2
exit 1
}
tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha")"
object_type="$(jq -r '.object.type' <<<"$tag_json")"
object_sha="$(jq -r '.object.sha' <<<"$tag_json")"
done
[ "$object_sha" = "$EXPECTED_SOURCE_SHA" ] || {
echo "ERROR: tag resolves to $object_sha, expected $EXPECTED_SOURCE_SHA" >&2
exit 1
}
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ] || {
echo "ERROR: release $RELEASE_TAG must remain a draft until finalization succeeds" >&2
exit 1
}

- name: Download, securely extract, and hash-check build inputs
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
mkdir -p staging
gh release download "$RELEASE_TAG" --pattern "$STAGING_ASSET" --dir staging
printf '%s %s\n' "$STAGING_SHA256" "staging/$STAGING_ASSET" | shasum -a 256 -c -
scripts/validate-release-inputs.py \
--contract "$RELEASE_CONTRACT" \
--archive "staging/$STAGING_ASSET" \
--destination inputs

- name: Validate every binary format and native command surface
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
bins=(terraphim-agent terraphim-cli terraphim-grep)
for bin in "${bins[@]}"; do
[ "$(lipo -archs "inputs/$bin-aarch64-apple-darwin")" = arm64 ]
[ "$(lipo -archs "inputs/$bin-x86_64-apple-darwin")" = x86_64 ]
for target in x86_64-unknown-linux-gnu x86_64-unknown-linux-musl; do
description="$(file -b "inputs/$bin-$target")"
[[ "$description" == *"ELF 64-bit"* && "$description" == *"x86-64"* ]] || {
echo "ERROR: unexpected $target format for $bin: $description" >&2
exit 1
}
done
description="$(file -b "inputs/$bin-aarch64-unknown-linux-musl")"
[[ "$description" == *"ELF 64-bit"* && "$description" == *"ARM aarch64"* ]] || {
echo "ERROR: unexpected aarch64 Linux format for $bin: $description" >&2
exit 1
}
description="$(file -b "inputs/$bin-x86_64-pc-windows-msvc.exe")"
[[ "$description" == *"PE32+ executable"* && "$description" == *"x86-64"* ]] || {
echo "ERROR: unexpected Windows format for $bin: $description" >&2
exit 1
}
done
file inputs/* | tee staging/file-inventory.txt

for bin in "${bins[@]}"; do
lipo -create \
"inputs/$bin-x86_64-apple-darwin" \
"inputs/$bin-aarch64-apple-darwin" \
-output "inputs/$bin-universal-apple-darwin"
chmod 0755 "inputs/$bin-universal-apple-darwin"
reported="$("inputs/$bin-universal-apple-darwin" --version | tail -n 1 | awk '{print $NF}')"
[ "$reported" = "$VERSION" ] || {
echo "ERROR: $bin reports $reported, expected $VERSION" >&2
exit 1
}
done
inputs/terraphim-agent-universal-apple-darwin learn --help >/dev/null
inputs/terraphim-agent-universal-apple-darwin memory --help >/dev/null
inputs/terraphim-agent-universal-apple-darwin sessions expand --help >/dev/null

- name: Apple-sign and notarize every shipped macOS binary
shell: bash
env:
RUNNER_TEMP: ${{ runner.temp }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
CERT_BASE64: ${{ secrets.CERT_BASE64 }}
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
run: |
set -euo pipefail
for required in APPLE_ID APPLE_TEAM_ID APPLE_APP_PASSWORD CERT_BASE64 CERT_PASSWORD; do
[ -n "${!required:-}" ] || {
echo "ERROR: environment-scoped secret $required is unavailable" >&2
exit 1
}
done
for target in aarch64-apple-darwin x86_64-apple-darwin universal-apple-darwin; do
for bin in terraphim-agent terraphim-cli terraphim-grep; do
scripts/sign-macos-binary.sh \
"inputs/$bin-$target" "$APPLE_ID" "$APPLE_TEAM_ID" \
"$APPLE_APP_PASSWORD" "$CERT_BASE64" "$CERT_PASSWORD"
done
done

- name: Package and archive-sign release assets
shell: bash
env:
VERSION: ${{ inputs.version }}
ZIPSIGN_PRIVATE_KEY: ${{ secrets.ZIPSIGN_PRIVATE_KEY }}
run: |
set -euo pipefail
[ -n "${ZIPSIGN_PRIVATE_KEY:-}" ] || {
echo "ERROR: ZIPSIGN_PRIVATE_KEY is unavailable" >&2
exit 1
}
mkdir -p release-assets package-root
unix_targets=(
aarch64-apple-darwin
x86_64-apple-darwin
universal-apple-darwin
x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl
aarch64-unknown-linux-musl
)
bins=(terraphim-agent terraphim-cli terraphim-grep)
for target in "${unix_targets[@]}"; do
for bin in "${bins[@]}"; do
cp "inputs/$bin-$target" "release-assets/$bin-$target"
cp "inputs/$bin-$target" "package-root/$bin"
tar -czf "release-assets/$bin-$VERSION-$target.tar.gz" \
-C package-root "$bin"
rm -f "package-root/$bin"
done
done
# Windows v1.21.14 artifacts are manual-download packages. The tagged
# updater cannot verify ZIP signatures, so Windows is deliberately
# omitted from stable manifests until that source defect is fixed.
for bin in "${bins[@]}"; do
cp "inputs/$bin-x86_64-pc-windows-msvc.exe" \
"release-assets/$bin-x86_64-pc-windows-msvc.exe"
cp "inputs/$bin-x86_64-pc-windows-msvc.exe" "package-root/$bin.exe"
ditto -c -k --keepParent \
"package-root/$bin.exe" \
"release-assets/$bin-$VERSION-x86_64-pc-windows-msvc.zip"
rm -f "package-root/$bin.exe"
done
cargo install zipsign --version 0.2.1 --locked
scripts/sign-release-archives.sh release-assets

- name: Generate manifests and final checksums
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
for bin in terraphim-agent terraphim-cli terraphim-grep; do
scripts/build-manifest.sh "$VERSION" "$bin" release-assets \
> "release-assets/$bin-stable.json"
done
(
cd release-assets
shasum -a 256 ./* > SHA256SUMS
)

- name: Upload and byte-verify draft assets
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ] || {
echo "ERROR: release ceased to be a draft before mutation" >&2
exit 1
}
gh release upload "$RELEASE_TAG" release-assets/* --clobber
mkdir -p remote-assets
gh release download "$RELEASE_TAG" --dir remote-assets
for local_asset in release-assets/*; do
remote_asset="remote-assets/$(basename "$local_asset")"
[ -f "$remote_asset" ] || {
echo "ERROR: remote asset missing: $(basename "$local_asset")" >&2
exit 1
}
cmp "$local_asset" "$remote_asset"
done
expected_draft="$(mktemp)"
actual_draft="$(mktemp)"
trap 'rm -f "$expected_draft" "$actual_draft"' EXIT
{
for asset in release-assets/*; do basename "$asset"; done
printf '%s\n' "$STAGING_ASSET"
} | LC_ALL=C sort > "$expected_draft"
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ]
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual_draft"
diff -u "$expected_draft" "$actual_draft" || {
echo "ERROR: unexpected draft release inventory before publication" >&2
exit 1
}

- name: Publish atomically and verify final inventory
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release delete-asset "$RELEASE_TAG" "$STAGING_ASSET" --yes
expected="$(mktemp)"
actual="$(mktemp)"
trap 'rm -f "$expected" "$actual"' EXIT
for asset in release-assets/*; do basename "$asset"; done | LC_ALL=C sort > "$expected"
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ]
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual"
if ! diff -u "$expected" "$actual"; then
gh release upload "$RELEASE_TAG" "staging/$STAGING_ASSET" --clobber
echo "ERROR: final inventory changed; staging restored and release kept draft" >&2
exit 1
fi
publication_state=""
if ! gh release edit "$RELEASE_TAG" --draft=false; then
if ! publication_state="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"; then
echo "ERROR: publication result is ambiguous; no recovery mutation attempted" >&2
exit 1
fi
case "$(jq -r '.draft' <<<"$publication_state")" in
true)
gh release upload "$RELEASE_TAG" "staging/$STAGING_ASSET" --clobber
echo "ERROR: publication definitively failed; staging restored" >&2
exit 1
;;
false)
echo "WARN: publish command failed after GitHub committed publication; verifying state" >&2
;;
*)
echo "ERROR: publication state is unknown; no recovery mutation attempted" >&2
exit 1
;;
esac
fi
if [ -n "$publication_state" ]; then
release_json="$publication_state"
else
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
fi
[ "$(jq -r '.draft' <<<"$release_json")" = false ]
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual"
diff -u "$expected" "$actual"

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: client-release-finalization-evidence-${{ inputs.version }}
path: |
staging/file-inventory.txt
release-assets/SHA256SUMS
release-assets/*-stable.json
if-no-files-found: warn
Loading
Loading