Repository navigation
Release v1.21.16 from ba182b507c03dc039e994822078ce7f2ba597d62 (correlation release-v1.21.16-clients-ba182b5) #40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |