The point of running the CLI in CI is to stop committing signing material to a private repository and stop fragile bash glue around the Apple Developer portal. Instead, store the certificate and profile IDs as pipeline variables, fetch them by ID with the CLI, import them, and run xcodebuild as normal.
What you need
- A service credential with
hexsign-api/readscope (write isn't needed if you only fetch). - The IDs of the certificate and profile you want to sign with. The dashboard exposes the ID on each detail page; the CLI prints it via
hexsign certificates list -o json. - An ephemeral, isolated keychain per run (the CLI's
--keychainflag creates one for you), so the imported certificate doesn't leak into other jobs.
The CI step
# .github/workflows/release.yml
- name: Fetch signing material
env:
HEXSIGN_CLIENT_ID: ${{ secrets.HEXSIGN_CLIENT_ID }}
HEXSIGN_CLIENT_SECRET: ${{ secrets.HEXSIGN_CLIENT_SECRET }}
PROFILE_ID: ${{ vars.HEXSIGN_PROFILE_ID }}
CERT_ID: ${{ vars.HEXSIGN_CERT_ID }}
run: |
hexsign certificates download "$CERT_ID" --output-dir build/sign \
--keychain "$RUNNER_TEMP/signing.keychain-db"
hexsign profiles download "$PROFILE_ID" --output-dir build/sign --installcertificates download writes a .p12 and a sibling .password file (both 0600). profiles download writes the .mobileprovision Apple expects. Both files land in the directory you pick; keep it inside the workspace so the run-specific filesystem cleanup wipes it at the end.
Build
--keychain already imported and authorized the certificate, and --install already placed the profile where Xcode looks for it, so there is nothing left but to run xcodebuild.
# The signing keychain and profile are ready, build as usual xcrun xcodebuild archive \ -workspace MyApp.xcworkspace \ -scheme MyApp \ -archivePath build/MyApp.xcarchive
Prefer to manage the keychain and profile yourself? Omit the flags and run security import against a keychain you create, then copy the .mobileprovision into ~/Library/MobileDevice/Provisioning Profiles. --keychain and --install are conveniences, not requirements.
Alternative: pin to cert type + bundle id (rotation-proof)
If you regenerate certificates or profiles regularly, hard-coded UUIDs become a recurring chore; every rotation means updating the CI variable. Use the bulk-download flags instead: ask for every certificate of a given type for a given Apple Developer team, and every profile for a bundle id. --keychain and --install still import and install every match, so the build step needs no changes when an artefact rotates.
# .github/workflows/release.yml: rotation-proof variant
- name: Fetch signing material
env:
HEXSIGN_CLIENT_ID: ${{ secrets.HEXSIGN_CLIENT_ID }}
HEXSIGN_CLIENT_SECRET: ${{ secrets.HEXSIGN_CLIENT_SECRET }}
HEXSIGN_TEAM_ID: ${{ vars.HEXSIGN_TEAM_ID }} # e.g. ABCDE12345
HEXSIGN_BUNDLE_ID: ${{ vars.HEXSIGN_BUNDLE_ID }} # e.g. com.example.app
run: |
hexsign certificates download \
--type DISTRIBUTION --team-id "$HEXSIGN_TEAM_ID" \
--output-dir build/sign --keychain "$RUNNER_TEMP/signing.keychain-db"
hexsign profiles download \
--bundle-id "$HEXSIGN_BUNDLE_ID" --team-id "$HEXSIGN_TEAM_ID" \
--output-dir build/sign --install--team-id is required when downloading certificates by type (so you never pull certs across multiple linked Apple accounts), and optional but recommended on profiles download --bundle-id for the same reason.
After the build
Most CI runners give every job a fresh, isolated VM, so the keychain is destroyed when the job ends. If you're on long-lived runners, delete the keychain explicitly at the end of the step so the next job doesn't inherit it.
security delete-keychain "$RUNNER_TEMP/signing.keychain-db" rm -rf build/sign
Why not commit the .p12 to the repo?
- Anyone with read access to the repo gets the private key forever.
- Rotating the certificate means rotating the file in every branch and fork that mirrors the repo.
- An incident response ("who had access to the cert and when") relies on git history, not on a managed audit log.
- Fetching by ID with the CLI gives you a per-call audit entry and instant revocation.