Vorssaint-Utils Release Process: A Step-by-Step Guide to Automated macOS Distribution
Vorssaint-utils uses a fully automated GitHub Actions pipeline that triggers on version tags, performs cryptographic signing and Apple notarization, and publishes immutable releases without manual intervention.
The vorssaint-utils repository implements a security-hardened, reproducible release workflow designed for macOS command-line tools. Every release undergoes strict validation, code signing with Apple Developer ID credentials, and notarization before distribution. Understanding this process helps contributors prepare releases correctly and organizations adapt similar patterns for their own Swift-based utilities.
How the Release Pipeline Triggers
The entire workflow activates when a maintainer pushes a **semantic version tag matching v[0-9]*** to the main branch. This single action kicks off three sequential jobs defined in .github/workflows/release.yml.
The tag must correspond to:
- A GPG-verified commit on
main - The
CFBundleShortVersionStringvalue inResources/Info.plist - A dated entry in
CHANGELOG.md
Phase 1: Tag Validation and Preflight Checks
The preflight job enforces release readiness before any build artifacts are created.
Semantic Version Verification
The workflow validates tags against strict patterns at lines 38-66 of .github/workflows/release.yml:
- Structure:
vX.Y.Zwith optional prerelease suffix (e.g.,v3.2.0-beta.1) - Branch alignment: The tagged commit must be on
main(checked viagit merge-base --is-ancestorat lines 52-56) - GPG signature: Commit verification runs at lines 45-50
Version Consistency Checks
Two additional safeguards prevent version drift:
- Info.plist match (lines 58-63): The tag version is compared against
CFBundleShortVersionStringin the plist file - Changelog requirement (lines 64-67): A corresponding dated entry must exist in
CHANGELOG.md
Test Execution
Unit tests execute via ./build.sh --test (lines 69-71), blocking the release if any test in Tests/MetricsTests.swift or related suites fails.
Phase 2: Build, Code Signing, and Notarization
Upon passing preflight, the release job performs the actual artifact construction on a macOS-26 runner.
Building the Binary Bundle
The build.sh script (.github/workflows/release.yml lines 72-84, build.sh lines 5-65) handles compilation:
# Build release-ready bundle locally
./build.sh
# With unit tests (mirrors CI behavior)
./build.sh --test
The script compiles Swift sources, assembles the .app bundle structure, and applies codesigning with timestamp retry logic for resilience against transient Apple service issues.
Signing Key Preparation
Apple Developer ID credentials enter the pipeline through Tools/ci-setup-signing.sh:
- Injects
SIGNING_CERT_P12andSIGNING_CERT_PASSWORDfrom the protectedrelease-signingenvironment - Configures the signing identity for subsequent
codesigninvocations
These secrets are isolated to the release job only and never exposed to other workflow steps or logs.
Apple Notarization
The Tools/notarize.sh script submits the signed binary to Apple's notary service:
# Manual notarization for debugging
./Tools/notarize.sh build/stage/Vorssaint.app
Required notarization credentials (NOTARY_API_KEY_P8, NOTARY_KEY_ID, NOTARY_ISSUER_ID) are also restricted to the release-signing environment. The script polls Apple's service until notarization succeeds or fails definitively.
DMG Creation and Final Signing
Tools/make-dmg.sh produces the distributable disk image (lines 44-50), which is then:
- Signed with the same Developer ID identity
- Submitted for secondary notarization as a standalone artifact
This dual-notarization (app + DMG) ensures seamless gatekeeper compliance regardless of extraction method.
Phase 3: Immutable Release Publication
The final publish job creates a non-modifiable GitHub release.
Release Metadata Generation
The workflow derives release properties (release.yml lines 98-118):
- Title and body extracted from
CHANGELOG.md prereleaseflag set based on tag suffix detection- Initial
draftstatus for final human review (optional)
Asset Integrity Verification
Before promotion, SHA-256 digests are validated against GitHub's stored values (lines 11-22), preventing corruption or tampering during upload.
Immutability Enforcement
The release is marked with isImmutable=true (lines 86-95), guaranteeing that:
- Assets cannot be replaced post-publication
- Checksums remain permanently verifiable
- Downstream consumers can pin to cryptographic hashes
Key Files in the Release System
| File | Function |
|---|---|
.github/workflows/release.yml |
Orchestrates validation, build, signing, notarization, and publishing |
build.sh |
Compiles Swift sources and performs conditional signing |
Tools/ci-setup-signing.sh |
Prepares Apple Developer ID from encrypted secrets |
Tools/notarize.sh |
Interfaces with Apple notary service API |
Tools/make-dmg.sh |
Constructs signed, notarized disk images |
Resources/Info.plist |
Source of truth for bundle version |
CHANGELOG.md |
Release notes source for GitHub releases |
Creating a Release as a Maintainer
Follow this exact sequence to trigger a production release:
# 1. Update version metadata
# - Edit CFBundleShortVersionString in Resources/Info.plist
# - Add dated entry to CHANGELOG.md
git commit -am "Release v3.2.0"
# 2. Create and push annotated tag
git tag v3.2.0
git push origin v3.2.0 # Triggers the full pipeline
The tag push immediately initiates validation. Successful completion produces a signed, notarized DMG attached to an immutable GitHub release within approximately 10-15 minutes.
Local Development and Debugging
For testing without CI infrastructure:
# Ad-hoc build (no signing required)
./build.sh
# Generate unsigned DMG for internal testing
./Tools/make-dmg.sh
# Full signing simulation (requires valid Developer ID certificate)
./Tools/ci-setup-signing.sh
./build.sh
./Tools/notarize.sh build/stage/Vorssaint.app
Summary
- Trigger mechanism: Push semantic version tag (
vX.Y.Z) tomain - Validation layer: GPG verification, version alignment, changelog presence, unit test passage
- Security controls: Developer ID signing, Apple notarization, secret isolation via
release-signingenvironment - Output: Immutable GitHub release with signed, notarized DMG and verified checksums
- Key scripts:
build.sh,Tools/ci-setup-signing.sh,Tools/notarize.sh,Tools/make-dmg.sh
Frequently Asked Questions
How do I prepare a beta or prerelease version?
Use a prerelease suffix in your tag (e.g., v3.2.0-beta.1). The workflow detects this pattern and automatically sets the GitHub release's prerelease flag to true. Ensure your CHANGELOG.md entry and Info.plist version match exactly, including the suffix.
What happens if notarization fails?
The publish job will not execute. The workflow logs contain the notarization service response from Tools/notarize.sh. Common failures include expired credentials, malformed API keys, or binary issues detected by Apple's automated analysis. Fix the underlying issue, delete the failed tag, and re-push after correction.
Can I build and sign locally without CI?
Yes. With a valid Apple Developer ID certificate installed, run ./Tools/ci-setup-signing.sh to configure your local keychain, then ./build.sh and ./Tools/notarize.sh with your app path. Note that local builds use your personal Developer ID rather than the CI organization's certificate, producing different signatures.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →