# Vorssaint-Utils Release Process: A Step-by-Step Guide to Automated macOS Distribution

> Automate your macOS distribution with the vorssaint-utils release process. Discover how GitHub Actions handles signing and notarization for immutable releases.

- Repository: [vorssaint/vorssaint-utils](https://github.com/vorssaint/vorssaint-utils)
- Tags: how-to-guide
- Published: 2026-09-05

---

**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`](https://github.com/vorssaint/vorssaint-utils/blob/main/.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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/.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`](https://github.com/vorssaint/vorssaint-utils/blob/main/CHANGELOG.md)

### Test Execution

Unit tests execute via `./build.sh --test` (lines 69-71), blocking the release if any test in [`Tests/MetricsTests.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/build.sh) script ([`.github/workflows/release.yml`](https://github.com/vorssaint/vorssaint-utils/blob/main/.github/workflows/release.yml) lines 72-84, [`build.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/build.sh) lines 5-65) handles compilation:

```bash

# 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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/Tools/notarize.sh) script submits the signed binary to Apple's notary service:

```bash

# 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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/release.yml) lines 98-118):
- Title and body extracted from [`CHANGELOG.md`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/.github/workflows/release.yml) | Orchestrates validation, build, signing, notarization, and publishing |
| [`build.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/build.sh) | Compiles Swift sources and performs conditional signing |
| [`Tools/ci-setup-signing.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/Tools/ci-setup-signing.sh) | Prepares Apple Developer ID from encrypted secrets |
| [`Tools/notarize.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/Tools/notarize.sh) | Interfaces with Apple notary service API |
| [`Tools/make-dmg.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/Tools/make-dmg.sh) | Constructs signed, notarized disk images |
| `Resources/Info.plist` | Source of truth for bundle version |
| [`CHANGELOG.md`](https://github.com/vorssaint/vorssaint-utils/blob/main/CHANGELOG.md) | Release notes source for GitHub releases |

---

## Creating a Release as a Maintainer

Follow this exact sequence to trigger a production release:

```bash

# 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:

```bash

# 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`](https://github.com/vorssaint/vorssaint-utils/blob/main/build.sh), [`Tools/ci-setup-signing.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/Tools/ci-setup-signing.sh), [`Tools/notarize.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/Tools/notarize.sh), [`Tools/make-dmg.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/./Tools/ci-setup-signing.sh) to configure your local keychain, then [`./build.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/./build.sh) and [`./Tools/notarize.sh`](https://github.com/vorssaint/vorssaint-utils/blob/main/./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.