Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

tools

A collection of developer tools for Apple platform projects, all written in Swift.

Tools

ToolDescription
migrate-changelogMove Unreleased changelog entries to a new versioned section
changetagExtract changelog sections and write them as annotated git tags
vrsnBump semantic or numeric version numbers in xcconfig, plist, podspec, gemspec, Swift, and arbitrary files (via regex)
prepare-releaseBump version, migrate changelog, tag, push, and create a GitHub release in one step; supports RC cycles and semver build metadata
prepare-github-releaseCreate a GitHub release from a git tag with changelog-derived release notes
read-changelogPrint a changelog section to stdout by tag name or latest git tag
xcbsDump fully-resolved Xcode build settings to lock files for diffing
psstInject secrets from a values file, env vars, or macOS Keychain into source placeholders
inject-git-infoWrite git SHA, branch, and clean status into Info.plist (Xcode build phase)
upload-symbolsUpload dSYM files to Sentry (Xcode build phase)
tag-iconsOverlay version/commit/custom text onto app icons (Xcode build phase)
ota-publishUpload an ad-hoc iOS .ipa to S3 and emit a short-lived itms-services OTA install landing page
spm-acknowledgementsGenerate an acknowledgements file from SPM dependency licenses

Requirements

  • Swift 6.1+
  • macOS 13+

Installation

Shipped as a binary-only Homebrew cask. Source lives in this (private) repo; built binaries are uploaded to releases on armcknight/homebrew-tools and installed via cask. Apple Silicon only (aarch64-apple-darwin).

brew tap armcknight/tools
brew trust armcknight/tools # third-party taps require explicit trust
brew install --cask armcknight/tools/tools

Development

Building and testing

make build # Release build
make test# Run unit and integration tests

Local install

make install # Build and install dev binaries (shadows the Homebrew cask)
make uninstall # Remove dev binaries; reinstalls the Homebrew cask if one was present

make install replaces the cask-installed binary symlinks at $(brew --prefix)/bin/ with locally built ones for testing. make uninstall removes them and reinstalls the cask via brew reinstall --cask tools.

Releasing

make patch # Bump patch version (must run before first deploy-beta in a cycle)
make minor # Bump minor version
make major # Bump major version
make deploy-beta # Tag an RC from [Unreleased] and push
make deploy # Consolidate RC entries, tag, push

The release GitHub Actions workflow runs on every numeric tag push. It builds the binaries on macos-14 (arm64), tarballs them, creates a release on armcknight/homebrew-tools with the tarball attached, and updates the appropriate cask there:

  • tag X.Y.ZCasks/tools.rb (stable channel — what brew install --cask tools gets)
  • tag X.Y.Z-rc.NCasks/tools-rc.rb (release-candidate channel — opt-in via brew install --cask armcknight/tools/tools-rc)

So make deploy and make deploy-beta only handle the source-side tag push — the homebrew side is fully automated. (tools and tools-rc casks are marked conflicts_with each other, so brew won't let you have both installed at once.)

One-time secret setup

The workflow needs a TAP_RELEASE_TOKEN repo secret on tools. Create a fine-grained personal access token with:

  • Repository access: armcknight/homebrew-tools only
  • Permissions:
    • Contents: Read and write (for git push of the cask bump + creating releases)
    • Metadata: Read-only (auto-granted)

Add it at Settings → Secrets and variables → Actions → New repository secret with name TAP_RELEASE_TOKEN. (The same PAT used for workr can be reused — fine-grained tokens may grant access to multiple repos.)

Version bumping and deploying are separate steps. Bump first, then deploy as many RCs as needed:

make minor # 1.2.3 → 1.3.0
make deploy-beta # tags 1.3.0-RC1
make deploy-beta # tags 1.3.0-RC2 (new RC, no version re-bump needed)
make deploy # consolidates all RC sections into 1.3.0

To change the target version mid-cycle, bump again before the next RC:

make minor # 1.2.3 → 1.3.0
make deploy-beta # 1.3.0-RC1
make major # 1.3.0 → 2.0.0 (scope grew)
make deploy-beta # 2.0.0-RC2
make deploy # consolidates all RC sections into 2.0.0

Usage

Every tool supports --help and --version.

migrate-changelog

migrate-changelog CHANGELOG.md 1.2.0
migrate-changelog CHANGELOG.md 1.2.0 --no-commit

changetag

changetag CHANGELOG.md 1.2.0
changetag CHANGELOG.md 1.2.0 --commit --message "bump version to 1.2.0"
changetag CHANGELOG.md v1.2.0 --name 1.2.0

vrsn

vrsn major -f Config.xcconfig
vrsn minor -f Config.xcconfig -k CURRENT_PROJECT_VERSION
vrsn patch -f Info.plist
vrsn -n -f Config.xcconfig # bump numeric version
vrsn -r -f Config.xcconfig # read current version
vrsn major -t -f Config.xcconfig # dry run
vrsn -u 2.0.0-beta.1 -f Config.xcconfig # set custom version
vrsn patch -f Sources/Shared/Version.swift -k toolsVersion # Swift file
vrsn patch -f Formula/tools.rb -p 'tag: "([^"]+)"' # regex pattern for any file format
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit # bump and commit in one step
vrsn minor -f Config.xcconfig -k MARKETING_VERSION --commit --stash # stash other staged changes first

prepare-release

Version bumping is the caller's responsibility. Run vrsn (or make patch/minor/major) before calling prepare-release. It will error if the version in --file matches the most recent non-RC changelog section, catching the "forgot to bump" case.

# Final release — migrates changelog, commits, tags, pushes, creates GitHub release
# (version must already be bumped in the file)
prepare-release --file Sources/Shared/Version.swift --key toolsVersion --push --github-release
# Plain VERSION file — --key is optional
prepare-release --file VERSION --push --github-release
# JSON and other formats --key cannot read — locate the version by regex instead.
# Same capture-group semantics as `vrsn --pattern`.
prepare-release --file plugin.json --pattern '"version": "([^"]+)"' --push --github-release
# Release candidate — tags [Unreleased] as RC; pass rc as the first argument
prepare-release rc --file Sources/Shared/Version.swift --key toolsVersion --push --github-release --prerelease
# iOS-style: separate marketing version and build number
# --build-number-key auto-increments CURRENT_PROJECT_VERSION and appends it as +N.
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release --prerelease
prepare-release --file Config.xcconfig --key MARKETING_VERSION --build-number-key CURRENT_PROJECT_VERSION --push --github-release
# Or supply the build number explicitly instead of auto-incrementing:
prepare-release rc --file Config.xcconfig --key MARKETING_VERSION --build-number 42 --push --github-release --prerelease

prepare-release prints the resolved tag name to stdout, so callers can capture it:

NEW_VERSION=$(prepare-release --file ...)

RC tags are auto-numbered by scanning consecutive RC sections in the changelog — numbering is sequential even when the base version changes mid-cycle (e.g. 1.2.4-RC11.3.0-RC22.0.0-RC3). Use --rc-number to override.

When a final release follows an RC cycle, prepare-release detects the existing RC sections and consolidates all of them into the final release entry regardless of their version prefix.

prepare-github-release

prepare-github-release 1.2.0 # create release from tag with changelog notes
prepare-github-release 1.2.0 --draft # create as draft
prepare-github-release 1.2.0 --prerelease # mark as prerelease
prepare-github-release 1.2.0 --changelog path/to/CHANGELOG.md

read-changelog

read-changelog CHANGELOG.md --latest-tag # print the section for the most recent git tag
read-changelog CHANGELOG.md --tag 1.2.0 # print the section for a specific tag
read-changelog CHANGELOG.md --tag 1.0-RC1+42 # works with semver metadata and RC tags

Useful in Fastfiles and scripts that need release notes:

# fastlane/Fastfilechangelog_text=sh("read-changelog ../CHANGELOG.md --latest-tag").stripupload_to_testflight(changelog: changelog_text)

xcbs

xcbs MyProject.xcodeproj

Exit code 66 means build settings changed from the previous lock files.

To use as a git pre-commit hook, copy Resources/xcbs/pre-commit.sample to .git/hooks/pre-commit.

psst

psst # use .psst/values and env vars
psst /path/to/keychain # also check macOS Keychain

Keys are defined in .psst/keys, values in .psst/values (gitignored).

inject-git-info

As an Xcode build phase:

${PATH_TO_TOOLS}/inject-git-info

Or with explicit arguments:

inject-git-info --plist-path /path/to/Info.plist --project-dir /path/to/project

upload-symbols

As an Xcode build phase:

${PATH_TO_TOOLS}/upload-symbols

Resolves Sentry credentials from environment variables, ~/.sentryclirc, or a .env file.

tag-icons

As an Xcode build phase:

${PATH_TO_TOOLS}/tag-icons version /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons commit /path/to/AppIcon.appiconset
${PATH_TO_TOOLS}/tag-icons custom /path/to/AppIcon.appiconset "beta"
${PATH_TO_TOOLS}/tag-icons cleanup /path/to/AppIcon.appiconset

Testing

swift test

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Automation for shipping mobile applications and utility packages.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages