diff --git a/.github/workflows/migrate-release-secrets.yml b/.github/workflows/migrate-release-secrets.yml new file mode 100644 index 0000000..37ed836 --- /dev/null +++ b/.github/workflows/migrate-release-secrets.yml @@ -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." diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 80a7c0e..4639d4a 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -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//. - # (--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: --.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//stable.json" \ No newline at end of file + echo "This legacy build-and-publish workflow is retired." >&2 + echo "Use 'Finalize Prebuilt Client Release' with a reviewed release contract." >&2 + exit 1