Skip to main content

Swift projects

Swift is the first non-JavaScript language module. Everything the CLI does for a JS repo — scaffold with setup, audit with doctor, repair with fix — it also does for a SwiftPM package, using SwiftPM/SwiftLint/Periphery in place of pnpm/Biome/Vitest.

The standard here is the one swift-common actually runs. For the config file contents and the full check/fix table, see the Swift reference; this guide covers the project lifecycle.

Scaffold a new package

npx @rtorcato/repo-tooling setup --preset swift-library -d ./my-swift-lib

Or run setup with no flags and pick Swift at the language prompt. Unlike the JS wizard there's only one further question — the package name. SwiftLint, Periphery and swift test aren't options here, they're the standard, and a wizard whose every question has one answer is worse than no wizard.

--skip-install is accepted but redundant: nothing is installed either way. SwiftPM resolves dependencies on the first swift build, and there's no node_modules for the CLI to populate.

What it writes

FileWhy
Package.swiftSwiftPM manifest — tools version 6.0, platforms: [.iOS(.v17), .macOS(.v14)], one library product
Sources/<Module>/<Module>.swiftA placeholder public enum, so swift build succeeds
Tests/<Module>Tests/<Module>Tests.swiftOne XCTest case, so swift test succeeds
.swiftlint.ymlLint and format config
.periphery.ymlDead-code scan config (retain_public: true)
.gitignore.build, DerivedData, xcuserdata and friends
.githooks/pre-commit + pre-pushNode-free git hooks (SwiftLint, swift build/test)
.editorconfigShared baseline plus a [*.swift] 4-space override
.github/workflows/ci.ymlSwift CI on macOS runners
.github/workflows/release.ymlBuild/test gate + GitHub Release on a version tag
.github/workflows/codeql.ymlCodeQL with language: swift
.github/dependabot.yml + auto-merge workflowGrouped dependency updates
AGENTS.md, CLAUDE.md, Cursor/Copilot rules, Claude skill, .mcp.json.exampleAI agent files (language-agnostic)
README.mdSwiftPM-flavoured README
.repo-tooling.jsonLockfile recording language: "swift"

The module name is derived from the package name: my-swift-libMySwiftLib. Swift target names are type identifiers, so hyphens, dots and spaces are stripped and a leading digit gets a Package prefix.

Preview the list without writing anything:

npx @rtorcato/repo-tooling setup --preset swift-library -d ./my-swift-lib --dry-run

The scaffold passes its own CI

The generated package builds, tests, and lints clean out of the box:

cd my-swift-lib
swift build
swift test
swiftlint lint --strict # the exact command the generated CI runs

That last one is the reason Package.swift has no trailing commas in its collection literals — the .swiftlint.yml written alongside leaves trailing_comma enabled, so a manifest with them would fail the scaffold's own lint job on the first push.

You'll need the tools locally:

brew install swiftlint periphery

How the Swift path differs from JS

setup branches on the language before any generator runs, because everything in the JS path is rooted in package.json — the file a Swift repo is defined by not having. Concretely:

JavaScript/TypeScriptSwift
Manifestpackage.jsonPackage.swift
Buildtsup/esbuild/Vite/Rollupswift build
TestVitest/Jest/Playwright/Cypressswift test (XCTest)
Lint + formatBiome or ESLint + PrettierSwiftLint (both jobs), or swift-format for the formatter half
API docsTypeDocDocC
Dead codeknipPeriphery
Install steppnpm installnone — SwiftPM resolves on build
Git hooksHusky + lint-staged (.husky/)committed .githooks/ + core.hooksPath
Releasesemantic-release → npmversion tag → GitHub Release (release.yml)
CI runnerubuntu-latestmacos-latest (Xcode)
CodeQLjavascript-typescriptswift

The lockfile's JS-shaped fields (linting.tool, testing.framework, bundler) are all recorded as none for a Swift package. That means "no JS tool", not "no tooling" — SwiftLint and swift test are wired unconditionally, and the Swift checks doctor runs never consult those fields.

CI

The workflow is derived from Package.swift rather than from a config object, which is why setup writes the manifest first and reads it back:

JobWhat it does
build-testswift build + swift test, SwiftPM cache keyed on Package.resolved
lintswiftlint lint --strict
dead-codeperiphery scan --strict, continue-on-error
platformsxcodebuild once per platform in the manifest's platforms: clause

Every job runs on macos-latest — SwiftPM needs Xcode, and there's no setup-node or pnpm anywhere in the file. The platforms matrix appears only when the manifest declares both a platforms: clause and a library product (the product name becomes the xcodebuild scheme); the swift-library preset emits both, so a fresh scaffold gets iOS and macOS.

GitLab is available via fix swift-gitlab-ci, but covers the Linux-portable half only — the official swift:6.0 image has no Xcode, so no SwiftLint job and no platform matrix.

Existing packages: doctor + fix

Don't rerun setup on a repo that already exists — it's a fresh-project scaffolder and would overwrite your manifest. Use doctor and fix instead:

npx @rtorcato/repo-tooling doctor # audit
npx @rtorcato/repo-tooling fix swiftlint # .swiftlint.yml
npx @rtorcato/repo-tooling fix periphery # .periphery.yml
npx @rtorcato/repo-tooling fix swift-format # .swift-format (optional formatter)
npx @rtorcato/repo-tooling fix docc # Sources/<Target>/<Target>.docc
npx @rtorcato/repo-tooling fix swift-gitignore # append build artefacts
npx @rtorcato/repo-tooling fix swift-git-hooks # .githooks/ + core.hooksPath
npx @rtorcato/repo-tooling fix swift-ci # .github/workflows/ci.yml
npx @rtorcato/repo-tooling fix swift-release # .github/workflows/release.yml
npx @rtorcato/repo-tooling fix swift-codeql # .github/workflows/codeql.yml
npx @rtorcato/repo-tooling fix swift-gitlab-ci # .gitlab-ci.yml

doctor detects Swift from Package.swift and runs the language-agnostic checks (git hooks, commit linting, README badges, CI, CodeQL, Dependabot, GitHub repo settings) plus the Swift ones. There is deliberately no fixer for Package.swift — rewriting someone's manifest isn't safe, so doctor reports and you edit.

Not covered yet

The README badges check runs on a Swift repo but has no fixer — fix badges derives every badge URL from a package.json name + repository, which a SwiftPM repo hasn't got. doctor reports; you add the badges by hand.