Reusable GitHub Actions workflows and release tooling.
The goal is to keep product repositories clean: app repos keep small workflow wrappers and app-specific config, while common CI/release behavior lives here.
Caller repositories can trigger TestFlight upload with a thin wrapper:
name: TestFlight
on:
workflow_dispatch:
jobs:
testflight:
uses: vuon9/gh-workflows/.github/workflows/ios-testflight.yml@v0.1.5
with:
project-path: MyApp.xcodeproj
scheme: MyApp
team-id: ABCDE12345
dry-run: true
skip-cert-check: false
runner-label: macos-26
secrets: inheritFor workspace-based apps, use workspace-path instead of project-path.
Required caller secrets when dry-run is false:
APP_STORE_CONNECT_API_KEY_P8: App Store Connect API private key content.APP_STORE_CONNECT_API_KEY_ID: App Store Connect API key ID.APP_STORE_CONNECT_API_ISSUER_ID: App Store Connect issuer ID.
Optional caller secrets when signing-style: manual:
APPLE_DISTRIBUTION_CERTIFICATE_P12_BASE64: base64-encoded Apple Distribution.p12.APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD:.p12password.APPLE_PROVISIONING_PROFILE_BASE64: base64-encoded App Store provisioning profile.
Use manual signing when a clean GitHub-hosted macOS runner should sign without asking Xcode to create certificates or profiles.
- Export an Apple Distribution certificate from a trusted Mac:
security find-identity -v -p codesigningPick the Apple Distribution: ... (TEAMID) identity, then export it to a temporary .p12. This may show a macOS Keychain approval prompt:
P12_PASSWORD="$(openssl rand -base64 24)"
security export \
-k "$HOME/Library/Keychains/login.keychain-db" \
-t identities \
-f pkcs12 \
-o /tmp/apple-distribution.p12 \
-P "$P12_PASSWORD"
base64 -i /tmp/apple-distribution.p12 -o /tmp/apple-distribution.p12.b64- Base64-encode the App Store provisioning profile for the app:
base64 -i "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/<UUID>.mobileprovision" \
-o /tmp/app-store-profile.mobileprovision.b64- Set repository secrets:
gh secret set APPLE_DISTRIBUTION_CERTIFICATE_P12_BASE64 \
--repo owner/app-repo \
< /tmp/apple-distribution.p12.b64
printf '%s' "$P12_PASSWORD" | gh secret set APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD \
--repo owner/app-repo
gh secret set APPLE_PROVISIONING_PROFILE_BASE64 \
--repo owner/app-repo \
< /tmp/app-store-profile.mobileprovision.b64- Delete local temporary signing files:
rm -f /tmp/apple-distribution.p12 /tmp/apple-distribution.p12.b64 /tmp/app-store-profile.mobileprovision.b64
unset P12_PASSWORDNever commit .p12, .mobileprovision, .p8, archives, IPAs, or base64 secret files.
Useful inputs:
dry-run: print commands without running archive/export.skip-tests: skip simulator tests before archive.skip-archive: skip archive and export an archive downloaded from a previous release artifact.export-destination:uploadfor TestFlight/App Store Connect upload,exportfor a reusable IPA/archive artifact.upload-artifact-name: uploadbuild/TestFlightas a GitHub Actions artifact after archive/export.download-artifact-name: download a priorbuild/TestFlightartifact before export.skip-cert-check: skip the local Apple Distribution identity preflight check. This is useful when testing whetherxcodebuild -allowProvisioningUpdatescan handle signing on a clean GitHub-hosted macOS runner.runner-label: runner used for the release job. Defaults tomacos-26because App Store Connect requires the iOS 26 SDK or newer for uploads.signing-style: defaults toautomatic; usemanualwithbundle-idandprovisioning-profilewhen running on clean hosted runners.bundle-id/provisioning-profile: required with manual signing so archive/export can use an installed profile instead of creating signing assets.
Recommended caller controls:
- Protect the workflow with a GitHub Environment such as
testflight. - Use a tag-triggered workflow named
Releaseto build and export the release artifact. - Gate the dependent TestFlight upload through a regular approval job with a GitHub Environment such as
testflight; configure required reviewers on that environment so the upload is actually manual. - Reference a version tag such as
@v0.1.3for pilots or@v1after the workflow is proven, not@main.
Recommended release wrapper shape:
name: Release
on:
push:
tags:
- 'ios/myapp/v*'
jobs:
release:
uses: vuon9/gh-workflows/.github/workflows/ios-testflight.yml@v0.1.6
with:
project-path: MyApp.xcodeproj
scheme: MyApp
team-id: ABCDE12345
export-destination: export
upload-artifact-name: myapp-ios-release-${{ github.run_id }}
runner-label: macos-26
secrets: inherit
approve-testflight:
needs: release
runs-on: ubuntu-latest
environment: testflight
steps:
- run: echo "TestFlight upload approved for $GITHUB_REF_NAME"
testflight:
needs: approve-testflight
uses: vuon9/gh-workflows/.github/workflows/ios-testflight.yml@v0.1.6
with:
project-path: MyApp.xcodeproj
scheme: MyApp
team-id: ABCDE12345
skip-tests: true
skip-archive: true
archive-path: build/TestFlight/MyApp.xcarchive
download-artifact-name: myapp-ios-release-${{ github.run_id }}
runner-label: macos-26
secrets: inheritRun the Go tests:
go test ./...Build the iOS TestFlight CLI:
go build ./ios/testflight/cmd/ios-testflightExample dry run from an iOS app repository:
go run /path/to/gh-workflows/ios/testflight/cmd/ios-testflight \
--project MyApp.xcodeproj \
--scheme MyApp \
--team-id ABCDE12345 \
--skip-tests \
--dry-runact can catch basic workflow wiring problems before pushing:
act workflow_dispatch \
--validate \
-W .github/workflows/testflight.yml \
--input dry-run=true \
--input skip-tests=trueIt can also dry-run the job graph:
act workflow_dispatch \
--dryrun \
-W .github/workflows/testflight.yml \
--input dry-run=true \
--input skip-tests=trueUse act as a preflight only. iOS release workflows still need a real GitHub-hosted macOS runner to verify Xcode, signing, Keychain behavior, and App Store Connect upload.
Use the pilot tag while validating the first app migration:
uses: vuon9/gh-workflows/.github/workflows/ios-testflight.yml@v0.1.5After one real TestFlight upload succeeds, create a major tag for the stable workflow contract:
git tag v1
git push origin v1Breaking changes should use a new major tag. Additive inputs can stay under the current major tag after testing.