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
118 changes: 118 additions & 0 deletions .github/workflows/migrate-release-secrets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Migrate Release Secrets

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: migrate-tsm-production-release-secrets
cancel-in-progress: false

jobs:
migrate:
name: Copy release credentials into the protected environment
if: >-
github.repository == 'terraphim/terraphim-clients' &&
github.ref == 'refs/heads/main'
environment: tsm-production-release
runs-on: ubuntu-24.04
steps:
- name: Install 1Password CLI
uses: 1password/install-cli-action@c1b138d5779f64eda6936d5caa8e754b9f3996c0 # v2

- name: Copy credentials without exposing their values
shell: bash
env:
GH_TOKEN: ${{ secrets.TERRAPHIM_AI_RELEASE_TOKEN }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
ZIPSIGN_PRIVATE_KEY: ${{ secrets.ZIPSIGN_PRIVATE_KEY }}
run: |
set -euo pipefail
set +x
environment=tsm-production-release
repository="$GITHUB_REPOSITORY"

for required in GH_TOKEN OP_SERVICE_ACCOUNT_TOKEN ZIPSIGN_PRIVATE_KEY; do
[ -n "${!required:-}" ] || {
echo "ERROR: repository migration secret $required is unavailable" >&2
exit 1
}
done

APPLE_ID="$(op read \
'op://TerraphimPlatform/apple.developer.credentials/username' \
--no-newline)"
APPLE_TEAM_ID="$(op read \
'op://TerraphimPlatform/apple.developer.credentials/APPLE_TEAM_ID' \
--no-newline)"
APPLE_APP_PASSWORD="$(op read \
'op://TerraphimPlatform/apple.developer.credentials/APPLE_APP_SPECIFIC_PASSWORD' \
--no-newline)"
CERT_BASE64="$(op read \
'op://TerraphimPlatform/apple.developer.certificate/base64' \
--no-newline)"
CERT_PASSWORD="$(op read \
'op://TerraphimPlatform/apple.developer.certificate/password' \
--no-newline)"

for required in APPLE_ID APPLE_TEAM_ID APPLE_APP_PASSWORD \
CERT_BASE64 CERT_PASSWORD; do
[ -n "${!required:-}" ] || {
echo "ERROR: 1Password value $required is empty" >&2
exit 1
}
done
[[ "$APPLE_ID" =~ ^[^[:space:]@]+@[^[:space:]@]+$ ]] || {
echo "ERROR: APPLE_ID is not an email address" >&2
exit 1
}
[[ "$APPLE_TEAM_ID" =~ ^[A-Z0-9]{10}$ ]] || {
echo "ERROR: APPLE_TEAM_ID is not a ten-character team identifier" >&2
exit 1
}
[[ "$APPLE_APP_PASSWORD" =~ ^[A-Za-z0-9-]{8,64}$ ]] || {
echo "ERROR: APPLE_APP_PASSWORD has an unexpected format" >&2
exit 1
}
export CERT_PASSWORD
printf '%s' "$CERT_BASE64" | base64 --decode |
openssl pkcs12 -in /dev/stdin -passin env:CERT_PASSWORD -noout

copy_secret() {
local name="$1"
printf '%s' "${!name}" |
gh secret set "$name" --repo "$repository" --env "$environment"
}

copy_secret APPLE_ID
copy_secret APPLE_TEAM_ID
copy_secret APPLE_APP_PASSWORD
copy_secret CERT_BASE64
copy_secret CERT_PASSWORD
copy_secret ZIPSIGN_PRIVATE_KEY

expected="$(printf '%s\n' \
APPLE_APP_PASSWORD APPLE_ID APPLE_TEAM_ID CERT_BASE64 CERT_PASSWORD \
ZIPSIGN_PRIVATE_KEY | LC_ALL=C sort)"
actual="$(gh secret list --repo "$repository" --env "$environment" \
--json name --jq '.[].name' | LC_ALL=C sort)"
[ "$actual" = "$expected" ] || {
echo "ERROR: protected environment secret inventory is incomplete" >&2
exit 1
}

# Remove the reusable migration inputs only after the target inventory
# is complete. A later dispatch therefore fails closed before mutation.
for source_secret in OP_SERVICE_ACCOUNT_TOKEN ZIPSIGN_PRIVATE_KEY \
TERRAPHIM_AI_RELEASE_TOKEN; do
gh secret delete "$source_secret" --repo "$repository"
done
repository_secrets="$(gh secret list --repo "$repository" --json name)"
for removed in OP_SERVICE_ACCOUNT_TOKEN ZIPSIGN_PRIVATE_KEY \
TERRAPHIM_AI_RELEASE_TOKEN; do
jq -e --arg name "$removed" \
'all(.[]; .name != $name)' <<<"$repository_secrets" >/dev/null
done
echo "Protected release secrets migrated; reusable source path retired."
260 changes: 9 additions & 251 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
@@ -1,260 +1,18 @@
name: Release Client Binaries
name: Legacy Client Binary Release (retired)

on:
workflow_dispatch:
inputs:
version:
description: 'Release version without v prefix (e.g. 1.20.5)'
required: true
type: string
release_tag:
description: 'GitHub release tag (e.g. v1.20.5)'
required: true
type: string
target_repo:
description: 'GitHub repo to attach binaries to'
required: false
default: terraphim-ai
type: string

permissions:
contents: write

env:
CARGO_TERM_COLOR: always
permissions: {}

jobs:
build-binaries:
name: Build client binaries for ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include:
# GitHub-hosted Linux: terraphim-ai self-hosted runners are repo-scoped.
- 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-latest
target: x86_64-apple-darwin
use_cross: false
- os: macos-latest
target: aarch64-apple-darwin
use_cross: false
- os: windows-latest
target: x86_64-pc-windows-msvc
use_cross: false
runs-on: ${{ matrix.os }}
env:
CARGO_REGISTRIES_TERRAPHIM_TOKEN: ${{ secrets.CARGO_REGISTRIES_TERRAPHIM_TOKEN }}
retired:
name: Use the review-bound release finalizer
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install zig
if: contains(matrix.target, 'apple-darwin') || contains(matrix.target, 'windows')
shell: bash
run: |
if command -v zig &>/dev/null; then exit 0; fi
if command -v brew &>/dev/null; then brew install zig; fi
if command -v choco &>/dev/null; then choco install zig -y; fi
- name: Install cross
if: matrix.use_cross
run: |
if command -v cross &>/dev/null; then
cross --version
exit 0
fi
rustup run stable cargo install cross --locked --git https://github.com/cross-rs/cross
- uses: Swatinem/rust-cache@v2
if: matrix.target != 'x86_64-unknown-linux-gnu'
with:
key: clients-${{ matrix.target }}
- name: Set release version (#67 — binaries must report the tag version)
shell: bash
run: |
# Bump the workspace version to the release input so CARGO_PKG_VERSION
# baked into the binaries matches the git tag. Not committed to main
# (the bump lives only in this CI checkout, like the tag itself).
# Use a backup suffix so the in-place edit works on both GNU and BSD/macOS sed.
sed -i.bak 's/^version = ".*"/version = "${{ inputs.version }}"/' Cargo.toml
rm -f Cargo.toml.bak
grep -n '^version =' Cargo.toml
cargo metadata --no-deps --format-version 1 >/dev/null
- name: Build client binaries
- name: Explain the supported release path
shell: bash
run: |
if [ "${{ matrix.use_cross }}" = "true" ]; then
BUILD="rustup run stable cross"
else
BUILD="rustup run stable cargo"
fi
$BUILD build --release --target ${{ matrix.target }} -p terraphim_agent --bin terraphim-agent
$BUILD build --release --target ${{ matrix.target }} -p terraphim-cli --bin terraphim-cli
$BUILD build --release --target ${{ matrix.target }} -p terraphim_grep --bin terraphim-grep --features "code-search openrouter"
- name: Package artifacts (Unix)
if: matrix.os != 'windows-latest'
env:
VERSION: ${{ inputs.version }}
run: |
mkdir -p artifacts
tar -czf "artifacts/terraphim-agent-${VERSION}-${{ matrix.target }}.tar.gz" -C "target/${{ matrix.target }}/release" terraphim-agent
tar -czf "artifacts/terraphim-cli-${VERSION}-${{ matrix.target }}.tar.gz" -C "target/${{ matrix.target }}/release" terraphim-cli
tar -czf "artifacts/terraphim-grep-${VERSION}-${{ matrix.target }}.tar.gz" -C "target/${{ matrix.target }}/release" terraphim-grep
cp target/${{ matrix.target }}/release/terraphim-agent artifacts/terraphim-agent-${{ matrix.target }}
cp target/${{ matrix.target }}/release/terraphim-cli artifacts/terraphim-cli-${{ matrix.target }}
cp target/${{ matrix.target }}/release/terraphim-grep artifacts/terraphim-grep-${{ matrix.target }}
chmod +x artifacts/*
- name: Package artifacts (Windows)
if: matrix.os == 'windows-latest'
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
mkdir -p artifacts
cd "target/${{ matrix.target }}/release"
7z a -tzip "../../../artifacts/terraphim-agent-${VERSION}-${{ matrix.target }}.zip" terraphim-agent.exe
7z a -tzip "../../../artifacts/terraphim-cli-${VERSION}-${{ matrix.target }}.zip" terraphim-cli.exe
7z a -tzip "../../../artifacts/terraphim-grep-${VERSION}-${{ matrix.target }}.zip" terraphim-grep.exe
cd -
cp target/${{ matrix.target }}/release/terraphim-agent.exe artifacts/terraphim-agent-${{ matrix.target }}.exe
cp target/${{ matrix.target }}/release/terraphim-cli.exe artifacts/terraphim-cli-${{ matrix.target }}.exe
cp target/${{ matrix.target }}/release/terraphim-grep.exe artifacts/terraphim-grep-${{ matrix.target }}.exe
- uses: actions/upload-artifact@v4
with:
name: client-binaries-${{ matrix.target }}
path: artifacts/*

create-universal-macos:
name: Create macOS universal client binaries
needs: build-binaries
if: always() && needs.build-binaries.result != 'cancelled'
runs-on: macos-latest
steps:
- uses: actions/download-artifact@v4
with:
name: client-binaries-x86_64-apple-darwin
path: x86_64
- uses: actions/download-artifact@v4
with:
name: client-binaries-aarch64-apple-darwin
path: aarch64
- run: |
mkdir -p universal
lipo -create x86_64/terraphim-agent-x86_64-apple-darwin aarch64/terraphim-agent-aarch64-apple-darwin -output universal/terraphim-agent-universal-apple-darwin
lipo -create x86_64/terraphim-grep-x86_64-apple-darwin aarch64/terraphim-grep-aarch64-apple-darwin -output universal/terraphim-grep-universal-apple-darwin
chmod +x universal/*
- uses: actions/upload-artifact@v4
with:
name: client-binaries-universal-apple-darwin
path: universal/*

sign-and-notarize-macos:
name: Sign and notarize macOS client binaries
needs: create-universal-macos
if: always() && needs.create-universal-macos.result == 'success'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: client-binaries-universal-apple-darwin
path: universal
- uses: 1password/install-cli-action@v2
- name: Load signing credentials
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
run: |
echo "APPLE_ID=$(op read 'op://TerraphimPlatform/apple.developer.credentials/username' --no-newline)" >> $GITHUB_ENV
echo "APPLE_TEAM_ID=$(op read 'op://TerraphimPlatform/apple.developer.credentials/APPLE_TEAM_ID' --no-newline)" >> $GITHUB_ENV
echo "APPLE_APP_PASSWORD=$(op read 'op://TerraphimPlatform/apple.developer.credentials/APPLE_APP_SPECIFIC_PASSWORD' --no-newline)" >> $GITHUB_ENV
echo "CERT_BASE64=$(op read 'op://TerraphimPlatform/apple.developer.certificate/base64' --no-newline)" >> $GITHUB_ENV
echo "CERT_PASSWORD=$(op read 'op://TerraphimPlatform/apple.developer.certificate/password' --no-newline)" >> $GITHUB_ENV
- name: Sign and notarize agent and grep
env:
RUNNER_TEMP: ${{ runner.temp }}
run: |
chmod +x scripts/sign-macos-binary.sh
./scripts/sign-macos-binary.sh universal/terraphim-agent-universal-apple-darwin "$APPLE_ID" "$APPLE_TEAM_ID" "$APPLE_APP_PASSWORD" "$CERT_BASE64" "$CERT_PASSWORD"
./scripts/sign-macos-binary.sh universal/terraphim-grep-universal-apple-darwin "$APPLE_ID" "$APPLE_TEAM_ID" "$APPLE_APP_PASSWORD" "$CERT_BASE64" "$CERT_PASSWORD"
- uses: actions/upload-artifact@v4
with:
name: client-binaries-signed-universal-apple-darwin
path: universal/*

upload-to-target-release:
name: Sign + attach to GitHub release + publish to R2
needs: [build-binaries, sign-and-notarize-macos]
# Attach when macOS sign succeeded; do not require full matrix (Windows is optional).
if: >-
always() &&
!cancelled() &&
needs.sign-and-notarize-macos.result == 'success' &&
needs.build-binaries.result != 'cancelled'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: client-binaries*
path: release-assets
merge-multiple: true
- name: Install zipsign
run: cargo install zipsign --locked
- name: Sign .tar.gz archives (Ed25519, fail-closed)
env:
ZIPSIGN_PRIVATE_KEY: ${{ secrets.ZIPSIGN_PRIVATE_KEY }}
run: |
if [ -z "$ZIPSIGN_PRIVATE_KEY" ]; then
echo "ERROR: ZIPSIGN_PRIVATE_KEY secret not set" >&2; exit 2
fi
scripts/sign-release-archives.sh release-assets
- name: Upload to target GitHub release
env:
GH_TOKEN: ${{ secrets.TERRAPHIM_AI_RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
run: |
TAG="${{ inputs.release_tag }}"
REPO="terraphim/${{ inputs.target_repo }}"
find release-assets -type f | sort
gh release upload "$TAG" release-assets/* --repo "$REPO" --clobber
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Publish signed artifacts + manifest to R2 (#68)
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [ -z "${CLOUDFLARE_API_TOKEN:-}" ]; then
echo "WARN: CLOUDFLARE_API_TOKEN not set; skipping R2 publish" >&2; exit 0
fi
# Upload each signed archive to r2://terraphim-releases/<bin>/<filename>.
# (--remote is required: without it wrangler writes to local worker storage
# and the object is invisible via the downloads.terraphim.ai custom domain.)
for f in release-assets/*.tar.gz; do
base="$(basename "$f")"
# asset filename pattern: <bin>-<version>-<target>.tar.gz
bin="${base%%-$VERSION-*}"
bunx wrangler r2 object put "terraphim-releases/${bin}/${base}" --file "$f" --remote
done
# Publish manifests LAST (atomicity: never point at a missing asset).
# One manifest per binary that appears in release-assets.
bins="$(ls -1 release-assets/*.tar.gz | sed -E "s|^release-assets/||;s/-${VERSION}-.*//" | sort -u)"
for bin in $bins; do
scripts/build-manifest.sh "$VERSION" "$bin" release-assets > "/tmp/${bin}.stable.json"
bunx wrangler r2 object put "terraphim-releases/${bin}/stable.json" \
--file "/tmp/${bin}.stable.json" --content-type application/json --remote
done
# Fail-closed: every published URL must be fetchable.
for bin in $bins; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://downloads.terraphim.ai/${bin}/stable.json")
[ "$code" = "200" ] || { echo "manifest $bin returned $code" >&2; exit 1; }
done
echo "R2 publish complete; manifests live at https://downloads.terraphim.ai/<bin>/stable.json"
echo "This legacy build-and-publish workflow is retired." >&2
echo "Use 'Finalize Prebuilt Client Release' with a reviewed release contract." >&2
exit 1
Loading