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 CFBundleShortVersionString value in Resources/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.Z with optional prerelease suffix (e.g., v3.2.0-beta.1)
  • Branch alignment: The tagged commit must be on main (checked via git merge-base --is-ancestor at lines 52-56)
  • GPG signature: Commit verification runs at lines 45-50

Version Consistency Checks

Two additional safeguards prevent version drift:

  1. Info.plist match (lines 58-63): The tag version is compared against CFBundleShortVersionString in the plist file
  2. 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_P12 and SIGNING_CERT_PASSWORD from the protected release-signing environment
  • Configures the signing identity for subsequent codesign invocations

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
  • prerelease flag set based on tag suffix detection
  • Initial draft status 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) to main
  • Validation layer: GPG verification, version alignment, changelog presence, unit test passage
  • Security controls: Developer ID signing, Apple notarization, secret isolation via release-signing environment
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →