@smoothbricks/cli 0.10.2 → 0.10.4
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.
- package/README.md +31 -23
- package/dist/bun/preload.d.ts +13 -0
- package/dist/bun/preload.d.ts.map +1 -0
- package/dist/bun/preload.js +12 -0
- package/dist/cli.js +36 -4
- package/dist/generate/index.d.ts.map +1 -1
- package/dist/generate/index.js +0 -6
- package/dist/github-ci/index.d.ts +17 -2
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +192 -26
- package/dist/github-ci/outputs.d.ts +22 -0
- package/dist/github-ci/outputs.d.ts.map +1 -0
- package/dist/github-ci/outputs.js +648 -0
- package/dist/lib/cli-package.js +2 -2
- package/dist/lib/json.d.ts +118 -11
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +317 -29
- package/dist/lib/workspace.d.ts +7 -14
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +76 -82
- package/dist/monorepo/ci-workflow.d.ts +2 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +28 -2
- package/dist/monorepo/git-config.js +2 -2
- package/dist/monorepo/index.d.ts +3 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +6 -2
- package/dist/monorepo/lockfile.d.ts +13 -1
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +33 -21
- package/dist/monorepo/managed-files.d.ts +12 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +86 -28
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +78 -45
- package/dist/monorepo/packed-manifest.d.ts +5 -2
- package/dist/monorepo/packed-manifest.d.ts.map +1 -1
- package/dist/monorepo/packed-manifest.js +16 -47
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +28 -16
- package/dist/monorepo/packs/index.d.ts +3 -0
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +18 -6
- package/dist/monorepo/publish-workflow.d.ts +1 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +242 -3
- package/dist/monorepo/runtime.d.ts +15 -0
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +66 -12
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +32 -23
- package/dist/monorepo/wrangler.d.ts.map +1 -1
- package/dist/monorepo/wrangler.js +13 -13
- package/dist/nx/index.d.ts +12 -4
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +86 -19
- package/dist/pr/index.d.ts.map +1 -1
- package/dist/pr/index.js +28 -32
- package/dist/release/candidates.d.ts +3 -2
- package/dist/release/candidates.d.ts.map +1 -1
- package/dist/release/candidates.js +4 -5
- package/dist/release/github-release.d.ts +3 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -0
- package/dist/release/index.d.ts +20 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +221 -89
- package/dist/release/orchestration.d.ts +8 -1
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +38 -1
- package/dist/wrangler/prepare-env.d.ts +0 -1
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +100 -51
- package/dist/wrangler/scaffold.d.ts.map +1 -1
- package/dist/wrangler/scaffold.js +12 -12
- package/managed/raw/git-format-staged.yml +19 -1
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +6 -0
- package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
- package/managed/raw/tooling/direnv/setup-environment.ts +187 -19
- package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
- package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
- package/managed/templates/github/actions/cache-node-modules/action.yml +6 -2
- package/managed/templates/github/actions/cache-nx/action.yml +2 -2
- package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
- package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
- package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
- package/managed/templates/github/workflows/managed-files.yml +80 -0
- package/package.json +18 -4
- package/src/bun/preload.ts +12 -0
- package/src/cli.ts +49 -11
- package/src/generate/index.ts +0 -6
- package/src/github-ci/index.test.ts +486 -0
- package/src/github-ci/index.ts +220 -30
- package/src/github-ci/outputs.ts +505 -0
- package/src/lib/cli-package.ts +2 -2
- package/src/lib/json.ts +291 -32
- package/src/lib/workspace.ts +88 -100
- package/src/monorepo/__tests__/ci-workflow.test.ts +14 -3
- package/src/monorepo/__tests__/publish-workflow.test.ts +222 -1
- package/src/monorepo/ci-workflow.ts +28 -2
- package/src/monorepo/git-config.ts +2 -2
- package/src/monorepo/index.ts +6 -2
- package/src/monorepo/lockfile.test.ts +35 -36
- package/src/monorepo/lockfile.ts +48 -24
- package/src/monorepo/managed-files.test.ts +63 -0
- package/src/monorepo/managed-files.ts +94 -34
- package/src/monorepo/package-policy.test.ts +70 -51
- package/src/monorepo/package-policy.ts +104 -59
- package/src/monorepo/packed-manifest.ts +18 -52
- package/src/monorepo/packed-package.ts +43 -22
- package/src/monorepo/packs/index.test.ts +18 -1
- package/src/monorepo/packs/index.ts +20 -7
- package/src/monorepo/publish-workflow.ts +482 -3
- package/src/monorepo/runtime.test.ts +42 -3
- package/src/monorepo/runtime.ts +90 -12
- package/src/monorepo/tool-validation.ts +35 -25
- package/src/monorepo/wrangler.test.ts +11 -15
- package/src/monorepo/wrangler.ts +15 -15
- package/src/nx/index.test.ts +43 -0
- package/src/nx/index.ts +110 -23
- package/src/pr/index.ts +42 -36
- package/src/release/__tests__/candidates.test.ts +5 -4
- package/src/release/__tests__/fixture-repo.test.ts +18 -16
- package/src/release/__tests__/github-release.test.ts +11 -0
- package/src/release/__tests__/orchestration.test.ts +57 -2
- package/src/release/__tests__/trust-publisher.test.ts +98 -2
- package/src/release/candidates.ts +10 -17
- package/src/release/github-release.ts +12 -0
- package/src/release/index.ts +321 -112
- package/src/release/orchestration.ts +55 -3
- package/src/wrangler/prepare-env.ts +75 -54
- package/src/wrangler/scaffold.test.ts +9 -12
- package/src/wrangler/scaffold.ts +13 -13
|
@@ -8,6 +8,8 @@ export enum CiWorkflowStepKind {
|
|
|
8
8
|
Build = 'build',
|
|
9
9
|
Lint = 'lint',
|
|
10
10
|
UnitTests = 'unit-tests',
|
|
11
|
+
ManagedFilesCheck = 'managed-files-check',
|
|
12
|
+
ManagedFilesDispatch = 'managed-files-dispatch',
|
|
11
13
|
Deploy = 'deploy',
|
|
12
14
|
SaveNxCache = 'save-nx-cache',
|
|
13
15
|
UploadTraceDbs = 'upload-trace-dbs',
|
|
@@ -37,6 +39,8 @@ export function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkfl
|
|
|
37
39
|
{ kind: CiWorkflowStepKind.Build, name: '🔨 Build' },
|
|
38
40
|
{ kind: CiWorkflowStepKind.Lint, name: '🔍 Lint' },
|
|
39
41
|
{ kind: CiWorkflowStepKind.UnitTests, name: '🧪 Unit Tests' },
|
|
42
|
+
{ kind: CiWorkflowStepKind.ManagedFilesCheck, name: '🩺 Check managed-file drift' },
|
|
43
|
+
{ kind: CiWorkflowStepKind.ManagedFilesDispatch, name: '🔁 Dispatch managed-file drift healing' },
|
|
40
44
|
];
|
|
41
45
|
if (options.deploy) {
|
|
42
46
|
steps.push({ kind: CiWorkflowStepKind.Deploy, name: '🚀 Deploy Staging' });
|
|
@@ -64,7 +68,8 @@ ${renderYamlList(options.pushBranches, 6)}
|
|
|
64
68
|
pull_request:
|
|
65
69
|
|
|
66
70
|
permissions:
|
|
67
|
-
actions:
|
|
71
|
+
# actions:write lets the drift step dispatch the managed-files workflow.
|
|
72
|
+
actions: write
|
|
68
73
|
contents: read
|
|
69
74
|
statuses: write
|
|
70
75
|
|
|
@@ -161,6 +166,27 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
161
166
|
return nxSmartStep(step, 'lint', 'Lint');
|
|
162
167
|
case CiWorkflowStepKind.UnitTests:
|
|
163
168
|
return nxSmartStep(step, 'test', 'Unit Tests');
|
|
169
|
+
case CiWorkflowStepKind.ManagedFilesCheck:
|
|
170
|
+
// The authoritative drift check is nearly free here (devenv is already
|
|
171
|
+
// up). --warn never fails the job and publishes the step output
|
|
172
|
+
// `drifted=<count>` for the dispatch step below.
|
|
173
|
+
return [
|
|
174
|
+
` - name: ${step.name}`,
|
|
175
|
+
' id: managed-drift',
|
|
176
|
+
' if:',
|
|
177
|
+
" ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}',",
|
|
178
|
+
' github.event.repository.default_branch) }}',
|
|
179
|
+
' run: smoo monorepo check --warn',
|
|
180
|
+
];
|
|
181
|
+
case CiWorkflowStepKind.ManagedFilesDispatch:
|
|
182
|
+
// Dispatches the managed-files workflow, which maintains the persistent
|
|
183
|
+
// review PR. workflow_dispatch is exempt from GitHub's GITHUB_TOKEN
|
|
184
|
+
// recursion guard, so the default token suffices.
|
|
185
|
+
return [
|
|
186
|
+
` - name: ${step.name}`,
|
|
187
|
+
" if: ${{ steps.managed-drift.outputs.drifted != '' && steps.managed-drift.outputs.drifted != '0' }}",
|
|
188
|
+
' run: gh workflow run managed-files.yml --ref "$GITHUB_REF_NAME"',
|
|
189
|
+
];
|
|
164
190
|
case CiWorkflowStepKind.Deploy:
|
|
165
191
|
return [
|
|
166
192
|
` - name: ${step.name}`,
|
|
@@ -181,7 +207,7 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
181
207
|
' path: |',
|
|
182
208
|
' .nx/cache',
|
|
183
209
|
' .nx/workspace-data/*.db*',
|
|
184
|
-
' key: ${{ runner.os }}-nx-db-v1-${{ github.sha }}',
|
|
210
|
+
' key: ${{ runner.os }}-${{ runner.arch }}-nx-db-v1-${{ github.sha }}',
|
|
185
211
|
];
|
|
186
212
|
case CiWorkflowStepKind.UploadTraceDbs:
|
|
187
213
|
return [
|
|
@@ -17,8 +17,8 @@ export async function applyWorkspaceGitConfig(root: string): Promise<void> {
|
|
|
17
17
|
// Keep the newer runtime version pins on any merge (nvfetcher overlay +
|
|
18
18
|
// devenv.lock) so a mirror sync's `git am --3way` never stalls on a version
|
|
19
19
|
// conflict. Mapped by the managed .gitattributes (merge=smoo-newer-pins);
|
|
20
|
-
// implemented in tooling/direnv/merge-newer-pins.sh.
|
|
21
|
-
//
|
|
20
|
+
// implemented in tooling/direnv/merge-newer-pins.sh. Runtime package.json
|
|
21
|
+
// pin repair is explicit via `smoo monorepo init --runtime-only`.
|
|
22
22
|
await $`git config --local merge.smoo-newer-pins.name ${'keep the newer devenv/nvfetcher runtime pins'}`.cwd(root);
|
|
23
23
|
await $`git config --local merge.smoo-newer-pins.driver ${'bash tooling/direnv/merge-newer-pins.sh %O %A %B %P'}`.cwd(
|
|
24
24
|
root,
|
package/src/monorepo/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { escapeRegex, getWorkspacePackages, getWorkspacePatterns, listReleasePac
|
|
|
5
5
|
import { formatCommitMessage, validateCommitMessage } from './commit-msg.js';
|
|
6
6
|
import { applyWorkspaceGitConfig } from './git-config.js';
|
|
7
7
|
import { syncBunLockfileVersions } from './lockfile.js';
|
|
8
|
-
import { applyManagedFiles, printResults } from './managed-files.js';
|
|
8
|
+
import { applyManagedFiles, printResults, warnOnManagedFileDrift } from './managed-files.js';
|
|
9
9
|
import { listValidCommitScopes, validatePublicTags } from './package-policy.js';
|
|
10
10
|
import { runInitPacks, runValidatePacks } from './packs/index.js';
|
|
11
11
|
import { syncRootRuntimeVersions } from './runtime.js';
|
|
@@ -78,7 +78,11 @@ export function updateManagedFiles(root: string): void {
|
|
|
78
78
|
printResults(applyManagedFiles(root, 'update'));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
export function checkManagedFiles(root: string): void {
|
|
81
|
+
export function checkManagedFiles(root: string, options: { warn?: boolean } = {}): void {
|
|
82
|
+
if (options.warn === true) {
|
|
83
|
+
warnOnManagedFileDrift(root);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
82
86
|
const results = applyManagedFiles(root, 'check');
|
|
83
87
|
printResults(results);
|
|
84
88
|
if (results.some((result) => result.action === 'drifted')) {
|
|
@@ -17,15 +17,6 @@ function git(root: string, args: string): string {
|
|
|
17
17
|
return execSync(`git ${args}`, {
|
|
18
18
|
cwd: root,
|
|
19
19
|
encoding: 'utf8',
|
|
20
|
-
env: {
|
|
21
|
-
...process.env,
|
|
22
|
-
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
23
|
-
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
24
|
-
GIT_AUTHOR_NAME: 'fixture',
|
|
25
|
-
GIT_AUTHOR_EMAIL: 'fixture@invalid',
|
|
26
|
-
GIT_COMMITTER_NAME: 'fixture',
|
|
27
|
-
GIT_COMMITTER_EMAIL: 'fixture@invalid',
|
|
28
|
-
},
|
|
29
20
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
21
|
});
|
|
31
22
|
}
|
|
@@ -40,6 +31,9 @@ function makeFixtureRepo(options: FixtureOptions): string {
|
|
|
40
31
|
const root = mkdtempSync(join(tmpdir(), 'smoo-lockfile-'));
|
|
41
32
|
cleanupRoots.push(root);
|
|
42
33
|
git(root, 'init -q');
|
|
34
|
+
// Local identity only — CI runners have no global git user.
|
|
35
|
+
git(root, 'config user.name Test');
|
|
36
|
+
git(root, 'config user.email test@example.com');
|
|
43
37
|
git(root, 'commit -q --allow-empty -m init');
|
|
44
38
|
writeFileSync(
|
|
45
39
|
join(root, 'package.json'),
|
|
@@ -56,8 +50,8 @@ function makeFixtureRepo(options: FixtureOptions): string {
|
|
|
56
50
|
|
|
57
51
|
function writeFixturePackage(root: string, version: string): void {
|
|
58
52
|
writeFileSync(
|
|
59
|
-
join(root, 'packages
|
|
60
|
-
JSON.stringify({ name: '@fixture/foo', version, nx: { name: 'foo' } })
|
|
53
|
+
join(root, 'packages/foo/package.json'),
|
|
54
|
+
`${JSON.stringify({ name: '@fixture/foo', version, private: false, nx: { name: 'foo' } }, null, 2)}\n`,
|
|
61
55
|
);
|
|
62
56
|
}
|
|
63
57
|
|
|
@@ -71,9 +65,9 @@ function writeFixtureLock(root: string, version: string): void {
|
|
|
71
65
|
' "": { "name": "fixture-root" },',
|
|
72
66
|
' "packages/foo": {',
|
|
73
67
|
' "name": "@fixture/foo",',
|
|
74
|
-
` "version": "${version}"
|
|
75
|
-
' }
|
|
76
|
-
' }
|
|
68
|
+
` "version": "${version}"`,
|
|
69
|
+
' }',
|
|
70
|
+
' }',
|
|
77
71
|
'}',
|
|
78
72
|
'',
|
|
79
73
|
].join('\n'),
|
|
@@ -87,59 +81,64 @@ function lockVersionIn(root: string): string {
|
|
|
87
81
|
}
|
|
88
82
|
|
|
89
83
|
describe('bun.lock workspace version sync', () => {
|
|
90
|
-
it('
|
|
91
|
-
// The b3298fc4c shape: package.json holds the next prerelease, a stable tag
|
|
92
|
-
// exists, and `bun install` rewrote the lockfile back to the prerelease.
|
|
84
|
+
it('install validate requires lockfile ≡ package.json including -next', () => {
|
|
93
85
|
const root = makeFixtureRepo({
|
|
94
86
|
packageVersion: '0.1.3-next.0',
|
|
95
|
-
lockVersion: '0.1.
|
|
87
|
+
lockVersion: '0.1.2',
|
|
96
88
|
stableTag: 'foo@0.1.2',
|
|
97
89
|
});
|
|
98
90
|
expect(validateBunLockfileVersions(root)).toBe(1);
|
|
99
91
|
});
|
|
100
92
|
|
|
101
|
-
it('sync restores
|
|
93
|
+
it('install mode sync restores package.json -next for frozen CI', () => {
|
|
94
|
+
const root = makeFixtureRepo({
|
|
95
|
+
packageVersion: '0.1.3-next.0',
|
|
96
|
+
lockVersion: '0.1.2',
|
|
97
|
+
stableTag: 'foo@0.1.2',
|
|
98
|
+
});
|
|
99
|
+
expect(syncBunLockfileVersions(root, { log: false, mode: 'install' })).toBe(1);
|
|
100
|
+
expect(lockVersionIn(root)).toBe('0.1.3-next.0');
|
|
101
|
+
expect(validateBunLockfileVersions(root)).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('publish mode rewrites unpublished -next to last stable tag for pack', () => {
|
|
102
105
|
const root = makeFixtureRepo({
|
|
103
106
|
packageVersion: '0.1.3-next.0',
|
|
104
107
|
lockVersion: '0.1.3-next.0',
|
|
105
108
|
stableTag: 'foo@0.1.2',
|
|
106
109
|
});
|
|
107
|
-
|
|
108
|
-
expect(lockVersionIn(root)).toBe('0.1.2');
|
|
110
|
+
// Install/CI is happy with -next matching package.json.
|
|
109
111
|
expect(validateBunLockfileVersions(root)).toBe(0);
|
|
112
|
+
// Pre-publish rewrite embeds last stable for bun pm pack.
|
|
113
|
+
expect(syncBunLockfileVersions(root, { log: false, mode: 'publish' })).toBe(1);
|
|
114
|
+
expect(lockVersionIn(root)).toBe('0.1.2');
|
|
110
115
|
});
|
|
111
116
|
|
|
112
|
-
it('
|
|
113
|
-
// The new-workspace-package shape (e.g. cowshed): no stable tag exists yet.
|
|
117
|
+
it('never-published prerelease keeps package.json version in both modes', () => {
|
|
114
118
|
const root = makeFixtureRepo({ packageVersion: '0.2.0-next.0', lockVersion: '0.2.0-next.0' });
|
|
115
119
|
expect(validateBunLockfileVersions(root)).toBe(0);
|
|
116
|
-
expect(syncBunLockfileVersions(root, { log: false })).toBe(0);
|
|
120
|
+
expect(syncBunLockfileVersions(root, { log: false, mode: 'install' })).toBe(0);
|
|
121
|
+
expect(syncBunLockfileVersions(root, { log: false, mode: 'publish' })).toBe(0);
|
|
117
122
|
});
|
|
118
123
|
|
|
119
|
-
it('
|
|
124
|
+
it('stable manifest version is required verbatim', () => {
|
|
120
125
|
const root = makeFixtureRepo({ packageVersion: '0.1.0', lockVersion: '0.0.9' });
|
|
121
126
|
expect(validateBunLockfileVersions(root)).toBe(1);
|
|
122
|
-
expect(syncBunLockfileVersions(root, { log: false })).toBe(1);
|
|
127
|
+
expect(syncBunLockfileVersions(root, { log: false, mode: 'install' })).toBe(1);
|
|
123
128
|
expect(lockVersionIn(root)).toBe('0.1.0');
|
|
124
129
|
});
|
|
125
130
|
|
|
126
131
|
it('stage: true stages the healed bun.lock', () => {
|
|
127
132
|
const root = makeFixtureRepo({
|
|
128
133
|
packageVersion: '0.1.3-next.0',
|
|
129
|
-
lockVersion: '0.1.
|
|
134
|
+
lockVersion: '0.1.2',
|
|
130
135
|
stableTag: 'foo@0.1.2',
|
|
131
136
|
});
|
|
132
|
-
expect(syncBunLockfileVersions(root, { log: false, stage: true })).toBe(1);
|
|
137
|
+
expect(syncBunLockfileVersions(root, { log: false, mode: 'install', stage: true })).toBe(1);
|
|
133
138
|
expect(git(root, 'diff --cached --name-only')).toContain('bun.lock');
|
|
134
139
|
});
|
|
135
140
|
|
|
136
|
-
it('
|
|
137
|
-
const root = makeFixtureRepo({ packageVersion: '0.1.2', lockVersion: '0.1.2', stableTag: 'foo@0.1.2' });
|
|
138
|
-
expect(syncBunLockfileVersions(root, { log: false, stage: true })).toBe(0);
|
|
139
|
-
expect(git(root, 'diff --cached --name-only').trim()).toBe('');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('property: after sync, validation always passes', () => {
|
|
141
|
+
it('property: after install-mode sync, validation always passes', () => {
|
|
143
142
|
const version = fc
|
|
144
143
|
.tuple(fc.nat({ max: 20 }), fc.nat({ max: 20 }), fc.nat({ max: 20 }), fc.option(fc.nat({ max: 5 })))
|
|
145
144
|
.map(([major, minor, patch, next]) => `${major}.${minor}.${patch}${next === null ? '' : `-next.${next}`}`);
|
|
@@ -148,7 +147,7 @@ describe('bun.lock workspace version sync', () => {
|
|
|
148
147
|
fc.property(version, version, (packageVersion, lockVersion) => {
|
|
149
148
|
writeFixturePackage(root, packageVersion);
|
|
150
149
|
writeFixtureLock(root, lockVersion);
|
|
151
|
-
syncBunLockfileVersions(root, { log: false });
|
|
150
|
+
syncBunLockfileVersions(root, { log: false, mode: 'install' });
|
|
152
151
|
return validateBunLockfileVersions(root) === 0;
|
|
153
152
|
}),
|
|
154
153
|
{ numRuns: 25 },
|
package/src/monorepo/lockfile.ts
CHANGED
|
@@ -5,27 +5,36 @@ import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
|
|
|
5
5
|
|
|
6
6
|
export interface SyncBunLockfileVersionsOptions {
|
|
7
7
|
log?: boolean;
|
|
8
|
-
/** `git add` bun.lock
|
|
8
|
+
/** `git add` bun.lock when versions were resynced. */
|
|
9
9
|
stage?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* `install` (default for validation): lockfile must match package.json.
|
|
12
|
+
* `publish`: rewrite unpublished prerelease package.json versions to the last
|
|
13
|
+
* stable git tag so `bun pm pack` embeds installable dependency versions.
|
|
14
|
+
*/
|
|
15
|
+
mode?: 'install' | 'publish';
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
// Temporary Bun workaround. Delete
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// versions stale in bun.lock after manifest bumps. Until then, `bun pm pack`
|
|
16
|
-
// rewrites `workspace:*` dependencies using those stale lockfile versions instead
|
|
17
|
-
// of the current package.json versions. Track removal against:
|
|
18
|
+
// Temporary Bun workaround. Delete once supported Bun versions stop leaving
|
|
19
|
+
// workspace package versions stale in bun.lock after manifest bumps, and stop
|
|
20
|
+
// resolving `workspace:*` from the lockfile during `bun pm pack`:
|
|
18
21
|
// - https://github.com/oven-sh/bun/issues/18906
|
|
19
22
|
// - https://github.com/oven-sh/bun/issues/20477
|
|
20
23
|
// - https://github.com/oven-sh/bun/issues/20829
|
|
21
24
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
25
|
+
// Two modes, because one lockfile serves two jobs:
|
|
26
|
+
//
|
|
27
|
+
// 1) install / frozen CI → lockfile ≡ package.json (including -next)
|
|
28
|
+
// 2) pre-publish pack → for package.json prereleases that were never published
|
|
29
|
+
// (post-release "prepare next"), lockfile entry := last
|
|
30
|
+
// stable tag so a releasing package does not embed an
|
|
31
|
+
// unpublished -next dependency version.
|
|
32
|
+
//
|
|
33
|
+
// Do not run publish-mode rewrite in day-to-day validate or pre-commit: bun install
|
|
34
|
+
// rewrites lockfile back to package.json and frozen CI then fails.
|
|
27
35
|
export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVersionsOptions = {}): number {
|
|
28
36
|
const log = options.log ?? true;
|
|
37
|
+
const mode = options.mode ?? 'publish';
|
|
29
38
|
const lockfilePath = join(root, 'bun.lock');
|
|
30
39
|
if (!existsSync(lockfilePath)) {
|
|
31
40
|
throw new Error('bun.lock not found');
|
|
@@ -34,13 +43,7 @@ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVe
|
|
|
34
43
|
let lockfile = readFileSync(lockfilePath, 'utf8');
|
|
35
44
|
let updated = 0;
|
|
36
45
|
for (const pkg of packages) {
|
|
37
|
-
|
|
38
|
-
// never have been published. Use the latest stable git tag version so
|
|
39
|
-
// that `bun pm pack` writes an installable dependency range for consumers.
|
|
40
|
-
const targetVersion = pkg.version.includes('-')
|
|
41
|
-
? (latestStableTagVersion(root, pkg.projectName) ?? pkg.version)
|
|
42
|
-
: pkg.version;
|
|
43
|
-
|
|
46
|
+
const targetVersion = lockfileTargetVersion(root, pkg.projectName, pkg.version, mode);
|
|
44
47
|
const relativePath = pkg.path.replaceAll('\\', '/');
|
|
45
48
|
const escaped = escapeRegex(relativePath);
|
|
46
49
|
const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
|
|
@@ -60,7 +63,10 @@ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVe
|
|
|
60
63
|
}
|
|
61
64
|
lockfile = lockfile.replace(pattern, `$1${targetVersion}$3`);
|
|
62
65
|
if (log) {
|
|
63
|
-
const suffix =
|
|
66
|
+
const suffix =
|
|
67
|
+
mode === 'publish' && targetVersion !== pkg.version
|
|
68
|
+
? ` (latest stable tag; package.json has ${pkg.version})`
|
|
69
|
+
: '';
|
|
64
70
|
console.log(`fix: ${relativePath}: ${lockVersion} -> ${targetVersion}${suffix}`);
|
|
65
71
|
}
|
|
66
72
|
updated++;
|
|
@@ -81,6 +87,7 @@ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVe
|
|
|
81
87
|
return updated;
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/** Install/CI invariant: bun.lock workspace versions match package.json. */
|
|
84
91
|
export function validateBunLockfileVersions(root: string): number {
|
|
85
92
|
const lockfilePath = join(root, 'bun.lock');
|
|
86
93
|
if (!existsSync(lockfilePath)) {
|
|
@@ -91,10 +98,7 @@ export function validateBunLockfileVersions(root: string): number {
|
|
|
91
98
|
const lockfile = readFileSync(lockfilePath, 'utf8');
|
|
92
99
|
let failures = 0;
|
|
93
100
|
for (const pkg of packages) {
|
|
94
|
-
const targetVersion = pkg.version
|
|
95
|
-
? (latestStableTagVersion(root, pkg.projectName) ?? pkg.version)
|
|
96
|
-
: pkg.version;
|
|
97
|
-
|
|
101
|
+
const targetVersion = pkg.version;
|
|
98
102
|
const relativePath = pkg.path.replaceAll('\\', '/');
|
|
99
103
|
const escaped = escapeRegex(relativePath);
|
|
100
104
|
const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
|
|
@@ -116,6 +120,26 @@ export function validateBunLockfileVersions(root: string): number {
|
|
|
116
120
|
return failures;
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Version Bun should embed for a workspace dependency when packing for publish.
|
|
125
|
+
* Unpublished package.json prereleases (prepare-next) map to the last stable tag.
|
|
126
|
+
*/
|
|
127
|
+
export function publishPackDependencyVersion(root: string, projectName: string, packageVersion: string): string {
|
|
128
|
+
return lockfileTargetVersion(root, projectName, packageVersion, 'publish');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function lockfileTargetVersion(
|
|
132
|
+
root: string,
|
|
133
|
+
projectName: string,
|
|
134
|
+
packageVersion: string,
|
|
135
|
+
mode: 'install' | 'publish',
|
|
136
|
+
): string {
|
|
137
|
+
if (mode === 'install' || !packageVersion.includes('-')) {
|
|
138
|
+
return packageVersion;
|
|
139
|
+
}
|
|
140
|
+
return latestStableTagVersion(root, projectName) ?? packageVersion;
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
function latestStableTagVersion(root: string, projectName: string): string | null {
|
|
120
144
|
try {
|
|
121
145
|
const output = execSync(`git tag --list '${projectName}@*' --sort=-v:refname`, {
|
|
@@ -1,16 +1,31 @@
|
|
|
1
|
+
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are asserted literally. */
|
|
1
2
|
import { describe, expect, it } from 'bun:test';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { LINUX_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
2
6
|
import fc from 'fast-check';
|
|
3
7
|
import {
|
|
4
8
|
extractInlineLocalBlocksForTest,
|
|
5
9
|
INLINE_LOCAL_BEGIN,
|
|
6
10
|
INLINE_LOCAL_END,
|
|
7
11
|
LOCAL_SECTION_MARKER,
|
|
12
|
+
platformTargetGlobsForTest,
|
|
8
13
|
reinsertInlineLocalBlocksForTest,
|
|
9
14
|
splitLocalSectionForTest,
|
|
10
15
|
} from './managed-files.js';
|
|
11
16
|
|
|
12
17
|
const MANAGED = '# managed content\npath merge=driver\n';
|
|
13
18
|
|
|
19
|
+
const REPO_ROOT = join(import.meta.dir, '..', '..', '..', '..');
|
|
20
|
+
const ARCHITECTURE_SCOPED_PREFIX = '${{ runner.os }}-${{ runner.arch }}-';
|
|
21
|
+
const NODE_MODULES_CACHE_KEY = "${{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}";
|
|
22
|
+
const CACHE_ACTIONS = [
|
|
23
|
+
{ name: 'cache-nix-devenv', osKeyLines: 6 },
|
|
24
|
+
{ name: 'cache-node-modules', osKeyLines: 2 },
|
|
25
|
+
{ name: 'cache-ttsc-plugins', osKeyLines: 2 },
|
|
26
|
+
{ name: 'cache-nx', osKeyLines: 2 },
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
14
29
|
describe('managed-file local sections', () => {
|
|
15
30
|
it('content without a marker is entirely managed', () => {
|
|
16
31
|
const { managed, localTail } = splitLocalSectionForTest(MANAGED);
|
|
@@ -139,3 +154,51 @@ describe('managed-file inline local blocks', () => {
|
|
|
139
154
|
);
|
|
140
155
|
});
|
|
141
156
|
});
|
|
157
|
+
|
|
158
|
+
describe('managed publish platform discovery', () => {
|
|
159
|
+
it('returns canonical target families from resolved target names without leaking project names', () => {
|
|
160
|
+
const discovered = platformTargetGlobsForTest(['build', 'bundle-linux', 'package-macos', 'simulator-ios', 'test']);
|
|
161
|
+
|
|
162
|
+
expect(discovered).toEqual([...PLATFORM_TARGET_GLOBS]);
|
|
163
|
+
expect(discovered).not.toContain('native-app');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('selects only supplemental Linux when resolved metadata has no Apple targets', () => {
|
|
167
|
+
expect(platformTargetGlobsForTest(['build', 'bundle-linux', 'test'])).toEqual([...LINUX_PLATFORM_TARGET_GLOBS]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns no platform families for ordinary Nx targets', () => {
|
|
171
|
+
expect(platformTargetGlobsForTest(['build', 'lint', 'test', 'typecheck'])).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('managed cache actions', () => {
|
|
176
|
+
it('renders the checked-in action copies from their managed templates', async () => {
|
|
177
|
+
for (const action of CACHE_ACTIONS) {
|
|
178
|
+
const [template, generated] = await Promise.all([
|
|
179
|
+
readFile(
|
|
180
|
+
join(REPO_ROOT, 'packages', 'cli', 'managed', 'templates', 'github', 'actions', action.name, 'action.yml'),
|
|
181
|
+
'utf8',
|
|
182
|
+
),
|
|
183
|
+
readFile(join(REPO_ROOT, '.github', 'actions', action.name, 'action.yml'), 'utf8'),
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
expect(generated).toBe(template.replace('{{NODE_MODULES_CACHE_KEY}}', NODE_MODULES_CACHE_KEY));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('scopes every primary, restore, and save key to the runner OS and architecture', async () => {
|
|
191
|
+
for (const action of CACHE_ACTIONS) {
|
|
192
|
+
for (const actionRoot of [
|
|
193
|
+
join(REPO_ROOT, 'packages', 'cli', 'managed', 'templates', 'github', 'actions'),
|
|
194
|
+
join(REPO_ROOT, '.github', 'actions'),
|
|
195
|
+
]) {
|
|
196
|
+
const content = await readFile(join(actionRoot, action.name, 'action.yml'), 'utf8');
|
|
197
|
+
const osKeyLines = content.split('\n').filter((line) => line.includes('${{ runner.os }}'));
|
|
198
|
+
|
|
199
|
+
expect(osKeyLines).toHaveLength(action.osKeyLines);
|
|
200
|
+
expect(osKeyLines.every((line) => line.includes(ARCHITECTURE_SCOPED_PREFIX))).toBe(true);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
6
|
+
import { type PackageJson, parseNxProjectJsonText, parseStringArrayText } from '../lib/json.js';
|
|
5
7
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
6
8
|
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
7
9
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
@@ -137,6 +139,7 @@ interface ManagedFileContext {
|
|
|
137
139
|
ciPushBranches: string[];
|
|
138
140
|
nodeModulesCacheKey: string;
|
|
139
141
|
repoName: string;
|
|
142
|
+
platformTargetGlobs: string[];
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
interface DeployTargetInfo {
|
|
@@ -207,6 +210,11 @@ const managedFiles: ManagedFile[] = [
|
|
|
207
210
|
target: '.github/workflows/publish.yml',
|
|
208
211
|
releasePackagesOnly: true,
|
|
209
212
|
},
|
|
213
|
+
{
|
|
214
|
+
kind: 'template',
|
|
215
|
+
source: 'github/workflows/managed-files.yml',
|
|
216
|
+
target: '.github/workflows/managed-files.yml',
|
|
217
|
+
},
|
|
210
218
|
{
|
|
211
219
|
kind: 'template',
|
|
212
220
|
source: 'github/actions/cache-nix-devenv/action.yml',
|
|
@@ -227,6 +235,11 @@ const managedFiles: ManagedFile[] = [
|
|
|
227
235
|
source: 'github/actions/cache-node-modules/action.yml',
|
|
228
236
|
target: '.github/actions/cache-node-modules/action.yml',
|
|
229
237
|
},
|
|
238
|
+
{
|
|
239
|
+
kind: 'template',
|
|
240
|
+
source: 'github/actions/cache-ttsc-plugins/action.yml',
|
|
241
|
+
target: '.github/actions/cache-ttsc-plugins/action.yml',
|
|
242
|
+
},
|
|
230
243
|
{
|
|
231
244
|
kind: 'template',
|
|
232
245
|
source: 'github/actions/cache-nx/action.yml',
|
|
@@ -295,6 +308,7 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
295
308
|
deploy: context.hasProductionDeployTargets,
|
|
296
309
|
deployProvider: context.productionDeployProvider,
|
|
297
310
|
repoName: context.repoName,
|
|
311
|
+
platformTargetGlobs: context.platformTargetGlobs,
|
|
298
312
|
});
|
|
299
313
|
}
|
|
300
314
|
throw new Error(`Unknown generated managed file source ${file.source}`);
|
|
@@ -314,6 +328,7 @@ function getManagedFileContext(root: string): ManagedFileContext {
|
|
|
314
328
|
const ciPushBranches = getCiPushBranches(packageJson?.json);
|
|
315
329
|
const stagingDeploy = getDeployTargetInfo(root, 'staging');
|
|
316
330
|
const productionDeploy = getDeployTargetInfo(root, 'production');
|
|
331
|
+
const platformTargetGlobs = platformTargetGlobsForTest(readResolvedNxTargetNames(root));
|
|
317
332
|
const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
|
|
318
333
|
? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
|
|
319
334
|
: `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
|
|
@@ -326,9 +341,46 @@ function getManagedFileContext(root: string): ManagedFileContext {
|
|
|
326
341
|
ciPushBranches,
|
|
327
342
|
nodeModulesCacheKey,
|
|
328
343
|
repoName,
|
|
344
|
+
platformTargetGlobs,
|
|
329
345
|
};
|
|
330
346
|
}
|
|
331
347
|
|
|
348
|
+
export function platformTargetGlobsForTest(targetNames: Iterable<string>): string[] {
|
|
349
|
+
const names = [...targetNames];
|
|
350
|
+
return PLATFORM_TARGET_GLOBS.filter((glob) => {
|
|
351
|
+
const suffix = glob.startsWith('*') ? glob.slice(1) : glob;
|
|
352
|
+
return names.some((name) => name.endsWith(suffix));
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function readResolvedNxTargetNames(root: string): string[] {
|
|
357
|
+
const output = execFileSync('nx', ['show', 'projects', '--json'], {
|
|
358
|
+
cwd: root,
|
|
359
|
+
encoding: 'utf8',
|
|
360
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
361
|
+
});
|
|
362
|
+
const projects = parseStringArrayText(output);
|
|
363
|
+
if (!projects) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
const targetNames = new Set<string>();
|
|
367
|
+
for (const project of projects) {
|
|
368
|
+
const projectOutput = execFileSync('nx', ['show', 'project', project, '--json'], {
|
|
369
|
+
cwd: root,
|
|
370
|
+
encoding: 'utf8',
|
|
371
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
372
|
+
});
|
|
373
|
+
const projectJson = parseNxProjectJsonText(projectOutput);
|
|
374
|
+
const targets = projectJson?.targets;
|
|
375
|
+
if (targets) {
|
|
376
|
+
for (const targetName of Object.keys(targets)) {
|
|
377
|
+
targetNames.add(targetName);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return [...targetNames];
|
|
382
|
+
}
|
|
383
|
+
|
|
332
384
|
function renderTemplate(context: ManagedFileContext, template: string): string {
|
|
333
385
|
return template
|
|
334
386
|
.replaceAll('{{REPO_NAME}}', context.repoName)
|
|
@@ -342,8 +394,8 @@ function getDeployTargetInfo(root: string, configuration: string): DeployTargetI
|
|
|
342
394
|
encoding: 'utf8',
|
|
343
395
|
stdio: ['ignore', 'pipe', 'inherit'],
|
|
344
396
|
});
|
|
345
|
-
const projects
|
|
346
|
-
if (!
|
|
397
|
+
const projects = parseStringArrayText(output);
|
|
398
|
+
if (!projects) {
|
|
347
399
|
return { exists: false };
|
|
348
400
|
}
|
|
349
401
|
let exists = false;
|
|
@@ -365,47 +417,25 @@ function nxDeployTarget(root: string, project: string, configuration: string): D
|
|
|
365
417
|
encoding: 'utf8',
|
|
366
418
|
stdio: ['ignore', 'pipe', 'inherit'],
|
|
367
419
|
});
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
const deploy = recordValue(targets?.deploy);
|
|
372
|
-
const configurations = recordValue(deploy?.configurations);
|
|
373
|
-
const config = recordValue(configurations?.[configuration]);
|
|
420
|
+
const projectJson = parseNxProjectJsonText(output);
|
|
421
|
+
const deploy = projectJson?.targets?.deploy;
|
|
422
|
+
const config = deploy?.configurations?.[configuration];
|
|
374
423
|
if (!config) {
|
|
375
424
|
return { exists: false };
|
|
376
425
|
}
|
|
377
|
-
const
|
|
426
|
+
const commandValue = config.command ?? config.options?.command ?? deploy?.options?.command;
|
|
427
|
+
const command = typeof commandValue === 'string' ? commandValue : '';
|
|
378
428
|
return { exists: true, provider: command.includes('wrangler ') ? 'cloudflare' : undefined };
|
|
379
429
|
}
|
|
380
430
|
|
|
381
|
-
function
|
|
382
|
-
const command = stringValue(config.command) ?? stringValue(recordValue(config.options)?.command);
|
|
383
|
-
return command ?? stringValue(recordValue(deploy?.options)?.command) ?? '';
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function stringValue(value: unknown): string | undefined {
|
|
387
|
-
return typeof value === 'string' ? value : undefined;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function getCiPushBranches(packageJson: unknown): string[] {
|
|
431
|
+
function getCiPushBranches(packageJson: PackageJson | null | undefined): string[] {
|
|
391
432
|
const configured = readCiPushBranches(packageJson);
|
|
392
433
|
return configured.length > 0 ? configured : ['main'];
|
|
393
434
|
}
|
|
394
435
|
|
|
395
|
-
function readCiPushBranches(packageJson:
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
const github = recordValue(smoo?.github);
|
|
399
|
-
const branches = Array.isArray(github?.pushBranches) ? github.pushBranches : [];
|
|
400
|
-
return branches.filter((branch): branch is string => typeof branch === 'string' && branch.length > 0);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
404
|
-
return isRecord(value) ? value : undefined;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
408
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
436
|
+
function readCiPushBranches(packageJson: PackageJson | null | undefined): string[] {
|
|
437
|
+
const branches = packageJson?.smoo?.github?.pushBranches ?? [];
|
|
438
|
+
return branches.filter((branch) => branch.length > 0);
|
|
409
439
|
}
|
|
410
440
|
|
|
411
441
|
function renderYamlFlowList(values: string[]): string {
|
|
@@ -432,3 +462,33 @@ export function validateManagedFiles(root: string): number {
|
|
|
432
462
|
}
|
|
433
463
|
return failures;
|
|
434
464
|
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Non-blocking drift report: prints the per-file table and surfaces drift as
|
|
468
|
+
* GitHub Actions warning annotations (plain stderr elsewhere) without failing
|
|
469
|
+
* the run. Managed-file drift is derived state with its own remediation flow
|
|
470
|
+
* (the persistent managed-files PR); only `monorepo check` without --warn and
|
|
471
|
+
* PR-scoped gates treat it as an error.
|
|
472
|
+
*
|
|
473
|
+
* Under GitHub Actions the drifted-file count is published as the step output
|
|
474
|
+
* `drifted`, so downstream steps gate declaratively instead of parsing logs.
|
|
475
|
+
*/
|
|
476
|
+
export function warnOnManagedFileDrift(root: string): void {
|
|
477
|
+
const results = applyManagedFiles(root, 'check');
|
|
478
|
+
printResults(results);
|
|
479
|
+
const drifted = results.filter((result) => result.action === 'drifted');
|
|
480
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
481
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `drifted=${drifted.length}\n`);
|
|
482
|
+
}
|
|
483
|
+
if (drifted.length === 0) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
487
|
+
for (const result of drifted) {
|
|
488
|
+
console.log(
|
|
489
|
+
`::warning title=Managed file drift::${result.target} drifted from the @smoothbricks/cli template; run 'smoo monorepo update'`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
console.error(`${drifted.length} managed monorepo file(s) drifted (non-blocking). Run: smoo monorepo update`);
|
|
494
|
+
}
|