Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3581978
Drain subprocess pipes concurrently to prevent large-output deadlock
rodchristiansen Jul 18, 2026
2d26cf1
Fail the build when notarization times out
rodchristiansen Jul 18, 2026
4b8fb62
Return distinct exit codes per failure class
rodchristiansen Jul 18, 2026
592b14a
Add --pkg-version and --output-dir build overrides
rodchristiansen Jul 18, 2026
fdc4295
Resolve dynamic version tokens in build-info
rodchristiansen Jul 18, 2026
3c5b33e
Reject package names that escape the build directory
rodchristiansen Jul 18, 2026
1ac3570
Expand ${HOME} and tilde in build-info keychain paths
rodchristiansen Jul 20, 2026
85ed93a
Accept --skip-import for munki-pkg compatibility
rodchristiansen Jul 20, 2026
379f1b3
Allow receipt-only projects with no payload and no scripts
rodchristiansen Jul 20, 2026
adb06b8
Defer notarization_info validation until notarization runs
rodchristiansen Jul 20, 2026
2ac58fc
Append .pkg to the package name when it lacks the extension
rodchristiansen Jul 20, 2026
8db6686
Add a GitHub composite action for building packages
rodchristiansen Jul 18, 2026
15821e9
Add --verify to check the built package against build-info
rodchristiansen Jul 18, 2026
83838ec
Add --provenance attestation sidecar
rodchristiansen Jul 18, 2026
f4016c9
Add --output-format json build manifest
rodchristiansen Jul 18, 2026
90b7eb1
Add an Azure DevOps steps template for building packages
rodchristiansen Jul 18, 2026
73b61a2
Add .env build-time variable substitution for scripts
rodchristiansen Jul 18, 2026
7e21571
Add --lint to validate a project without building
rodchristiansen Jul 18, 2026
ff120c3
Route notarization timeout and raw helper failures through typed exit…
rodchristiansen Jul 23, 2026
620b918
Add hermetic receipt-only build test
rodchristiansen Jul 23, 2026
6448b87
Verify package identifier and version, and make verifier tests hermetic
rodchristiansen Jul 24, 2026
d20096f
Include symlinks and file mode bits in the provenance input digest
rodchristiansen Jul 24, 2026
30c31d6
Harden lint: validate reverse-DNS components and reject script direct…
rodchristiansen Jul 24, 2026
4d651f9
Report notarization accuracy in the manifest and harden the verify gate
rodchristiansen Jul 24, 2026
2a733cc
Harden the CI templates: verify installer, unique download dir, safe …
rodchristiansen Jul 24, 2026
5cae3f1
Fail verification on package expansion errors and incomplete metadata
rodchristiansen Jul 24, 2026
0363414
Surface copy failures for non-script files in the .env override
rodchristiansen Jul 24, 2026
b71968a
Merge pull request #19 from rodchristiansen/feat/process-exit-correct…
jordancalhoun Jul 25, 2026
84a8c32
Merge pull request #20 from rodchristiansen/feat/version-resolution
jordancalhoun Jul 26, 2026
568e647
Merge next into feat/munkipkg-compat
jordancalhoun Jul 26, 2026
1dc103d
Merge pull request #21 from rodchristiansen/feat/munkipkg-compat
jordancalhoun Jul 26, 2026
a7ba357
Do not report shell-owned names as unresolved placeholders
rodchristiansen Jul 27, 2026
0a51b98
Report the variables a build applied, not the ones it loaded
rodchristiansen Jul 28, 2026
2ca3f5a
Carry a failing tool's own explanation into the error
rodchristiansen Jul 27, 2026
00b1754
Install a named, checksummed, publisher-signed release
rodchristiansen Jul 28, 2026
deb2915
Lint the bad-script case without a payload
rodchristiansen Jul 28, 2026
4c7bb9d
Merge next into feat/json-manifest
jordancalhoun Aug 2, 2026
c7f47ee
Merge pull request #22 from rodchristiansen/feat/json-manifest
jordancalhoun Aug 2, 2026
a85e3a8
Merge next into feat/lint
jordancalhoun Aug 2, 2026
af1fb2b
Merge pull request #23 from rodchristiansen/feat/lint
jordancalhoun Aug 2, 2026
24e2222
Merge next into feat/verify
jordancalhoun Aug 2, 2026
ce83959
Merge pull request #24 from rodchristiansen/feat/verify
jordancalhoun Aug 2, 2026
2401895
Merge next into feat/provenance
jordancalhoun Aug 2, 2026
4712d87
Merge pull request #25 from rodchristiansen/feat/provenance
jordancalhoun Aug 2, 2026
cc6160b
Merge next into feat/env-substitution
jordancalhoun Aug 2, 2026
a1976bb
Merge pull request #26 from rodchristiansen/feat/env-substitution
jordancalhoun Aug 2, 2026
999dc5a
Merge remote-tracking branch 'origin/next' into resolve-pr-27
jordancalhoun Aug 2, 2026
e64b034
Merge pull request #27 from rodchristiansen/feat/ci-templates
jordancalhoun Aug 2, 2026
a96ed42
Merge remote-tracking branch 'origin/next' into resolve-pr-47
jordancalhoun Aug 2, 2026
8b6bb21
Merge pull request #47 from rodchristiansen/fix/notarytool-diagnostics
jordancalhoun Aug 2, 2026
59d720f
Do not report environment-supplied names as unresolved placeholders
rodchristiansen Aug 4, 2026
a2b27f6
Default new projects to an org.swiftpkg identifier
rodchristiansen Aug 4, 2026
121f3ae
Merge pull request #49 from rodchristiansen/fix/default-identifier-br…
jordancalhoun Aug 5, 2026
d976038
Merge pull request #48 from rodchristiansen/fix/env-placeholder-envir…
jordancalhoun Aug 5, 2026
f565e04
update types from legacy Munki to SwiftPkg
jordancalhoun Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,42 @@ Only the trusted signing workflow creates the immutable GitHub Release. See
[VERIFICATION.md](VERIFICATION.md), [CONTRIBUTING.md](CONTRIBUTING.md), and
[SECURITY.md](SECURITY.md) for project processes.

## GitHub Action

`action.yml` is a composite action so any repository can build a package on a
macOS runner without hand-rolling install-and-invoke. It installs the swiftpkg
release, optionally lints, builds with `--output-format json`, and exposes the
result as step outputs.

```yaml
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- id: pkg
uses: codecarton/swiftpkg@v1
with:
project-path: packages/my-project
version: ${{ github.ref_name }}
lint: true
verify: true
- run: echo "Built ${{ steps.pkg.outputs.pkg-path }} (${{ steps.pkg.outputs.sha256 }})"
```

Inputs: `project-path` (required), `version` (→ `--pkg-version`), `output-dir`,
`swiftpkg-version`, `swiftpkg-sha256`, `expected-team-id`, `lint`, `verify`,
`provenance`, `extra-args`. Outputs: `pkg-path`, `version`, `sha256`. Requires a
swiftpkg release that includes the CI flags (`--output-format`, `--output-dir`,
`--pkg-version`, `--lint`, `--verify`, `--provenance`).

The action installs a release package as root, so it checks what it downloaded
first: the asset must match the release's `SHA256SUMS` and must be signed by the
`expected-team-id` Developer Team, and `spctl` must accept it. `swiftpkg-version`
defaults to a pinned tag rather than `latest`. GitHub release assets can be
replaced without moving the tag, so a build that must be reproducible byte for
byte should also set `swiftpkg-sha256` to the checksum it expects.

## Marketing site

The static marketing site lives in [`site/`](site/) and publishes to
Expand Down
2 changes: 1 addition & 1 deletion Swiftpkgr/State/ProjectEditorModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ final class ProjectEditorModel {

private func saveDraft() throws {
guard let projectURL, let document = buildInfoDocument else {
throw MunkiPkgError.message("No project is open.")
throw SwiftPkgError.message("No project is open.")
}
let configuration = try draft.validatedConfiguration()
try BuildInfoStore.write(configuration, to: projectURL, format: document.format)
Expand Down
169 changes: 169 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
name: 'swiftpkg build'
description: 'Build an Apple installer package from a swiftpkg project directory'
author: 'codecarton'

inputs:
project-path:
description: 'Path to the swiftpkg package project directory'
required: true
version:
description: 'Override the build-info version (e.g. a git tag). Maps to --pkg-version.'
required: false
default: ''
output-dir:
description: 'Directory to write the built package into. Maps to --output-dir.'
required: false
default: 'dist'
swiftpkg-version:
description: 'Release tag of swiftpkg to install. "latest" is accepted but makes builds depend on whatever ships next.'
required: false
default: 'v0.3.1'
swiftpkg-sha256:
description: 'Expected SHA-256 of the installer asset. Set it to pin the exact bytes; release assets are mutable, so a tag alone does not.'
required: false
default: ''
expected-team-id:
description: 'Apple Developer Team ID the installer must be signed by.'
required: false
default: 'DPXY7JLK67'
lint:
description: 'If "true", run `swiftpkg --lint` before building and fail on lint errors.'
required: false
default: 'false'
verify:
description: 'If "true", pass --verify so a signed/notarized build is checked after building.'
required: false
default: 'false'
provenance:
description: 'If "true", pass --provenance to write a <pkg>.provenance.json sidecar.'
required: false
default: 'false'
extra-args:
description: 'Additional arguments appended to the swiftpkg build invocation.'
required: false
default: ''

outputs:
pkg-path:
description: 'Path to the built package'
value: ${{ steps.build.outputs.pkg-path }}
version:
description: 'Version of the built package'
value: ${{ steps.build.outputs.version }}
sha256:
description: 'SHA-256 of the built package'
value: ${{ steps.build.outputs.sha256 }}

runs:
using: composite
steps:
- name: Install swiftpkg
shell: bash
env:
SWIFTPKG_VERSION: ${{ inputs.swiftpkg-version }}
SWIFTPKG_SHA256: ${{ inputs.swiftpkg-sha256 }}
EXPECTED_TEAM_ID: ${{ inputs.expected-team-id }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
runner_tmp="${RUNNER_TEMP:-/tmp}"
# Download into a fresh directory so a stale or unexpected package left
# in the runner temp can't be picked up.
download_dir="$(mktemp -d "$runner_tmp/swiftpkg-install.XXXXXX")"
# A release publishes swiftpkg-<version>-cli.pkg (the CLI alone),
# swiftpkg-<version>-combined.pkg (CLI + Swiftpkgr), and SHA256SUMS. CI
# wants the CLI. The version is embedded in the filename, so match it
# with a pattern rather than a fixed URL.
args=(--repo codecarton/swiftpkg --pattern 'swiftpkg-*-cli.pkg' --pattern 'SHA256SUMS' --dir "$download_dir")
if [ "$SWIFTPKG_VERSION" = "latest" ]; then
gh release download "${args[@]}"
else
gh release download "$SWIFTPKG_VERSION" "${args[@]}"
fi
# Require exactly one matching asset, so an ambiguous release can't cause
# a surprising package to be installed as root.
shopt -s nullglob
pkgs=("$download_dir"/swiftpkg-*-cli.pkg)
if [ "${#pkgs[@]}" -ne 1 ]; then
echo "Expected exactly one swiftpkg installer, found ${#pkgs[@]}: ${pkgs[*]:-none}" >&2
exit 1
fi
pkg="${pkgs[0]}"

# Three checks, each covering what the others cannot. Only the caller's
# own swiftpkg-sha256 pins the bytes against a release asset being
# replaced in place; SHA256SUMS ships from the same release, so it
# catches a truncated or corrupted download but moves with the release;
# and the signature is what makes a substituted asset unusable, since
# forging it requires the publisher's Developer ID certificate.
actual="$(shasum -a 256 "$pkg" | awk '{print $1}')"
if [ -n "$SWIFTPKG_SHA256" ] && [ "$actual" != "$SWIFTPKG_SHA256" ]; then
echo "Installer SHA-256 does not match swiftpkg-sha256: expected $SWIFTPKG_SHA256, got $actual" >&2
exit 1
fi
published="$(awk -v name="$(basename "$pkg")" '$2 == name { print $1 }' "$download_dir/SHA256SUMS")"
if [ -z "$published" ]; then
echo "SHA256SUMS has no entry for $(basename "$pkg")" >&2
exit 1
fi
if [ "$actual" != "$published" ]; then
echo "Installer SHA-256 does not match SHA256SUMS: expected $published, got $actual" >&2
exit 1
fi

# Assess before running the installer as root. spctl establishes that
# Apple notarized it; the Team ID establishes who signed it, which
# notarization alone does not.
signature="$(pkgutil --check-signature "$pkg")"
printf '%s\n' "$signature"
case "$signature" in
*"($EXPECTED_TEAM_ID)"*) ;;
*) echo "Installer is not signed by Team ID $EXPECTED_TEAM_ID" >&2; exit 1 ;;
esac
spctl --assess --type install -vv "$pkg"

sudo installer -pkg "$pkg" -target /
swiftpkg --version

- name: Lint
if: ${{ inputs.lint == 'true' }}
shell: bash
env:
PROJECT_PATH: ${{ inputs.project-path }}
run: swiftpkg --lint "$PROJECT_PATH"

- name: Build
id: build
shell: bash
env:
PROJECT_PATH: ${{ inputs.project-path }}
PKG_VERSION: ${{ inputs.version }}
OUTPUT_DIR: ${{ inputs.output-dir }}
DO_VERIFY: ${{ inputs.verify }}
DO_PROVENANCE: ${{ inputs.provenance }}
EXTRA_ARGS: ${{ inputs.extra-args }}
run: |
set -euo pipefail
# extra-args first so the action's required flags (json output,
# output-dir) always win and can't be overridden into a shape that
# breaks jq parsing.
args=()
if [ -n "$EXTRA_ARGS" ]; then
read -ra extra <<< "$EXTRA_ARGS"
args+=("${extra[@]}")
fi
args+=(--output-format json --output-dir "$OUTPUT_DIR")
if [ -n "$PKG_VERSION" ]; then
args+=(--pkg-version "$PKG_VERSION")
fi
if [ "$DO_VERIFY" = "true" ]; then
args+=(--verify)
fi
if [ "$DO_PROVENANCE" = "true" ]; then
args+=(--provenance)
fi
result="$(swiftpkg "${args[@]}" "$PROJECT_PATH")"
echo "$result"
echo "pkg-path=$(echo "$result" | jq -r '.pkg_path')" >> "$GITHUB_OUTPUT"
echo "version=$(echo "$result" | jq -r '.version')" >> "$GITHUB_OUTPUT"
echo "sha256=$(echo "$result" | jq -r '.sha256')" >> "$GITHUB_OUTPUT"
Loading