Skip to content

Release v1.21.16 from ba182b507c03dc039e994822078ce7f2ba597d62 (correlation release-v1.21.16-clients-ba182b5) #40

Release v1.21.16 from ba182b507c03dc039e994822078ce7f2ba597d62 (correlation release-v1.21.16-clients-ba182b5)

Release v1.21.16 from ba182b507c03dc039e994822078ce7f2ba597d62 (correlation release-v1.21.16-clients-ba182b5) #40

name: Release Client Binaries
run-name: Release ${{ inputs.release_tag }} from ${{ inputs.expected_source_sha }} (correlation ${{ inputs.correlation_id }})
on:
workflow_dispatch:
inputs:
version:
description: Release version without v prefix
required: true
type: string
release_tag:
description: Source release tag, including v prefix
required: true
type: string
source_ref:
description: Immutable source ref; must equal release_tag
required: true
type: string
expected_source_sha:
description: Expected peeled 40-character source commit SHA
required: true
type: string
correlation_id:
description: Safe caller identity used to resolve this exact run
required: true
type: string
target_repo:
description: Compatibility identity for the stage-only caller
required: false
default: terraphim-ai
type: string
publish_to_target_release:
description: Compatibility input; the producer requires stage-only false
required: false
default: false
type: boolean
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
preflight:
name: Validate immutable source contract
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.contract.outputs.version }}
release_tag: ${{ steps.contract.outputs.release_tag }}
source_sha: ${{ steps.contract.outputs.source_sha }}
source_date_epoch: ${{ steps.metadata.outputs.source_date_epoch }}
correlation_id: ${{ steps.contract.outputs.correlation_id }}
stable_version: ${{ steps.contract.outputs.stable_version }}
steps:
- name: Validate dispatch identity and peel source tag
id: contract
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
RELEASE_TAG: ${{ inputs.release_tag }}
SOURCE_REF: ${{ inputs.source_ref }}
EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}
TARGET_REPO: ${{ inputs.target_repo }}
CORRELATION_ID: ${{ inputs.correlation_id }}
PUBLISH_TO_TARGET_RELEASE: ${{ inputs.publish_to_target_release }}
run: |
set -euo pipefail
python3 - <<'PY'
import os, re, sys
version = os.environ["VERSION"]
release_tag = os.environ["RELEASE_TAG"]
source_ref = os.environ["SOURCE_REF"]
expected_source_sha = os.environ["EXPECTED_SOURCE_SHA"]
target_repo = os.environ["TARGET_REPO"]
correlation_id = os.environ["CORRELATION_ID"]
publish = os.environ["PUBLISH_TO_TARGET_RELEASE"]
stable = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
if not stable.fullmatch(version):
sys.exit(f"input version {version!r} is not a stable semantic version")
if release_tag != f"v{version}":
sys.exit(f"release_tag {release_tag!r} must equal 'v' plus version {version!r}")
if source_ref != release_tag:
sys.exit(f"release_tag {release_tag!r} must equal source_ref {source_ref!r}")
if not re.fullmatch(r"[0-9a-f]{40}", expected_source_sha):
sys.exit("expected_source_sha is not a 40-character lowercase hex SHA")
if target_repo != "terraphim-ai":
sys.exit("stage-only mode requires target_repo 'terraphim-ai'")
if publish != "false":
sys.exit("stage-only producer requires publish_to_target_release 'false'")
if not correlation_id or correlation_id != correlation_id.strip():
sys.exit("correlation_id must be non-empty with no surrounding whitespace")
if len(correlation_id) > 128 or not re.fullmatch(r"[A-Za-z0-9._:/@+-]+", correlation_id):
sys.exit("correlation_id contains unsafe characters or exceeds 128 characters")
PY
peel_tag_ref() {
local ref_name="$1" ref_json object_sha object_type tag_json
ref_json="$(gh api "repos/${{ github.repository }}/git/ref/tags/${ref_name}")"
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 "unsupported tag object $object_type" >&2; exit 1; }
tag_json="$(gh api "repos/${{ github.repository }}/git/tags/${object_sha}")"
object_sha="$(jq -r '.object.sha' <<<"$tag_json")"
object_type="$(jq -r '.object.type' <<<"$tag_json")"
done
printf '%s\n' "$object_sha"
}
source_sha="$(peel_tag_ref "$SOURCE_REF")"
[ "$source_sha" = "$EXPECTED_SOURCE_SHA" ] || {
echo "peeled source SHA does not match expected_source_sha" >&2
exit 1
}
# Keep the package-stage eligibility explicit at the job boundary.
# The immutable #248 producer currently accepts stable versions only,
# but this output keeps the narrower managed-package policy fail-closed
# if the top-level release contract is broadened in the future.
if [[ "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
stable_version=true
else
stable_version=false
fi
{
echo "version=$VERSION"
echo "release_tag=$RELEASE_TAG"
echo "source_sha=$source_sha"
echo "correlation_id=$CORRELATION_ID"
echo "stable_version=$stable_version"
} >> "$GITHUB_OUTPUT"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.contract.outputs.source_sha }}
- name: Validate immutable checked-in release metadata
id: metadata
shell: bash
env:
VERSION: ${{ steps.contract.outputs.version }}
RELEASE_TAG: ${{ steps.contract.outputs.release_tag }}
SOURCE_SHA: ${{ steps.contract.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
test -z "$(git status --porcelain --untracked-files=no)"
git diff --exit-code -- Cargo.toml Cargo.lock
cargo metadata --locked --no-deps --format-version 1 > /tmp/release-metadata.json
python3 - <<'PY'
import json, os, sys, tomllib
version = os.environ["VERSION"]
release_tag = os.environ["RELEASE_TAG"]
with open("Cargo.toml", "rb") as handle:
workspace_version = tomllib.load(handle)["workspace"]["package"]["version"]
if release_tag != f"v{workspace_version}":
sys.exit("release tag does not match checked-in workspace version")
if version != workspace_version:
sys.exit("input version does not match checked-in workspace version")
with open("/tmp/release-metadata.json", encoding="utf-8") as handle:
versions = {p["name"]: p["version"] for p in json.load(handle)["packages"]}
for package in ("terraphim_agent", "terraphim-cli", "terraphim_grep"):
if versions.get(package) != workspace_version:
sys.exit(f"{package} metadata version does not match {workspace_version}")
PY
git diff --exit-code -- Cargo.toml Cargo.lock
test -z "$(git status --porcelain)"
echo "source_date_epoch=$(git show -s --format=%ct HEAD)" >> "$GITHUB_OUTPUT"
build-binaries:
name: Build immutable binaries for ${{ matrix.target }}
needs: preflight
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
use_cross: false
- os: ubuntu-22.04
target: x86_64-unknown-linux-musl
use_cross: true
- os: ubuntu-22.04
target: aarch64-unknown-linux-musl
use_cross: true
- os: macos-15-intel
target: x86_64-apple-darwin
use_cross: false
- os: macos-15
target: aarch64-apple-darwin
use_cross: false
- os: windows-latest
target: x86_64-pc-windows-msvc
use_cross: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.preflight.outputs.source_sha }}
- name: Assert clean exact source checkout
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${{ needs.preflight.outputs.source_sha }}"
test -z "$(git status --porcelain)"
git diff --exit-code -- Cargo.toml Cargo.lock
- name: Install exact Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal --target "${{ matrix.target }}"
- name: Install cross
if: matrix.use_cross
run: >-
rustup run 1.96.0 cargo install cross --locked
--git https://github.com/cross-rs/cross
--rev 88f49ff79e777bef6d3564531636ee4d3cc2f8d2
- name: Install QEMU for supported foreign execution
if: matrix.target == 'aarch64-unknown-linux-musl'
run: sudo apt-get update -qq && sudo apt-get install -y -qq qemu-user-static
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.1
with:
key: immutable-clients-${{ matrix.target }}
- name: Build all shipped binaries from the locked source
shell: bash
env:
CARGO_PROFILE_RELEASE_STRIP: symbols
CARGO_REGISTRIES_TERRAPHIM_TOKEN: ${{ secrets.CARGO_REGISTRIES_TERRAPHIM_TOKEN }}
run: |
set -euo pipefail
if [ "${{ matrix.use_cross }}" = "true" ]; then
build=(rustup run 1.96.0 cross)
else
build=(rustup run 1.96.0 cargo)
fi
"${build[@]}" build --locked --release --target "${{ matrix.target }}" -p terraphim_agent --bin terraphim-agent
"${build[@]}" build --locked --release --target "${{ matrix.target }}" -p terraphim-cli --bin terraphim-cli
"${build[@]}" build --locked --release --target "${{ matrix.target }}" -p terraphim_grep --bin terraphim-grep --features "code-search openrouter"
git diff --exit-code -- Cargo.toml Cargo.lock
test -z "$(git status --porcelain)"
- name: Reject unstripped final Linux package bytes
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
for binary in terraphim-agent terraphim-cli terraphim-grep; do
path="target/${{ matrix.target }}/release/$binary"
if readelf -S "$path" | grep -Fq '.symtab'; then
echo "$path retains .symtab after the canonical release build" >&2
exit 1
fi
done
- name: Verify exact binary versions and architectures
shell: bash
env:
VERSION: ${{ needs.preflight.outputs.version }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
extension=""
[ "$TARGET" != "x86_64-pc-windows-msvc" ] || extension=".exe"
for binary in terraphim-agent terraphim-cli terraphim-grep; do
path="target/$TARGET/release/${binary}${extension}"
test -s "$path"
test -x "$path" || [ -n "$extension" ]
scripts/validate_release_binary.py "$TARGET" "$path"
case "$TARGET" in
aarch64-unknown-linux-musl) output="$(qemu-aarch64-static "$path" --version)" ;;
x86_64-apple-darwin) output="$(arch -x86_64 "$path" --version)" ;;
aarch64-apple-darwin) output="$(arch -arm64 "$path" --version)" ;;
*) output="$("$path" --version)" ;;
esac
reported="$(printf '%s\n' "$output" | tail -n1 | awk '{print $NF}')"
[ "$reported" = "$VERSION" ] || { echo "$binary reported $reported, expected $VERSION" >&2; exit 1; }
done
- name: Collect canonical binaries without byte mutation
shell: bash
run: |
set -euo pipefail
mkdir raw
extension=""
[ "${{ matrix.target }}" != "x86_64-pc-windows-msvc" ] || extension=".exe"
for binary in terraphim-agent terraphim-cli terraphim-grep; do
cp "target/${{ matrix.target }}/release/${binary}${extension}" "raw/${binary}-${{ matrix.target }}${extension}"
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: raw-client-binaries-${{ matrix.target }}
path: raw/*
if-no-files-found: error
overwrite: false
stage-canonical-linux:
name: Stage and hash canonical Linux binaries
needs: [preflight, build-binaries]
if: >-
always() && !cancelled() &&
needs.preflight.result == 'success' &&
needs.build-binaries.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.preflight.outputs.source_sha }}
- name: Assert clean exact source checkout
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${{ needs.preflight.outputs.source_sha }}"
test -z "$(git status --porcelain)"
git diff --exit-code -- Cargo.toml Cargo.lock
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-x86_64-unknown-linux-gnu
path: raw
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-x86_64-unknown-linux-musl
path: raw
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-aarch64-unknown-linux-musl
path: raw
- name: Stage and hash canonical Linux package bytes
run: |
set -euo pipefail
scripts/stage-canonical-linux.py raw canonical-binaries BINARY_SHA256SUMS
(cd canonical-binaries && sha256sum -c ../BINARY_SHA256SUMS)
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: canonical-linux-binaries-${{ needs.preflight.outputs.version }}-${{ needs.preflight.outputs.source_sha }}
path: |
canonical-binaries/*
BINARY_SHA256SUMS
if-no-files-found: error
overwrite: false
build-client-packages:
name: Build client managed packages for ${{ matrix.target }}
needs: [preflight, stage-canonical-linux]
permissions:
contents: read
env:
SOURCE_DATE_EPOCH: ${{ needs.preflight.outputs.source_date_epoch }}
# Fail closed: package only the final canonical Linux stage, and only for
# the stable-version channel supported by the managed-package contract.
if: >-
always() &&
!cancelled() &&
needs.preflight.result == 'success' &&
needs.stage-canonical-linux.result == 'success' &&
needs.preflight.outputs.stable_version == 'true'
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
runner: ubuntu-22.04
runner_arch: X64
nfpm_arch: x86_64
nfpm_archive_sha256: 0660ca602b2d2d2ae4781a06c692b3eeb9d437ffea05b831d76e41f4a3188783
nfpm_binary_sha256: 17133a2467ffb7cec851c2d7bae0c6098d09d7ed7d3d101a9605f6a473323936
- target: aarch64-unknown-linux-musl
runner: ubuntu-22.04-arm
runner_arch: ARM64
nfpm_arch: arm64
nfpm_archive_sha256: 1c0f5f2999b9a974bfb04fdb0cc3306096de530ac5dbb25d739cc5f5219c919c
nfpm_binary_sha256: 4d7ddf169945f7f557ac5373035d373429050d00476925953560e1ac65e16c74
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.preflight.outputs.source_sha }}
- name: Assert clean exact source checkout
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${{ needs.preflight.outputs.source_sha }}"
test -z "$(git status --porcelain)"
git diff --exit-code -- Cargo.toml Cargo.lock
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: canonical-linux-binaries-${{ needs.preflight.outputs.version }}-${{ needs.preflight.outputs.source_sha }}
path: canonical-stage
- name: Verify canonical binary receipt after artifact transfer
run: |
set -euo pipefail
(cd canonical-stage/canonical-binaries && sha256sum -c ../BINARY_SHA256SUMS)
- name: Install pinned nFPM
shell: bash
env:
NFPM_VERSION: 2.47.0
NFPM_ARCH: ${{ matrix.nfpm_arch }}
NFPM_ARCHIVE_SHA256: ${{ matrix.nfpm_archive_sha256 }}
NFPM_BINARY_SHA256: ${{ matrix.nfpm_binary_sha256 }}
run: |
set -euo pipefail
install_dir="/tmp/nfpm-bin"
mkdir -p "$install_dir"
archive="$install_dir/nfpm_${NFPM_VERSION}_Linux_${NFPM_ARCH}.tar.gz"
url="https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_Linux_${NFPM_ARCH}.tar.gz"
curl -fsSL "$url" -o "$archive"
printf '%s %s\n' "$NFPM_ARCHIVE_SHA256" "$archive" | sha256sum -c -
tar -xzf "$archive" -C "$install_dir" nfpm
printf '%s %s\n' "$NFPM_BINARY_SHA256" "$install_dir/nfpm" | sha256sum -c -
mv "$install_dir/nfpm" "$install_dir/nfpm-${NFPM_VERSION}"
chmod 0755 "$install_dir/nfpm-${NFPM_VERSION}"
.github/scripts/nfpm/verify-nfpm.sh \
--binary "$install_dir/nfpm-${NFPM_VERSION}" \
--version "$NFPM_VERSION" \
--sha256 "$NFPM_BINARY_SHA256"
- name: Run native managed-package lifecycle gate
shell: bash
env:
NFPM_BIN: /tmp/nfpm-bin/nfpm-2.47.0
TARGET: ${{ matrix.target }}
EXPECTED_RUNNER_ARCH: ${{ matrix.runner_arch }}
REQUIRE_INSTALL: "1"
run: |
set -euo pipefail
test "${{ runner.arch }}" = "$EXPECTED_RUNNER_ARCH"
.github/scripts/nfpm/tests/test_client_nfpm_native.sh
- name: Build managed DEB/RPM packages
shell: bash
env:
VERSION: ${{ needs.preflight.outputs.version }}
TARGET: ${{ matrix.target }}
NFPM_BIN: /tmp/nfpm-bin/nfpm-2.47.0
run: |
set -euo pipefail
AGENT_BIN="canonical-stage/canonical-binaries/terraphim-agent-${TARGET}"
GREP_BIN="canonical-stage/canonical-binaries/terraphim-grep-${TARGET}"
test -f "$AGENT_BIN"
test -f "$GREP_BIN"
chmod 0755 "$AGENT_BIN" "$GREP_BIN"
.github/scripts/nfpm/build-client-packages.sh \
--version "$VERSION" \
--target "$TARGET" \
--agent-binary "$AGENT_BIN" \
--grep-binary "$GREP_BIN" \
--out-dir "client-managed-packages/${TARGET}" \
--nfpm "$NFPM_BIN"
- name: Run actual-package native lifecycle gate (installs the real produced DEB/RPM)
shell: bash
env:
NFPM_BIN: /tmp/nfpm-bin/nfpm-2.47.0
TARGET: ${{ matrix.target }}
VERSION: ${{ needs.preflight.outputs.version }}
PACKAGE_DIR: client-managed-packages/${{ matrix.target }}
AGENT_BINARY: canonical-stage/canonical-binaries/terraphim-agent-${{ matrix.target }}
GREP_BINARY: canonical-stage/canonical-binaries/terraphim-grep-${{ matrix.target }}
EXPECTED_RUNNER_ARCH: ${{ matrix.runner_arch }}
REQUIRE_INSTALL: "1"
run: |
set -euo pipefail
test "${{ runner.arch }}" = "$EXPECTED_RUNNER_ARCH"
.github/scripts/nfpm/tests/test_client_nfpm_native_actual.sh
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: client-managed-packages-${{ matrix.target }}
path: client-managed-packages/${{ matrix.target }}/*
if-no-files-found: error
overwrite: false
create-universal-macos:
name: Create universal macOS agent and grep
needs: [preflight, build-binaries]
if: >-
always() && !cancelled() &&
needs.preflight.result == 'success' &&
needs.build-binaries.result == 'success'
runs-on: macos-15
permissions:
contents: read
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-x86_64-apple-darwin
path: x86_64
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-aarch64-apple-darwin
path: aarch64
- name: Create and validate universal binaries
run: |
set -euo pipefail
mkdir universal
for binary in terraphim-agent terraphim-grep; do
lipo -create \
"x86_64/${binary}-x86_64-apple-darwin" \
"aarch64/${binary}-aarch64-apple-darwin" \
-output "universal/${binary}-universal-apple-darwin"
chmod 755 "universal/${binary}-universal-apple-darwin"
# Xcode 16.4 lipo requires the input file before the
# -verify_arch command/arch flags; the legacy flags-first order
# parses the file path as an architecture and fails.
lipo "universal/${binary}-universal-apple-darwin" -verify_arch x86_64 arm64
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: raw-client-binaries-universal-apple-darwin
path: universal/*
if-no-files-found: error
overwrite: false
sign-and-notarize-macos:
name: Finalize all macOS bytes
needs: [preflight, build-binaries, create-universal-macos]
if: >-
always() && !cancelled() &&
needs.preflight.result == 'success' &&
needs.build-binaries.result == 'success' &&
needs.create-universal-macos.result == 'success'
runs-on: macos-15
permissions:
contents: read
# GitHub #21 reconciliation: production credentials (the 1Password service
# account feeding the Apple signing secrets) live only in the reviewed
# tsm-production-release environment on this repository; declare it so the
# job receives them, exactly as the release finalizer does.
environment: tsm-production-release
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.preflight.outputs.source_sha }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-x86_64-apple-darwin
path: macos
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-aarch64-apple-darwin
path: macos
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-universal-apple-darwin
path: macos
- uses: 1password/install-cli-action@9a0c9dd934086b7ab1d90115d455bda1c53c2bdb # v2, tag object c1b138d5779f64eda6936d5caa8e754b9f3996c0
- name: Provision and prove Rosetta for thin x86_64 execution
run: |
set -euo pipefail
sudo softwareupdate --install-rosetta --agree-to-license
arch -x86_64 /usr/bin/true
- name: Sign, notarize, and revalidate final macOS binaries
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
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 }}
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
# Shared validation: non-empty, CERT_BASE64 newline-stripped, all
# other values single-line, masked in logs, exported under $name.
normalise_and_mask() {
local name="$1" value="$2"
[ -n "$value" ] || { echo "empty credential $name" >&2; exit 1; }
if [ "$name" = "CERT_BASE64" ]; then
value="${value//$'\r'/}"
value="${value//$'\n'/}"
elif [[ "$value" == *$'\r'* || "$value" == *$'\n'* ]]; then
echo "multiline credential rejected for $name" >&2
exit 1
fi
printf '::add-mask::%s\n' "$value"
printf -v "$name" '%s' "$value"
# ${name?} both asserts the credential name is set/non-null and
# exports the variable it names (ShellCheck SC2163 form); the
# masking above and the empty-value check are unchanged.
export "${name?}"
}
load_masked() {
local name="$1" reference="$2"
normalise_and_mask "$name" "$(op read "$reference" --no-newline)"
}
load_masked_env() {
local name="$1"
normalise_and_mask "$name" "${!name:-}"
}
# Preferred source is the 1Password service account; when it has not
# been provisioned, the identical credentials held by the reviewed
# tsm-production-release environment (the same source
# finalize-prebuilt-release.yml uses) keep the lane unblocked.
if [ -n "${OP_SERVICE_ACCOUNT_TOKEN:-}" ]; then
load_masked APPLE_ID 'op://TerraphimPlatform/apple.developer.credentials/username'
load_masked APPLE_TEAM_ID 'op://TerraphimPlatform/apple.developer.credentials/APPLE_TEAM_ID'
load_masked APPLE_APP_PASSWORD 'op://TerraphimPlatform/apple.developer.credentials/APPLE_APP_SPECIFIC_PASSWORD'
load_masked CERT_BASE64 'op://TerraphimPlatform/apple.developer.certificate/base64'
load_masked CERT_PASSWORD 'op://TerraphimPlatform/apple.developer.certificate/password'
else
echo "NOTE: OP_SERVICE_ACCOUNT_TOKEN not set; using tsm-production-release environment secrets" >&2
load_masked_env APPLE_ID
load_masked_env APPLE_TEAM_ID
load_masked_env APPLE_APP_PASSWORD
load_masked_env CERT_BASE64
load_masked_env CERT_PASSWORD
fi
chmod 755 macos/*
for path in macos/*; do
scripts/sign-macos-binary.sh "$path" "$APPLE_ID" "$APPLE_TEAM_ID" "$APPLE_APP_PASSWORD" "$CERT_BASE64" "$CERT_PASSWORD"
codesign --verify --strict --verbose=2 "$path"
binary="$(basename "$path")"
case "$binary" in
*-x86_64-apple-darwin) output="$(arch -x86_64 "$path" --version)" ;;
*-aarch64-apple-darwin) output="$(arch -arm64 "$path" --version)" ;;
*) output="$("$path" --version)" ;;
esac
[ "$(printf '%s\n' "$output" | tail -n1 | awk '{print $NF}')" = "$VERSION" ]
done
git diff --exit-code -- Cargo.toml Cargo.lock
test -z "$(git status --porcelain --untracked-files=no)"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: signed-client-binaries-apple-darwin
path: macos/*
if-no-files-found: error
overwrite: false
seal-release-stage:
name: Validate and seal immutable release stage
needs: [preflight, build-binaries, stage-canonical-linux, build-client-packages, sign-and-notarize-macos]
# Managed packages may be skipped only when their stable-version gate is
# out of scope. A real packaging failure always blocks the sealed stage.
if: >-
always() && !cancelled() &&
needs.preflight.result == 'success' &&
needs.build-binaries.result == 'success' &&
needs.stage-canonical-linux.result == 'success' &&
(needs.build-client-packages.result == 'success' || needs.build-client-packages.result == 'skipped') &&
needs.sign-and-notarize-macos.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
# GitHub #21 reconciliation: the zipsign release private key is a
# production credential held only in the reviewed tsm-production-release
# environment on this repository; declare it so the sealing job receives
# it, exactly as the release finalizer does.
environment: tsm-production-release
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.preflight.outputs.source_sha }}
- name: Assert clean exact source checkout
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${{ needs.preflight.outputs.source_sha }}"
test -z "$(git status --porcelain)"
git diff --exit-code -- Cargo.toml Cargo.lock
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: canonical-linux-binaries-${{ needs.preflight.outputs.version }}-${{ needs.preflight.outputs.source_sha }}
path: .
- name: Verify canonical Linux binary receipt after artifact transfer
run: |
set -euo pipefail
(cd canonical-binaries && sha256sum -c ../BINARY_SHA256SUMS)
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-x86_64-pc-windows-msvc
path: raw
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: signed-client-binaries-apple-darwin
path: raw
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: needs.build-client-packages.result == 'success'
with:
name: client-managed-packages-x86_64-unknown-linux-musl
path: client-managed-staging/client-managed-packages-x86_64-unknown-linux-musl
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: needs.build-client-packages.result == 'success'
with:
name: client-managed-packages-aarch64-unknown-linux-musl
path: client-managed-staging/client-managed-packages-aarch64-unknown-linux-musl
- name: Create deterministic archives and exact asset enumeration
env:
VERSION: ${{ needs.preflight.outputs.version }}
SOURCE_DATE_EPOCH: ${{ needs.preflight.outputs.source_date_epoch }}
run: |
set -euo pipefail
mkdir -p release-assets package-root manifests
: > expected-assets.txt
for binary in terraphim-agent terraphim-cli terraphim-grep; do
targets=(x86_64-unknown-linux-gnu x86_64-unknown-linux-musl aarch64-unknown-linux-musl x86_64-apple-darwin aarch64-apple-darwin)
if [ "$binary" != "terraphim-cli" ]; then targets+=(universal-apple-darwin); fi
for target in "${targets[@]}"; do
root="package-root/$binary-$target"
mkdir -p "$root"
source="raw/$binary-$target"
if [[ "$target" == *-linux-* ]]; then
source="canonical-binaries/$binary-$target"
fi
cp "$source" "$root/$binary"
cmp "$source" "$root/$binary"
cp LICENSE-Apache-2.0 LICENSE-MIT "$root/"
chmod 755 "$root/$binary"
chmod 644 "$root/LICENSE-Apache-2.0" "$root/LICENSE-MIT"
touch -d "@$SOURCE_DATE_EPOCH" "$root/$binary" "$root/LICENSE-Apache-2.0" "$root/LICENSE-MIT"
archive="$binary-$VERSION-$target.tar.gz"
LC_ALL=C tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner \
-C "$root" -cf - "$binary" LICENSE-Apache-2.0 LICENSE-MIT | gzip -n -9 > "release-assets/$archive"
printf '%s\n' "$archive" >> expected-assets.txt
done
target=x86_64-pc-windows-msvc
root="package-root/$binary-$target"
mkdir -p "$root"
cp "raw/$binary-$target.exe" "$root/$binary.exe"
cp LICENSE-Apache-2.0 LICENSE-MIT "$root/"
chmod 755 "$root/$binary.exe"
chmod 644 "$root/LICENSE-Apache-2.0" "$root/LICENSE-MIT"
touch -d "@$SOURCE_DATE_EPOCH" "$root/$binary.exe" "$root/LICENSE-Apache-2.0" "$root/LICENSE-MIT"
archive="$binary-$VERSION-$target.zip"
scripts/create-deterministic-zip.py "$SOURCE_DATE_EPOCH" "$root" \
"release-assets/$archive" "$binary.exe" LICENSE-Apache-2.0 LICENSE-MIT
printf '%s\n' "$archive" >> expected-assets.txt
done
LC_ALL=C sort -o expected-assets.txt expected-assets.txt
find release-assets -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort > actual-assets.txt
diff -u expected-assets.txt actual-assets.txt
test "$(wc -l < expected-assets.txt | tr -d ' ')" = 20
- name: Install archive signer
run: cargo install zipsign --version 0.2.1 --locked
- name: Sign every final archive
env:
ZIPSIGN_PRIVATE_KEY: ${{ secrets.ZIPSIGN_PRIVATE_KEY }}
run: scripts/sign-release-archives.sh release-assets
- name: Validate exact post-sign archives before sealing
env:
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
scripts/sign-release-archives.sh --verify-only release-assets
while read -r archive; do
scripts/validate-release-archive.py "$VERSION" "release-assets/$archive"
done < expected-assets.txt
- name: Assemble unsigned managed packages into a separate stage-only inventory
shell: bash
env:
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
mkdir managed-release-assets
.github/scripts/release/assemble-client-release-inventory.sh \
--output managed-release-assets \
--expected-version "$VERSION" \
--managed-staging client-managed-staging \
--managed-target x86_64-unknown-linux-musl \
--managed-target aarch64-unknown-linux-musl
- name: Seal checksums and dual-schema candidate manifests
env:
VERSION: ${{ needs.preflight.outputs.version }}
SOURCE_DATE_EPOCH: ${{ needs.preflight.outputs.source_date_epoch }}
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
CORRELATION_ID: ${{ needs.preflight.outputs.correlation_id }}
run: |
set -euo pipefail
# NUL-safe, deterministic SHA256SUMS: find emits bare filenames
# NUL-delimited, sort -z applies the same LC_ALL=C byte order the
# unquoted-substitution form used, and a single sha256sum
# invocation over the array keeps the exact `hash filename`
# output format (bare names, later verified by `sha256sum -c`
# from inside release-assets). The explicit emptiness guard keeps
# a zero-asset stage failing closed instead of reading stdin.
(
cd release-assets
mapfile -d '' sealed_assets < <(LC_ALL=C find . -maxdepth 1 -type f -printf '%f\0' | LC_ALL=C sort -z)
[ "${#sealed_assets[@]}" -gt 0 ] || { echo "no release assets to seal" >&2; exit 1; }
LC_ALL=C sha256sum "${sealed_assets[@]}" > ../SHA256SUMS
)
(cd release-assets && sha256sum -c ../SHA256SUMS)
test "$(wc -l < SHA256SUMS | tr -d ' ')" = 20
for binary in terraphim-agent terraphim-cli terraphim-grep; do
scripts/build-manifest.sh "$VERSION" "$binary" release-assets "manifests/$binary.v2.candidate.json"
scripts/build-legacy-manifest.py "manifests/$binary.v2.candidate.json" "manifests/$binary.v1.candidate.json"
python3 -m json.tool "manifests/$binary.v2.candidate.json" >/dev/null
python3 -m json.tool "manifests/$binary.v1.candidate.json" >/dev/null
done
python3 - <<'PY'
import json, os, pathlib
provenance = {
"archive_signatures": "embedded-zipsign-ed25519",
"correlation_id": os.environ["CORRELATION_ID"],
"release_tag": os.environ["RELEASE_TAG"],
"source_sha": os.environ["SOURCE_SHA"],
"stage_identity": f"client-release-stage-{os.environ['VERSION']}-{os.environ['SOURCE_SHA']}",
"version": os.environ["VERSION"],
}
pathlib.Path("provenance.json").write_text(json.dumps(provenance, sort_keys=True, indent=2) + "\n")
PY
git diff --exit-code -- Cargo.toml Cargo.lock
test -z "$(git status --porcelain --untracked-files=no)"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: client-release-stage-${{ needs.preflight.outputs.version }}-${{ needs.preflight.outputs.source_sha }}
path: |
release-assets/*
managed-release-assets/*
canonical-binaries/*
manifests/*.candidate.json
SHA256SUMS
BINARY_SHA256SUMS
expected-assets.txt
provenance.json
if-no-files-found: error
overwrite: false