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 update a cask in a separate Homebrew tap after a release asset exists. The workflow resolves the release asset, downloads it, calculates the real SHA256, updates the cask file, runs Homebrew validation, and opens a PR to the tap repository by default.
name: Update Homebrew Tap
on:
release:
types: [published]
workflow_dispatch:
inputs:
release-tag:
type: string
required: true
jobs:
update-homebrew:
uses: vuon9/gh-workflows/.github/workflows/homebrew-cask-update.yml@v0.2.0
with:
tap-repository: owner/homebrew-tap
cask-token: myapp
release-tag: ${{ github.event.release.tag_name || inputs.release-tag }}
artifact-name-regex: "MyApp-.*\\.dmg$"
open-pull-request: true
secrets:
TAP_REPO_TOKEN: ${{ secrets.TAP_REPO_TOKEN }}Required caller secret:
TAP_REPO_TOKEN: fine-grained token or GitHub App token with write access to the tap repository. The caller repositoryGITHUB_TOKENis not enough when the tap lives in a different repository.
Useful inputs:
tap-repository: tap repo such asowner/homebrew-tap.cask-token: cask token such asmyapp.cask-path: cask path inside the tap repo. Defaults toCasks/<cask-token>.rb.source-repository: release repo. Defaults to the caller repository.release-tag: release tag. Defaults to the caller ref name.artifact-name-regex: must match exactly one release asset whenartifact-urlis omitted.artifact-url: explicit release artifact URL when the caller already knows it.version: cask version. Defaults to the release tag basename with a leadingvremoved.open-pull-request: defaults totrue. Set tofalseonly when direct tap pushes are acceptable.dry-run: updates and validates locally without pushing.
Caller repositories can sign, notarize, staple, package, and publish a Developer ID
macOS DMG after an app-specific build job uploads a .app bundle archive.
name: macOS Release
on:
push:
tags:
- 'macos/myapp/v*'
workflow_dispatch:
jobs:
build:
runs-on: macos-26
steps:
- uses: actions/checkout@v6
- run: task darwin:package:universal
- run: tar -czf "$RUNNER_TEMP/macos-app.tar.gz" -C bin MyApp.app
- uses: actions/upload-artifact@v7.0.1
with:
name: myapp-macos-app-${{ github.run_id }}
path: ${{ runner.temp }}/macos-app.tar.gz
release:
needs: build
uses: vuon9/gh-workflows/.github/workflows/macos-release.yml@v0.2.0
with:
app-name: MyApp
team-id: ABCDE12345
app-path: bin/MyApp.app
app-artifact-name: myapp-macos-app-${{ github.run_id }}
dmg-name: MyApp-macos-universal.dmg
artifact-name: myapp-macos-release-${{ github.run_id }}
runner-label: macos-26
github-release-prerelease: ${{ contains(github.ref_name, '-') }}
secrets: inheritRequired caller secrets:
APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_BASE64: base64-encoded Developer ID Application.p12.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD:.p12password.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 secret:
MACOS_CODESIGN_IDENTITY: fullDeveloper ID Application: ... (TEAMID)identity. If omitted, the workflow finds the imported Developer ID Application identity forteam-id.
What the workflow does:
- Downloads an app bundle archive uploaded by a caller-owned build job.
- Installs Go and
create-dmg. - Imports the Developer ID Application certificate into a temporary keychain.
- Signs the app with hardened runtime and timestamping.
- Notarizes and staples the app.
- Creates, signs, notarizes, staples, and verifies the DMG.
- Uploads the DMG as a GitHub Actions artifact and, on tags, a GitHub Release asset.
Set
github-release-prerelease: truefor prerelease tags such asv1.0.0-rc.1.
Recommended caller controls:
- Use a product/platform tag namespace such as
macos/myapp/v1.0.0. - Reference a version tag after the workflow is released, not
@main. - Keep Apple secrets in the app repository or a protected GitHub Environment.
- Keep app-specific build systems such as Wails, Xcode, Electron, or custom scripts in the caller repository.
- Run the first release manually with
workflow_dispatchbefore cutting the public tag. - Do final Gatekeeper verification by downloading the uploaded DMG on a clean macOS machine.
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.