Reusable GitHub Actions workflows and composite actions for the macOS and iOS app repositories across my privacykey and AdamXweb accounts. One pipeline, one place to fix it.
Warning
Project status. The badge above is generated from the privacykey status list, which says what I promise for this project and every other one.
Nothing here is installed or run on its own. Other repositories call into it, so quiet is the intended state: a shared workflow nobody has touched in months is one that is still doing its job. Changes land here only when a consumer needs something new, or when a pin needs bumping.
| Path | What it is |
|---|---|
.github/workflows/macos-sparkle-release.yml |
Tag-triggered macOS release: test gate → sign → notarize → DMG → appcast → GitHub Release → optional Homebrew cask PR against the tap |
.github/workflows/macos-app-ci.yml |
Push/PR CI for macOS apps: unsigned build + tests, zero secrets |
.github/workflows/ios-release.yml |
iOS archive/TestFlight: unsigned validate gate → signed archive → .ipa + dSYM artefacts → optional altool upload; two signing modes (self-hosted runner keychain, or hosted cert import) |
.github/workflows/project-status.yml |
Regenerates one account's status badges, STATUS.md and profile section from its own status.json, then reports what has drifted |
actions/assert-trusted-runner |
Fail-closed guard: on a self-hosted runner, refuses to continue unless the repository is private and the pull request is not from a fork |
actions/setup-apple-keychain |
Ephemeral keychain + certificate import (-T codesign, set-key-partition-list, masked password) |
actions/write-asc-api-key |
Stages the App Store Connect .p8 as a mode-600 file |
actions/install-sparkle-cli |
Pinned Sparkle release tarball with SHA-256 verification |
actions/publish-gh-pages-file |
Worktree-based single-file publish to a branch (appcast.xml → gh-pages) |
actions/project-status |
The Node scripts and canonical tier definitions behind project-status.yml |
Six repositories call in today — that is a code search across the privacykey, adamXbot and AdamXweb accounts, public and private repositories both. The two public ones:
| Repository | Uses |
|---|---|
| privacykey/privacycommand | macos-sparkle-release.yml@v1, macos-app-ci.yml@v1 |
| privacykey/.github | project-status.yml@v1 |
The other four are private and are not linked here. Between them they use
ios-release.yml@v1 (one repo) and assert-trusted-runner@v1 (all four —
across nine workflows in total, since one repo calls the guard from six).
A caller is thin by design — the pipeline lives here, the build script and
the version number stay in the consumer repo. A macOS release.yml is about
fifteen lines:
name: Release
on:
push:
tags: ['v*']
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
jobs:
release:
uses: privacykey/gh-workflows/.github/workflows/macos-sparkle-release.yml@v1
with:
xcodeproj: MyApp/MyApp.xcodeproj
scheme: MyApp
uses_xcodegen: true
secrets: inheritsecrets: inherit is not optional advice when SPARKLE_PRIVATE_KEY lives in
the macos-signing environment: explicitly mapped secrets resolve in the
caller's context, which has no environment, and come back empty.
-
macOS — inputs, the release-script env contract, the Homebrew cask PR flow, the gen-1 → gen-3 secret rename and the migration sequence: docs/macos-release.md.
-
iOS — the two signing modes, fastlane mode, secrets by mode, inputs: docs/ios-release.md.
-
Self-hosted runners — how to opt in, and the private-repositories-only rule the workflows enforce: docs/self-hosted-runners.md.
-
Project status — called from an account's own hub repo, with a read-only token scoped to that account:
jobs: status: uses: privacykey/gh-workflows/.github/workflows/project-status.yml@v1 secrets: status-token: ${{ secrets.STATUS_TOKEN }}
- Consumers pin by tag.
...@v1is the moving major tag and is what every consumer above uses. Exact tagsv1.0.0,v1.0.1,v1.0.2,v1.1.0andv1.2.0also exist. There are no GitHub Releases — the tags are the whole contract. v1is moved deliberately, as the release action. It currently points at the same commit asv1.2.0.- An exact pin is not a full freeze. Inside these workflows the composite
actions are referenced as
privacykey/gh-workflows/actions/<name>@v1, and GitHub resolves those refs at run time independently of the tag the workflow was pinned at. A caller onv1.1.0therefore gets that workflow, but whateverv1currently points at for the actions it calls. A relative path cannot be used instead, because the job's checkout is the consumer repo, not this one. - Third-party actions are SHA-pinned with a version comment — currently
actions/checkout,actions/upload-artifact,maxim-lobanov/setup-xcode,softprops/action-gh-releaseandruby/setup-ruby. The digests live in the workflow files rather than being repeated here, so they cannot drift out of sync with what actually runs.project-status.ymlis the exception: it runs onubuntu-latestand uses floatingactions/checkout@v4/actions/setup-node@v4. - The Sparkle CLI is pinned to version
2.9.5, and its tarball digest is verified before extraction. Bump thesparkle_versionandsparkle_sha256inputs together or the check fails, by design.
The blast radius of an edit here is every consumer above, and it lands the
moment v1 moves — not when main moves. Pushing to main changes nothing
for consumers, because both their pins and this repo's internal action
references resolve through tags. So merge to main freely, then move v1 as
a separate, deliberate act once you are satisfied. Before that:
- Run
just lint—actionlintover the workflows plus a YAML parse of everyactions/*/action.yml.actionlinthas to be installed locally, because there is no CI in this repository: every workflow here isworkflow_call-only, so none of them can run on a push. - Removing or renaming an input is a breaking change for a caller pinned at
@v1, and it will break at run time rather than at merge time. The deprecatedmacos_runnerinput is the pattern to copy: keep the old name working, document the replacement, migrate callers, remove later. - Adding a
secrets:entry tomacos-app-ci.ymlis out of bounds. It declares none and must never gain any — that is what makes it safe to run on pull requests. - Design rationale for the current shape: docs/design-decisions.md.