Add tag-triggered CI release pipeline for build provenance #1
Workflow file for this run
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 | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| # One release build per tag; never cancel a release mid-flight. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| env: | |
| WORKSPACE: Cosign.xcworkspace | |
| SCHEME: Cosign | |
| APP_NAME: Cosign | |
| BUILD_CLAIM_KEY_ID: cosign-release-2026-06 | |
| COSIGN_REPOSITORY: hackshare/cosign | |
| jobs: | |
| release: | |
| name: Build, sign, upload, publish | |
| runs-on: macos-26 | |
| environment: ios-release | |
| steps: | |
| - name: Checkout release tag | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Verify tag is annotated, signed, and matches the event commit | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| EVENT_SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --tags --force | |
| TAG_TYPE="$(git cat-file -t "refs/tags/${TAG_NAME}")" | |
| if [[ "${TAG_TYPE}" != "tag" ]]; then | |
| echo "Release tag must be an annotated tag, got: ${TAG_TYPE}" >&2 | |
| exit 1 | |
| fi | |
| git tag -v "${TAG_NAME}" | |
| TAG_COMMIT="$(git rev-parse "${TAG_NAME}^{}")" | |
| if [[ "${TAG_COMMIT}" != "${EVENT_SHA}" ]]; then | |
| echo "Tag commit does not match the event SHA" >&2 | |
| echo " tag commit: ${TAG_COMMIT}" >&2 | |
| echo " event sha: ${EVENT_SHA}" >&2 | |
| exit 1 | |
| fi | |
| - name: Parse version and build from the tag | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "${TAG_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\+[0-9]+$ ]]; then | |
| echo "Tag must look like v<version>+<build>, got: ${TAG_NAME}" >&2 | |
| exit 1 | |
| fi | |
| APP_VERSION="${TAG_NAME#v}" | |
| APP_VERSION="${APP_VERSION%%+*}" | |
| APP_BUILD="${TAG_NAME##*+}" | |
| echo "APP_VERSION=${APP_VERSION}" >> "$GITHUB_ENV" | |
| echo "APP_BUILD=${APP_BUILD}" >> "$GITHUB_ENV" | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable | |
| with: | |
| targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios | |
| - name: Build Rust XCFramework | |
| run: ./scripts/build-xcframework.sh | |
| - name: Install Tuist | |
| run: brew install tuist | |
| - name: Generate Xcode project | |
| run: tuist generate --no-open | |
| - name: Fail if the checkout was mutated by dependency resolution | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "$(git status --porcelain)" ]]; then | |
| echo "Working tree is dirty after project generation / package resolution" >&2 | |
| git status --short >&2 | |
| exit 1 | |
| fi | |
| - name: Generate and sign the BuildClaim | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| BUILD_CLAIM_PRIVATE_KEY_B64: ${{ secrets.BUILD_CLAIM_PRIVATE_KEY_B64 }} | |
| run: | | |
| set -euo pipefail | |
| BUILD_CLAIM_DIR="$RUNNER_TEMP/build-claim" | |
| rm -rf "$BUILD_CLAIM_DIR"; mkdir -p "$BUILD_CLAIM_DIR" | |
| echo "BUILD_CLAIM_DIR=${BUILD_CLAIM_DIR}" >> "$GITHUB_ENV" | |
| APP_VERSION="$APP_VERSION" APP_BUILD="$APP_BUILD" \ | |
| BUILD_CLAIM_KEY_ID="$BUILD_CLAIM_KEY_ID" \ | |
| COSIGN_TAG="$TAG_NAME" \ | |
| COSIGN_REPOSITORY="$COSIGN_REPOSITORY" \ | |
| python3 ci/create_build_claim.py "$BUILD_CLAIM_DIR/BuildClaim.json" | |
| xcrun swiftc Tools/SignBuildClaim.swift -o "$RUNNER_TEMP/sign-build-claim" | |
| "$RUNNER_TEMP/sign-build-claim" \ | |
| "$BUILD_CLAIM_DIR/BuildClaim.json" \ | |
| "$BUILD_CLAIM_DIR/BuildClaim.sig" | |
| shasum -a 256 "$BUILD_CLAIM_DIR/BuildClaim.json" | |
| - name: Materialize the App Store Connect API key | |
| env: | |
| ASC_KEY_P8_B64: ${{ secrets.ASC_KEY_P8_B64 }} | |
| ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} | |
| run: | | |
| set -euo pipefail | |
| KEY_DIR="$HOME/.appstoreconnect/private_keys" | |
| mkdir -p "$KEY_DIR" | |
| KEY_PATH="$KEY_DIR/AuthKey_${ASC_KEY_ID}.p8" | |
| printf '%s' "$ASC_KEY_P8_B64" | base64 -d > "$KEY_PATH" | |
| chmod 600 "$KEY_PATH" | |
| echo "ASC_KEY_PATH=${KEY_PATH}" >> "$GITHUB_ENV" | |
| - name: Archive with the embedded BuildClaim (cloud signing) | |
| env: | |
| ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} | |
| ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| ARCHIVE_PATH="$RUNNER_TEMP/${APP_NAME}.xcarchive" | |
| echo "ARCHIVE_PATH=${ARCHIVE_PATH}" >> "$GITHUB_ENV" | |
| rm -rf "$ARCHIVE_PATH" | |
| xcodebuild archive \ | |
| -workspace "$WORKSPACE" \ | |
| -scheme "$SCHEME" \ | |
| -configuration Release \ | |
| -destination 'generic/platform=iOS' \ | |
| -archivePath "$ARCHIVE_PATH" \ | |
| -allowProvisioningUpdates \ | |
| -authenticationKeyPath "$ASC_KEY_PATH" \ | |
| -authenticationKeyID "$ASC_KEY_ID" \ | |
| -authenticationKeyIssuerID "$ASC_ISSUER_ID" \ | |
| EMBED_BUILD_CLAIM=YES \ | |
| BUILD_CLAIM_DIR="$BUILD_CLAIM_DIR" \ | |
| MARKETING_VERSION="$APP_VERSION" \ | |
| CURRENT_PROJECT_VERSION="$APP_BUILD" | |
| - name: Assert the claim is sealed and the bundle signature is valid | |
| run: | | |
| set -euo pipefail | |
| APP_PATH="$ARCHIVE_PATH/Products/Applications/${APP_NAME}.app" | |
| cmp "$BUILD_CLAIM_DIR/BuildClaim.json" "$APP_PATH/BuildClaim.json" | |
| cmp "$BUILD_CLAIM_DIR/BuildClaim.sig" "$APP_PATH/BuildClaim.sig" | |
| codesign --verify --deep --strict --verbose=4 "$APP_PATH" | |
| - name: Export the IPA | |
| run: | | |
| set -euo pipefail | |
| EXPORT_DIR="$RUNNER_TEMP/export" | |
| rm -rf "$EXPORT_DIR"; mkdir -p "$EXPORT_DIR" | |
| xcodebuild -exportArchive \ | |
| -archivePath "$ARCHIVE_PATH" \ | |
| -exportPath "$EXPORT_DIR" \ | |
| -exportOptionsPlist ci/export-options.plist \ | |
| -allowProvisioningUpdates | |
| IPA_PATH="$(find "$EXPORT_DIR" -maxdepth 1 -type f -name '*.ipa' -print -quit)" | |
| [ -n "$IPA_PATH" ] || { echo "No IPA exported" >&2; exit 1; } | |
| echo "IPA_PATH=${IPA_PATH}" >> "$GITHUB_ENV" | |
| shasum -a 256 "$IPA_PATH" | |
| - name: Build the public verification artifact | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| RELEASE_DIR="$RUNNER_TEMP/release" | |
| STAGE_DIR="$RUNNER_TEMP/verification-stage" | |
| rm -rf "$RELEASE_DIR" "$STAGE_DIR" | |
| mkdir -p "$RELEASE_DIR" "$STAGE_DIR" | |
| echo "RELEASE_DIR=${RELEASE_DIR}" >> "$GITHUB_ENV" | |
| cp "$BUILD_CLAIM_DIR/BuildClaim.json" "$RELEASE_DIR/" | |
| cp "$BUILD_CLAIM_DIR/BuildClaim.sig" "$RELEASE_DIR/" | |
| ditto "$ARCHIVE_PATH" "$STAGE_DIR/${APP_NAME}.xcarchive" | |
| cp "$BUILD_CLAIM_DIR/BuildClaim.json" "$STAGE_DIR/" | |
| cp "$BUILD_CLAIM_DIR/BuildClaim.sig" "$STAGE_DIR/" | |
| VERIFICATION_ARTIFACT="$RELEASE_DIR/${APP_NAME}-${TAG_NAME}-verification.zip" | |
| ditto -c -k --keepParent "$STAGE_DIR" "$VERIFICATION_ARTIFACT" | |
| echo "VERIFICATION_ARTIFACT=${VERIFICATION_ARTIFACT}" >> "$GITHUB_ENV" | |
| shasum -a 256 "$VERIFICATION_ARTIFACT" | |
| - name: Upload the exact IPA to TestFlight | |
| env: | |
| ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} | |
| ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| UPLOADED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| echo "UPLOADED_AT=${UPLOADED_AT}" >> "$GITHUB_ENV" | |
| xcrun altool --upload-app \ | |
| --type ios \ | |
| --file "$IPA_PATH" \ | |
| --apiKey "$ASC_KEY_ID" \ | |
| --apiIssuer "$ASC_ISSUER_ID" | |
| - name: Generate and sign the submission receipt | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| EVENT_SHA: ${{ github.sha }} | |
| RUN_ID: ${{ github.run_id }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| SUBMISSION_PRIVATE_KEY_B64: ${{ secrets.SUBMISSION_PRIVATE_KEY_B64 }} | |
| run: | | |
| set -euo pipefail | |
| CLAIM_SHA256="$(shasum -a 256 "$BUILD_CLAIM_DIR/BuildClaim.json" | awk '{print $1}')" | |
| IPA_SHA256="$(shasum -a 256 "$IPA_PATH" | awk '{print $1}')" | |
| VERIFY_SHA256="$(shasum -a 256 "$VERIFICATION_ARTIFACT" | awk '{print $1}')" | |
| python3 ci/create_submission_receipt.py \ | |
| --out "$RELEASE_DIR/TestFlightSubmission.json" \ | |
| --key-id "$BUILD_CLAIM_KEY_ID" \ | |
| --repository "$GITHUB_REPOSITORY" \ | |
| --tag "$TAG_NAME" \ | |
| --commit "$EVENT_SHA" \ | |
| --version "$APP_VERSION" \ | |
| --build "$APP_BUILD" \ | |
| --claim-sha256 "sha256:${CLAIM_SHA256}" \ | |
| --ipa-sha256 "sha256:${IPA_SHA256}" \ | |
| --verification-sha256 "sha256:${VERIFY_SHA256}" \ | |
| --run-id "$RUN_ID" \ | |
| --run-attempt "$RUN_ATTEMPT" \ | |
| --uploaded-at "$UPLOADED_AT" | |
| xcrun swiftc Tools/SignJsonFile.swift -o "$RUNNER_TEMP/sign-json-file" | |
| SIGNING_PRIVATE_KEY_B64="$SUBMISSION_PRIVATE_KEY_B64" \ | |
| "$RUNNER_TEMP/sign-json-file" \ | |
| "$RELEASE_DIR/TestFlightSubmission.json" \ | |
| "$RELEASE_DIR/TestFlightSubmission.sig" | |
| - name: Write SHA256SUMS | |
| run: | | |
| set -euo pipefail | |
| ( | |
| cd "$RELEASE_DIR" | |
| shasum -a 256 \ | |
| BuildClaim.json \ | |
| BuildClaim.sig \ | |
| TestFlightSubmission.json \ | |
| TestFlightSubmission.sig \ | |
| "$(basename "$VERIFICATION_ARTIFACT")" \ | |
| > SHA256SUMS | |
| ) | |
| cat "$RELEASE_DIR/SHA256SUMS" | |
| - name: Attest the verification artifact | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-path: ${{ env.VERIFICATION_ARTIFACT }} | |
| - name: Publish the GitHub Release | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| gh release create "$TAG_NAME" \ | |
| --verify-tag \ | |
| --draft \ | |
| --title "$TAG_NAME" \ | |
| --notes "Signed release build $TAG_NAME. The app shows this build's identity under Settings, Build verification; match the SHA-256 of BuildClaim.json here against the fingerprint the app shows. Attached: BuildClaim.{json,sig}, a signed submission receipt, an attested verification artifact, and SHA256SUMS." \ | |
| "$RELEASE_DIR/BuildClaim.json" \ | |
| "$RELEASE_DIR/BuildClaim.sig" \ | |
| "$RELEASE_DIR/TestFlightSubmission.json" \ | |
| "$RELEASE_DIR/TestFlightSubmission.sig" \ | |
| "$RELEASE_DIR/SHA256SUMS" \ | |
| "$VERIFICATION_ARTIFACT" | |
| gh release edit "$TAG_NAME" --draft=false |