@rtorcato/repo-tooling 3.2.4 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,6 +105,42 @@ export async function checkPackageSwift(dir) {
105
105
  detail: `Package.swift declares tools ${toolsVersion} and explicit platforms`,
106
106
  };
107
107
  }
108
+ /**
109
+ * Release automation for a SwiftPM package (#310). There is no publish step to
110
+ * look for — a release *is* a semver git tag consumers resolve with
111
+ * `.package(url:from:)` — so "configured" means a workflow that fires on a tag
112
+ * push. semantic-release is deliberately not accepted as evidence: its pipeline
113
+ * is npm end to end, and a Swift repo running it is publishing the wrong thing.
114
+ *
115
+ * Only the trigger section is searched (everything above `jobs:`), because
116
+ * `tags:` also appears inside job steps — docker/metadata-action emits one — and
117
+ * a step that mentions tags is not a release trigger.
118
+ */
119
+ export async function checkSwiftRelease(dir) {
120
+ const check = 'Release automation';
121
+ const hint = 'Run `npx @rtorcato/repo-tooling fix swift-release` to scaffold a tag-triggered release workflow';
122
+ const workflowsDir = path.join(dir, '.github', 'workflows');
123
+ if (await fs.pathExists(workflowsDir)) {
124
+ const files = (await fs.readdir(workflowsDir)).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
125
+ for (const file of files) {
126
+ const contents = await fs.readFile(path.join(workflowsDir, file), 'utf-8');
127
+ const triggers = contents.split(/^jobs:/m)[0] ?? '';
128
+ if (/^\s+tags:/m.test(triggers)) {
129
+ return {
130
+ check,
131
+ status: 'ok',
132
+ detail: `.github/workflows/${file} releases on a tag push`,
133
+ };
134
+ }
135
+ }
136
+ }
137
+ return {
138
+ check,
139
+ status: 'optional-missing',
140
+ detail: 'no workflow triggered by a version tag',
141
+ hint,
142
+ };
143
+ }
108
144
  /**
109
145
  * The Swift shape of the base `Git hooks` / `Pre-push hook` checks (#309).
110
146
  * `install` is null because the wiring — `git config core.hooksPath` — is
@@ -123,5 +159,6 @@ export async function runSwiftChecks(dir) {
123
159
  await checkPackageSwift(dir),
124
160
  ...(await Promise.all(SWIFT_FILE_CHECKS.map((spec) => checkFile(dir, spec)))),
125
161
  await checkSwiftGitignore(dir),
162
+ await checkSwiftRelease(dir),
126
163
  ];
127
164
  }
@@ -140,6 +140,49 @@ ${pkg.platforms.map((p) => ` - ${p}`).join('\n')}`,
140
140
  export function renderSwiftWorkflow(pkg) {
141
141
  return renderGitHubWorkflow(swiftGithubJobs(pkg));
142
142
  }
143
+ /**
144
+ * The release pipeline (#310). SwiftPM has no registry publish step — a release
145
+ * *is* a semver git tag that consumers resolve with `.package(url:from:)` — so
146
+ * the workflow fires on the tag rather than on a merge, and its only output is
147
+ * a GitHub Release with generated notes.
148
+ *
149
+ * The build/test gate runs before the release is cut because a tag is
150
+ * effectively permanent: SwiftPM caches resolved tags, so re-pointing a bad one
151
+ * doesn't reliably reach consumers who already resolved it.
152
+ *
153
+ * `gh` (preinstalled on the runner) rather than a release action: one fewer
154
+ * third-party pin to track, and `--verify-tag` refuses to invent a tag that
155
+ * isn't actually pushed.
156
+ */
157
+ export function renderSwiftReleaseWorkflow() {
158
+ return `name: 🏷️ Release
159
+
160
+ on:
161
+ push:
162
+ tags:
163
+ - '[0-9]+.[0-9]+.[0-9]+'
164
+ - 'v[0-9]+.[0-9]+.[0-9]+'
165
+
166
+ jobs:
167
+ release:
168
+ runs-on: ${MACOS}
169
+ permissions:
170
+ contents: write
171
+ steps:
172
+ ${XCODE_SETUP}
173
+
174
+ - name: 🏗️ swift build
175
+ run: swift build
176
+
177
+ - name: 🧪 swift test
178
+ run: swift test
179
+
180
+ - name: 🏷️ Publish GitHub Release
181
+ env:
182
+ GH_TOKEN: \${{ github.token }}
183
+ run: gh release create "\${{ github.ref_name }}" --generate-notes --verify-tag
184
+ `;
185
+ }
143
186
  /**
144
187
  * GitLab runs Swift in the official Linux image. That rules out Xcode — so no
145
188
  * platform matrix and no SwiftLint (a Homebrew/macOS tool in practice); the
@@ -7,7 +7,7 @@ import fs from 'fs-extra';
7
7
  import { buildPresetConfig } from '../../cli/commands/setup-presets.js';
8
8
  import { copyPreset } from '../../cli/utils/copy-preset.js';
9
9
  import { LOCKFILE_NAME, writeLockfile } from '../../cli/utils/lockfile.js';
10
- import { readSwiftPackage, renderSwiftGitLabCI, renderSwiftWorkflow } from './ci.js';
10
+ import { readSwiftPackage, renderSwiftGitLabCI, renderSwiftReleaseWorkflow, renderSwiftWorkflow, } from './ci.js';
11
11
  import { SWIFT_HOOKS_DIR, installSwiftGitHooks } from './git-hooks.js';
12
12
  import { ensureSwiftGitignore } from './gitignore.js';
13
13
  export const SWIFT_FIXERS = [
@@ -73,6 +73,19 @@ export const SWIFT_FIXERS = [
73
73
  return { filesWritten: ['.github/workflows/ci.yml'] };
74
74
  },
75
75
  },
76
+ {
77
+ target: 'swift-release',
78
+ description: 'Scaffold .github/workflows/release.yml (build + test on a version tag, then publish a GitHub Release)',
79
+ appliesTo: ['Release automation'],
80
+ outputs: ['.github/workflows/release.yml'],
81
+ canFixDrift: true,
82
+ async run({ targetDir }) {
83
+ const workflowsDir = path.join(targetDir, '.github', 'workflows');
84
+ await fs.ensureDir(workflowsDir);
85
+ await fs.writeFile(path.join(workflowsDir, 'release.yml'), renderSwiftReleaseWorkflow());
86
+ return { filesWritten: ['.github/workflows/release.yml'] };
87
+ },
88
+ },
76
89
  {
77
90
  target: 'swift-gitlab-ci',
78
91
  description: 'Scaffold .gitlab-ci.yml for Swift (swift build + test on the Linux Swift image)',
@@ -18,7 +18,7 @@ import { generateEditorConfig } from '../../cli/generators/misc.js';
18
18
  import { generateCodeQLWorkflow, generateDependabotConfig } from '../../cli/generators/security.js';
19
19
  import { copyPreset } from '../../cli/utils/copy-preset.js';
20
20
  import { LANGUAGES } from '../registry.js';
21
- import { readSwiftPackage, renderSwiftWorkflow } from './ci.js';
21
+ import { readSwiftPackage, renderSwiftReleaseWorkflow, renderSwiftWorkflow } from './ci.js';
22
22
  import { SWIFT_HOOKS_DIR, installSwiftGitHooks } from './git-hooks.js';
23
23
  import { ensureSwiftGitignore } from './gitignore.js';
24
24
  /**
@@ -168,7 +168,18 @@ ${config.projectName}/
168
168
  - **GitHub Actions** — \`swift build\`/\`swift test\` on macOS, plus \`xcodebuild\` per declared platform
169
169
 
170
170
  Run \`npx @rtorcato/repo-tooling doctor\` any time to audit this repo against the standard.
171
+ ${config.semanticRelease
172
+ ? `
173
+ ## Releasing
171
174
 
175
+ SwiftPM has no registry: a release is a semver git tag. Pushing one runs the
176
+ build/test gate and publishes a GitHub Release from it.
177
+
178
+ \`\`\`bash
179
+ git tag 1.0.0 && git push origin 1.0.0
180
+ \`\`\`
181
+ `
182
+ : ''}
172
183
  ## Contributing
173
184
 
174
185
  1. Fork the repository
@@ -199,6 +210,9 @@ export function swiftFileList(config) {
199
210
  '.editorconfig',
200
211
  '.github/workflows/ci.yml',
201
212
  ];
213
+ if (config.semanticRelease) {
214
+ files.push('.github/workflows/release.yml');
215
+ }
202
216
  if (config.gitHooks) {
203
217
  files.push(`${SWIFT_HOOKS_DIR}/pre-commit`, `${SWIFT_HOOKS_DIR}/pre-push`);
204
218
  }
@@ -227,6 +241,10 @@ export async function generateSwiftProject(config, targetDir) {
227
241
  const workflowsDir = path.join(targetDir, '.github', 'workflows');
228
242
  await fs.ensureDir(workflowsDir);
229
243
  await fs.writeFile(path.join(workflowsDir, 'ci.yml'), renderSwiftWorkflow(await readSwiftPackage(targetDir)));
244
+ // A tag-triggered release, not an npm publish (#310) — see the preset.
245
+ if (config.semanticRelease) {
246
+ await fs.writeFile(path.join(workflowsDir, 'release.yml'), renderSwiftReleaseWorkflow());
247
+ }
230
248
  // `core.hooksPath` won't stick here — `setup` scaffolds into a directory that
231
249
  // isn't a git repo yet — so the hooks land as files and the README tells you
232
250
  // the one-line config to run after `git init`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.2.4",
3
+ "version": "3.3.0",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -46,6 +46,7 @@
46
46
  "files": [
47
47
  "dist/cli/**/*.js",
48
48
  "dist/languages/**/*.js",
49
+ "dist/base/**/*.js",
49
50
  "tooling/commitlint/commitlint.mjs",
50
51
  "tooling/commitlint/commitlint.d.mts",
51
52
  "tooling/esbuild/index.mjs",
@@ -26,7 +26,7 @@ jobs:
26
26
  node-version-file: .nvmrc
27
27
 
28
28
  - name: Setup pnpm
29
- uses: pnpm/action-setup@v4
29
+ uses: pnpm/action-setup@v6
30
30
 
31
31
  - name: Install dependencies
32
32
  run: pnpm install --frozen-lockfile