Publish Release #45
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: Publish Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Existing release tag to publish | |
| required: true | |
| type: string | |
| identity_migration_phase: | |
| description: Apple identity migration mode for this release | |
| required: true | |
| default: disabled | |
| type: choice | |
| options: | |
| - disabled | |
| - legacy-preparer | |
| concurrency: | |
| group: release-draft-creation | |
| queue: max | |
| permissions: | |
| contents: read | |
| jobs: | |
| validate-ref: | |
| name: Validate Approved Release Ref | |
| if: github.ref == 'refs/heads/main' | |
| runs-on: macos-26 | |
| outputs: | |
| commit: ${{ steps.release-ref.outputs.commit }} | |
| tooling-commit: ${{ steps.release-ref.outputs.tooling-commit }} | |
| steps: | |
| - name: Check out trusted release tooling | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Require a tag reachable from protected main | |
| id: release-ref | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| run: | | |
| echo "commit=$(./Scripts/verify_release_ref.sh "$RELEASE_TAG")" >> "$GITHUB_OUTPUT" | |
| echo "tooling-commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| stage: | |
| name: Build Approved Source Without Secrets | |
| needs: validate-ref | |
| if: github.ref == 'refs/heads/main' | |
| runs-on: macos-26 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out trusted release tooling | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: ${{ needs.validate-ref.outputs.tooling-commit }} | |
| path: trusted-control-plane | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Check out approved tagged source | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: ${{ needs.validate-ref.outputs.commit }} | |
| path: release-source | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Resolve Stable release identity context | |
| env: | |
| REQUESTED_IDENTITY_MIGRATION_PHASE: ${{ inputs.identity_migration_phase }} | |
| run: | | |
| set -euo pipefail | |
| python3 trusted-control-plane/Scripts/stable_rollout.py workflow-guard \ | |
| --declaration release-source/release-rollout.json \ | |
| --policy trusted-control-plane/Scripts/apple_identity_policy.json | |
| python3 trusted-control-plane/Scripts/stable_rollout.py packaging-context \ | |
| --declaration release-source/release-rollout.json \ | |
| --policy trusted-control-plane/Scripts/apple_identity_policy.json \ | |
| --version-env release-source/version.env \ | |
| --expected-migration-phase "$REQUESTED_IDENTITY_MIGRATION_PHASE" \ | |
| --github-env "$GITHUB_ENV" \ | |
| --github-summary "$GITHUB_STEP_SUMMARY" | |
| - name: Build and stage approved source | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_COMMIT: ${{ needs.validate-ref.outputs.commit }} | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| SOURCE_GITHUB_REPOSITORY: ${{ github.repository }} | |
| REPOPROMPT_CONTROL_PLANE_SCRIPTS_DIR: ${{ github.workspace }}/trusted-control-plane/Scripts | |
| REPOPROMPT_RELEASE_SOURCE_ROOT: ${{ github.workspace }}/release-source | |
| # Link the Sentry SDK into the official release binary. The protected DSN is | |
| # injected later in the signing job, so the secret-free stage stays inert. | |
| REPOPROMPT_ENABLE_SENTRY: "1" | |
| run: ./trusted-control-plane/Scripts/release.sh stage-publish | |
| - name: Upload staged release source | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: RepoPrompt-CE-staged-release | |
| path: | | |
| release-source/dist/*-stage.zip | |
| release-source/dist/*-stage.zip.sha256 | |
| if-no-files-found: error | |
| retention-days: 1 | |
| publish: | |
| name: Sign, Notarize, and Create Draft | |
| needs: | |
| - validate-ref | |
| - stage | |
| if: github.ref == 'refs/heads/main' | |
| environment: release | |
| runs-on: macos-26 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Check out trusted release tooling | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: ${{ needs.validate-ref.outputs.tooling-commit }} | |
| path: trusted-control-plane | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Download staged release source | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 | |
| with: | |
| name: RepoPrompt-CE-staged-release | |
| path: staged-release | |
| - name: Check out approved source as data | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: ${{ needs.validate-ref.outputs.commit }} | |
| path: approved-source | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Resolve Stable release identity context | |
| env: | |
| REQUESTED_IDENTITY_MIGRATION_PHASE: ${{ inputs.identity_migration_phase }} | |
| run: | | |
| set -euo pipefail | |
| python3 trusted-control-plane/Scripts/stable_rollout.py workflow-guard \ | |
| --declaration approved-source/release-rollout.json \ | |
| --policy trusted-control-plane/Scripts/apple_identity_policy.json | |
| python3 trusted-control-plane/Scripts/stable_rollout.py packaging-context \ | |
| --declaration approved-source/release-rollout.json \ | |
| --policy trusted-control-plane/Scripts/apple_identity_policy.json \ | |
| --version-env approved-source/version.env \ | |
| --expected-migration-phase "$REQUESTED_IDENTITY_MIGRATION_PHASE" \ | |
| --github-env "$GITHUB_ENV" \ | |
| --github-summary "$GITHUB_STEP_SUMMARY" | |
| - name: Verify and expand staged release source | |
| env: | |
| RELEASE_COMMIT: ${{ needs.validate-ref.outputs.commit }} | |
| run: | | |
| set -euo pipefail | |
| cd staged-release | |
| shopt -s nullglob | |
| archives=(./*-stage.zip) | |
| checksum_files=(./*-stage.zip.sha256) | |
| [[ "${#archives[@]}" == "1" ]] || { echo "Expected exactly one staged ZIP" >&2; exit 1; } | |
| [[ "${#checksum_files[@]}" == "1" ]] || { echo "Expected exactly one staged ZIP checksum" >&2; exit 1; } | |
| archive="${archives[0]}" | |
| [[ "${checksum_files[0]}" == "$archive.sha256" ]] || { echo "Staged ZIP checksum name mismatch" >&2; exit 1; } | |
| shasum -a 256 -c "${checksum_files[0]}" | |
| ../trusted-control-plane/Scripts/extract_staged_release.py "$archive" ../release-source RepoPrompt | |
| REPOPROMPT_RELEASE_SOURCE_ROOT=../release-source \ | |
| REPOPROMPT_APPROVED_SOURCE_ROOT=../approved-source \ | |
| ../trusted-control-plane/Scripts/validate_staged_release.sh | |
| - name: Import Developer ID certificate | |
| env: | |
| CERTIFICATE_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }} | |
| CERTIFICATE_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }} | |
| CI_KEYCHAIN_PASSWORD: ${{ secrets.CI_KEYCHAIN_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| KEYCHAIN_PATH="$RUNNER_TEMP/repoprompt-release.keychain-db" | |
| CERTIFICATE_PATH="$RUNNER_TEMP/repoprompt-release.p12" | |
| keychain_created=0 | |
| cleanup_certificate_and_failed_keychain() { | |
| status=$? | |
| rm -f "$CERTIFICATE_PATH" | |
| if (( status != 0 )) && (( keychain_created )); then | |
| security delete-keychain "$KEYCHAIN_PATH" || true | |
| fi | |
| } | |
| trap cleanup_certificate_and_failed_keychain EXIT | |
| : "${EXPECTED_SIGN_IDENTITY:?Apple application identity is missing from the reviewed policy}" | |
| printf '%s' "$CERTIFICATE_P12_BASE64" | base64 --decode > "$CERTIFICATE_PATH" | |
| security create-keychain -p "$CI_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| keychain_created=1 | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$CI_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$CERTIFICATE_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$CI_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" | | |
| grep -F "\"$EXPECTED_SIGN_IDENTITY\"" >/dev/null || { | |
| echo "Expected application identity is not present and usable in the ephemeral keychain" >&2 | |
| exit 1 | |
| } | |
| printf 'KEYCHAIN_PATH=%s\n' "$KEYCHAIN_PATH" >> "$GITHUB_ENV" | |
| printf 'SIGN_IDENTITY=%s\n' "$EXPECTED_SIGN_IDENTITY" >> "$GITHUB_ENV" | |
| - name: Prepare successor identity migration anchor | |
| if: inputs.identity_migration_phase == 'legacy-preparer' | |
| env: | |
| SUCCESSOR_CERTIFICATE_P12_BASE64: ${{ secrets.SUCCESSOR_DEVELOPER_ID_APPLICATION_P12_BASE64 }} | |
| SUCCESSOR_CERTIFICATE_P12_PASSWORD: ${{ secrets.SUCCESSOR_DEVELOPER_ID_APPLICATION_P12_PASSWORD }} | |
| CI_KEYCHAIN_PASSWORD: ${{ secrets.CI_KEYCHAIN_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| : "${SUCCESSOR_CERTIFICATE_P12_BASE64:?Successor Developer ID Application certificate is not configured}" | |
| : "${SUCCESSOR_CERTIFICATE_P12_PASSWORD:?Successor Developer ID Application password is not configured}" | |
| expected_identity="$EXPECTED_MIGRATION_ANCHOR_SIGN_IDENTITY" | |
| [[ -n "$expected_identity" ]] || { | |
| echo "Migration-anchor signing identity is missing from the reviewed policy" >&2 | |
| exit 1 | |
| } | |
| successor_certificate="$RUNNER_TEMP/repoprompt-release-successor.p12" | |
| anchor_source="trusted-control-plane/Scripts/identity_migration_anchor.c" | |
| anchor="$RUNNER_TEMP/repoprompt-successor-identity-anchor" | |
| [[ -f "$anchor_source" ]] || { | |
| echo "Missing trusted successor identity anchor source: $anchor_source" >&2 | |
| exit 1 | |
| } | |
| xcrun clang -arch arm64 -arch x86_64 -Os -Wl,-dead_strip -o "$anchor" "$anchor_source" | |
| [[ -f "$anchor" && -x "$anchor" ]] || { | |
| echo "Failed to build successor identity anchor" >&2 | |
| exit 1 | |
| } | |
| printf '%s' "$SUCCESSOR_CERTIFICATE_P12_BASE64" | base64 --decode > "$successor_certificate" | |
| security import "$successor_certificate" -k "$KEYCHAIN_PATH" \ | |
| -P "$SUCCESSOR_CERTIFICATE_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: \ | |
| -s -k "$CI_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" | | |
| grep -F "\"$expected_identity\"" >/dev/null || { | |
| echo "Expected successor application identity is not present and usable in the ephemeral keychain" >&2 | |
| exit 1 | |
| } | |
| codesign --force --sign "$expected_identity" --keychain "$KEYCHAIN_PATH" \ | |
| --identifier "$EXPECTED_MIGRATION_ANCHOR_BUNDLE_ID" \ | |
| --timestamp --options runtime "$anchor" | |
| codesign --verify --strict --verbose=2 \ | |
| -R="$EXPECTED_MIGRATION_ANCHOR_REQUIREMENT" "$anchor" | |
| signature_details="$(codesign -dv --verbose=4 "$anchor" 2>&1)" | |
| grep -Fx "Identifier=$EXPECTED_MIGRATION_ANCHOR_BUNDLE_ID" \ | |
| <<< "$signature_details" >/dev/null | |
| grep -Fx "TeamIdentifier=$EXPECTED_MIGRATION_ANCHOR_TEAM_ID" \ | |
| <<< "$signature_details" >/dev/null | |
| printf 'REPOPROMPT_IDENTITY_MIGRATION_ANCHOR=%s\n' "$anchor" >> "$GITHUB_ENV" | |
| - name: Prepare provisioning profile and notarization key | |
| env: | |
| PROVISIONING_PROFILE_BASE64: ${{ secrets.REPOPROMPT_CE_PROVISIONING_PROFILE_BASE64 }} | |
| NOTARYTOOL_PRIVATE_KEY_BASE64: ${{ secrets.NOTARYTOOL_PRIVATE_KEY_BASE64 }} | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| SECRETS_DIR="$RUNNER_TEMP/repoprompt-release-secrets" | |
| mkdir -p "$SECRETS_DIR" | |
| printf '%s' "$PROVISIONING_PROFILE_BASE64" | base64 --decode > "$SECRETS_DIR/repoprompt-ce.provisionprofile" | |
| printf '%s' "$NOTARYTOOL_PRIVATE_KEY_BASE64" | base64 --decode > "$SECRETS_DIR/notarytool-private-key.p8" | |
| echo "REPOPROMPT_PROVISIONING_PROFILE=$SECRETS_DIR/repoprompt-ce.provisionprofile" >> "$GITHUB_ENV" | |
| echo "NOTARYTOOL_PRIVATE_KEY=$SECRETS_DIR/notarytool-private-key.p8" >> "$GITHUB_ENV" | |
| - name: Install Sentry CLI when symbol upload is configured | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]] && ! command -v sentry-cli >/dev/null 2>&1; then | |
| brew install getsentry/tools/sentry-cli | |
| fi | |
| - name: Prepare Sentry symbol upload token file | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then | |
| umask 077 | |
| SENTRY_TOKEN_FILE="$RUNNER_TEMP/repoprompt-release-secrets/sentry-auth-token" | |
| printf '%s' "$SENTRY_AUTH_TOKEN" > "$SENTRY_TOKEN_FILE" | |
| echo "REPOPROMPT_SENTRY_AUTH_TOKEN_FILE=$SENTRY_TOKEN_FILE" >> "$GITHUB_ENV" | |
| fi | |
| - name: Sign, notarize, and create draft release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_COMMIT: ${{ needs.validate-ref.outputs.commit }} | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| SOURCE_GITHUB_REPOSITORY: ${{ github.repository }} | |
| REPOPROMPT_CONTROL_PLANE_SCRIPTS_DIR: ${{ github.workspace }}/trusted-control-plane/Scripts | |
| REPOPROMPT_GIT_ROOT: ${{ github.workspace }}/trusted-control-plane | |
| REPOPROMPT_RELEASE_SOURCE_ROOT: ${{ github.workspace }}/release-source | |
| REPOPROMPT_APPROVED_SOURCE_ROOT: ${{ github.workspace }}/approved-source | |
| REPOPROMPT_REQUIRE_HEAD_MATCH: 0 | |
| REPOPROMPT_ENABLE_SENTRY: "1" | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| NOTARYTOOL_KEY_ID: ${{ secrets.NOTARYTOOL_KEY_ID }} | |
| NOTARYTOOL_ISSUER_ID: ${{ secrets.NOTARYTOOL_ISSUER_ID }} | |
| # Protected release environment secret. sign_staged_release.sh bakes it | |
| # into the signed bundle's Info.plist as RepoPromptSentryDSN. Optional: if | |
| # unset, telemetry stays off (fail-safe). | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| REPOPROMPT_SENTRY_ORG: ${{ vars.SENTRY_ORG }} | |
| REPOPROMPT_SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }} | |
| run: ./trusted-control-plane/Scripts/release.sh publish-staged | |
| - name: Remove ephemeral keychain | |
| if: always() | |
| run: | | |
| KEYCHAIN_PATH="$RUNNER_TEMP/repoprompt-release.keychain-db" | |
| CERTIFICATE_PATH="$RUNNER_TEMP/repoprompt-release.p12" | |
| security delete-keychain "$KEYCHAIN_PATH" || true | |
| rm -f "$CERTIFICATE_PATH" | |
| rm -f "$RUNNER_TEMP/repoprompt-release-successor.p12" | |
| rm -f "$RUNNER_TEMP/repoprompt-successor-identity-anchor" | |
| rm -rf "$RUNNER_TEMP/repoprompt-release-secrets" | |
| - name: Upload signed release ZIP for secret-free smoke | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: RepoPrompt-CE-signed-release-zip | |
| path: | | |
| release-source/dist/*.zip | |
| release-source/dist/SHA256SUMS | |
| release-source/dist/*-artifact-manifest.json | |
| if-no-files-found: error | |
| retention-days: 1 | |
| smoke-signed-helper: | |
| name: Smoke Signed Helper Without Release Secrets | |
| needs: | |
| - validate-ref | |
| - publish | |
| if: github.ref == 'refs/heads/main' | |
| runs-on: macos-26 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out trusted release tooling | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: ${{ needs.validate-ref.outputs.tooling-commit }} | |
| path: trusted-control-plane | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Download signed release ZIP | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 | |
| with: | |
| name: RepoPrompt-CE-signed-release-zip | |
| path: signed-release | |
| - name: Extract and statically validate signed helper layout | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| archives=(signed-release/*.zip) | |
| checksum_manifests=(signed-release/*SHA256SUMS) | |
| artifact_manifests=(signed-release/*-artifact-manifest.json) | |
| [[ "${#archives[@]}" == "1" ]] || { echo "Expected exactly one signed ZIP" >&2; exit 1; } | |
| [[ "${#checksum_manifests[@]}" == "1" ]] || { echo "Expected exactly one signed ZIP checksum manifest" >&2; exit 1; } | |
| [[ "${#artifact_manifests[@]}" == "1" ]] || { echo "Expected exactly one signed app artifact manifest" >&2; exit 1; } | |
| archive="${archives[0]}" | |
| archive_name="$(basename "$archive")" | |
| archive_checksum_entry="$RUNNER_TEMP/signed-release-zip.sha256" | |
| awk -v archive_name="$archive_name" '$2 == archive_name { print }' "${checksum_manifests[0]}" > "$archive_checksum_entry" | |
| [[ "$(wc -l < "$archive_checksum_entry" | tr -d ' ')" == "1" ]] || { echo "Expected exactly one signed ZIP checksum entry" >&2; exit 1; } | |
| (cd signed-release && shasum -a 256 -c "$archive_checksum_entry") | |
| mkdir extracted | |
| ditto -x -k "$archive" extracted | |
| ./trusted-control-plane/Scripts/validate_embedded_mcp_helper_layout.sh \ | |
| "extracted/RepoPrompt CE.app" \ | |
| "Signed draft MCP helper layout" | |
| ./trusted-control-plane/Scripts/validate_app_architectures.sh \ | |
| "extracted/RepoPrompt CE.app" \ | |
| "arm64,x86_64" \ | |
| "Signed draft app" | |
| ./trusted-control-plane/Scripts/write_app_artifact_manifest.py verify \ | |
| --app "extracted/RepoPrompt CE.app" \ | |
| --manifest "${artifact_manifests[0]}" \ | |
| --expected-architectures "arm64,x86_64" | |
| echo "ARTIFACT_MANIFEST_PATH=${artifact_manifests[0]}" >> "$GITHUB_ENV" | |
| - name: Execute signed helper and packaged roundtrip without inherited secrets | |
| env: | |
| REPOPROMPT_PACKAGED_SMOKE_TIMEOUT: "240" | |
| REPOPROMPT_PACKAGED_SMOKE_HELPER_TIMEOUT: "60" | |
| run: | | |
| env -i \ | |
| PATH=/usr/bin:/bin:/usr/sbin:/sbin \ | |
| HOME="$HOME" \ | |
| TMPDIR="$RUNNER_TEMP" \ | |
| USER="$USER" \ | |
| LOGNAME="$LOGNAME" \ | |
| ./trusted-control-plane/Scripts/smoke_embedded_mcp_helper.sh \ | |
| "extracted/RepoPrompt CE.app" \ | |
| "Signed draft MCP helper" | |
| env -i \ | |
| PATH=/usr/bin:/bin:/usr/sbin:/sbin \ | |
| HOME="$HOME" \ | |
| TMPDIR="$RUNNER_TEMP" \ | |
| USER="$USER" \ | |
| LOGNAME="$LOGNAME" \ | |
| REPOPROMPT_PACKAGED_SMOKE_TIMEOUT="$REPOPROMPT_PACKAGED_SMOKE_TIMEOUT" \ | |
| REPOPROMPT_PACKAGED_SMOKE_HELPER_TIMEOUT="$REPOPROMPT_PACKAGED_SMOKE_HELPER_TIMEOUT" \ | |
| ./trusted-control-plane/Scripts/smoke_packaged_mcp_roundtrip.sh \ | |
| "extracted/RepoPrompt CE.app" \ | |
| "Signed draft packaged app" \ | |
| "$ARTIFACT_MANIFEST_PATH" |