@gjsify/cli 0.36.0 → 0.38.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.
- package/dist/cli.gjs.mjs +169 -169
- package/lib/actions/build.d.ts +7 -0
- package/lib/actions/build.js +18 -3
- package/lib/bundler-pick.js +61 -87
- package/lib/cli-fail.d.ts +4 -7
- package/lib/cli-fail.js +29 -55
- package/lib/commands/affected.js +161 -286
- package/lib/commands/install.d.ts +63 -66
- package/lib/commands/install.js +372 -536
- package/lib/config.d.ts +14 -0
- package/lib/config.js +18 -1
- package/lib/templates/install.mjs.tmpl +164 -75
- package/lib/types/cli-build-options.d.ts +33 -67
- package/lib/types/command.d.ts +9 -12
- package/lib/types/config-data.d.ts +137 -221
- package/lib/utils/auth-npmrc.d.ts +5 -10
- package/lib/utils/auth-npmrc.js +11 -21
- package/lib/utils/bin-shim.d.ts +40 -25
- package/lib/utils/bin-shim.js +51 -26
- package/lib/utils/clear-targets.d.ts +8 -11
- package/lib/utils/clear-targets.js +22 -29
- package/lib/utils/copy-targets.d.ts +13 -19
- package/lib/utils/copy-targets.js +31 -44
- package/lib/utils/dep-aggregation.d.ts +7 -15
- package/lib/utils/dep-aggregation.js +9 -22
- package/lib/utils/detect-native-packages.d.ts +89 -122
- package/lib/utils/detect-native-packages.js +119 -169
- package/lib/utils/dir-link.d.ts +29 -48
- package/lib/utils/dir-link.js +33 -56
- package/lib/utils/discover-showcases.d.ts +6 -7
- package/lib/utils/discover-showcases.js +11 -15
- package/lib/utils/dlx-cache.d.ts +29 -38
- package/lib/utils/dlx-cache.js +53 -63
- package/lib/utils/force-exit.d.ts +0 -3
- package/lib/utils/force-exit.js +13 -24
- package/lib/utils/gi-search-path.d.ts +16 -24
- package/lib/utils/gi-search-path.js +54 -82
- package/lib/utils/gjs-bundle-guard.d.ts +5 -13
- package/lib/utils/gjs-bundle-guard.js +35 -57
- package/lib/utils/gjs-source-escape.d.ts +2 -2
- package/lib/utils/gjs-source-escape.js +21 -39
- package/lib/utils/gjsify-shim.d.ts +17 -20
- package/lib/utils/gjsify-shim.js +80 -120
- package/lib/utils/html-entry.d.ts +8 -12
- package/lib/utils/html-entry.js +19 -28
- package/lib/utils/install-backend-native.d.ts +123 -161
- package/lib/utils/install-backend-native.js +536 -737
- package/lib/utils/install-backend.d.ts +42 -57
- package/lib/utils/install-backend.js +16 -21
- package/lib/utils/install-cache-fs.d.ts +10 -15
- package/lib/utils/install-cache-fs.js +18 -26
- package/lib/utils/install-lock.d.ts +5 -6
- package/lib/utils/install-lock.js +46 -65
- package/lib/utils/install-packument-cache.d.ts +27 -33
- package/lib/utils/install-packument-cache.js +44 -57
- package/lib/utils/install-progress.d.ts +2 -9
- package/lib/utils/install-progress.js +14 -39
- package/lib/utils/install-tarball-cache.d.ts +14 -20
- package/lib/utils/install-tarball-cache.js +41 -59
- package/lib/utils/library-output.d.ts +9 -22
- package/lib/utils/library-output.js +19 -37
- package/lib/utils/load-npmrc.d.ts +5 -6
- package/lib/utils/load-npmrc.js +18 -27
- package/lib/utils/node-bundle-guard.d.ts +4 -8
- package/lib/utils/node-bundle-guard.js +21 -35
- package/lib/utils/node-script.js +40 -53
- package/lib/utils/node-version.d.ts +5 -14
- package/lib/utils/node-version.js +15 -24
- package/lib/utils/normalize-bundler-options.d.ts +7 -11
- package/lib/utils/normalize-bundler-options.js +24 -42
- package/lib/utils/npm-oidc.d.ts +23 -37
- package/lib/utils/npm-oidc.js +28 -53
- package/lib/utils/npm-otp-cache.d.ts +4 -4
- package/lib/utils/npm-otp-cache.js +20 -25
- package/lib/utils/npm-otp.d.ts +20 -30
- package/lib/utils/npm-otp.js +32 -51
- package/lib/utils/onboard-probe.d.ts +7 -13
- package/lib/utils/onboard-probe.js +16 -27
- package/lib/utils/parse-spec.d.ts +3 -3
- package/lib/utils/parse-spec.js +4 -5
- package/lib/utils/pkg-json-edit.d.ts +7 -12
- package/lib/utils/pkg-json-edit.js +13 -26
- package/lib/utils/pkg-json.d.ts +11 -18
- package/lib/utils/pkg-json.js +19 -28
- package/lib/utils/prefixed-output.d.ts +3 -4
- package/lib/utils/prefixed-output.js +13 -19
- package/lib/utils/prompt.d.ts +3 -4
- package/lib/utils/prompt.js +14 -18
- package/lib/utils/publish-diagnose.d.ts +11 -16
- package/lib/utils/publish-diagnose.js +15 -28
- package/lib/utils/publish-headers.d.ts +7 -11
- package/lib/utils/publish-headers.js +36 -65
- package/lib/utils/publishable-packages.d.ts +4 -6
- package/lib/utils/publishable-packages.js +14 -18
- package/lib/utils/resolve-gjs-entry.d.ts +5 -8
- package/lib/utils/resolve-gjs-entry.js +10 -27
- package/lib/utils/resolve-npm-package.d.ts +6 -11
- package/lib/utils/resolve-npm-package.js +29 -63
- package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
- package/lib/utils/resolve-plugin-by-name.js +9 -14
- package/lib/utils/run-gjs.d.ts +16 -21
- package/lib/utils/run-gjs.js +31 -56
- package/lib/utils/run-lifecycle-script.d.ts +5 -9
- package/lib/utils/run-lifecycle-script.js +23 -42
- package/lib/utils/run-node.d.ts +21 -33
- package/lib/utils/run-node.js +43 -64
- package/lib/utils/runtimes.d.ts +25 -35
- package/lib/utils/runtimes.js +34 -46
- package/lib/utils/spawn.d.ts +32 -48
- package/lib/utils/spawn.js +51 -84
- package/lib/utils/stdin.d.ts +2 -2
- package/lib/utils/stdin.js +10 -25
- package/lib/utils/suggest.d.ts +6 -7
- package/lib/utils/suggest.js +8 -10
- package/lib/utils/system-gi.d.ts +36 -27
- package/lib/utils/system-gi.js +85 -114
- package/lib/utils/trust-registry.d.ts +4 -8
- package/lib/utils/trust-registry.js +7 -15
- package/lib/utils/win32-command.d.ts +8 -14
- package/lib/utils/win32-command.js +29 -43
- package/lib/utils/workspace-root.js +7 -12
- package/package.json +18 -18
package/lib/commands/affected.js
CHANGED
|
@@ -1,117 +1,59 @@
|
|
|
1
|
-
// `gjsify affected
|
|
1
|
+
// `gjsify affected` — diff against `<base>` and emit the workspaces AFFECTED by
|
|
2
|
+
// the change set: seeds plus everything that transitively depends on them. CI
|
|
3
|
+
// feeds the output to `gjsify foreach test --include …`, so a single-package PR
|
|
4
|
+
// tests the touched workspace and its consumers instead of the whole monorepo.
|
|
2
5
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// EVERY VERDICT HERE IS SCOPED TO ONE WORKFLOW: `main.yml`. That is the single
|
|
7
|
+
// most misread thing in this file. "Ignored" NEVER means "irrelevant to the
|
|
8
|
+
// repository" — it means "cannot change what `main.yml` builds or tests". Sibling
|
|
9
|
+
// workflows gate themselves with their own `paths:` filters, so `packages/node-gi/**`
|
|
10
|
+
// and `packages/napi/**` are load-bearing build inputs to THEIR workflows and still
|
|
11
|
+
// belong in IGNORE here.
|
|
9
12
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// outputs. So "ignored" NEVER means "irrelevant to the repository" — it means
|
|
15
|
-
// "cannot change what `main.yml` builds or tests". Sibling workflows
|
|
16
|
-
// (`prebuilds.yml`, `audit-runtimes.yml`, `node-gi.yml`, `napi.yml`,
|
|
17
|
-
// `deploy-docs.yml`, …) gate themselves with their OWN `paths:` filters and are
|
|
18
|
-
// unaffected by anything decided here. `IGNORE` has always worked this way —
|
|
19
|
-
// `packages/node-gi/**` and `packages/napi/**` are load-bearing build inputs to
|
|
20
|
-
// their own workflows and ignored here — but the table read as a global claim,
|
|
21
|
-
// which is how a directory that is genuinely irrelevant to `main.yml` still
|
|
22
|
-
// looks like something that must not be ignored.
|
|
13
|
+
// Classifier order, first match wins: IGNORE → GLOBAL_TRIGGERS → SCRIPT_COUPLINGS
|
|
14
|
+
// → TEST_ONLY → CODE (map file→workspace, then walk reverse-dep edges). IGNORE runs
|
|
15
|
+
// first and therefore WINS OVER GLOBAL: `packages/infra/cli/README.md` must not
|
|
16
|
+
// force a full run just because `packages/infra/cli/` is a global trigger.
|
|
23
17
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
18
|
+
// Integration suites run when a closure member is a dependency of an
|
|
19
|
+
// `@gjsify/integration-*` workspace, or on a global trigger; e2e runs on any
|
|
20
|
+
// global trigger, an explicit `tests/e2e/**` touch, or a coupling that declares it.
|
|
26
21
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// `packages/infra/cli/README.md` must not force a full run just because
|
|
33
|
-
// `packages/infra/cli/` is a global trigger). Ignore wins over global.
|
|
22
|
+
// `include-args` CONTRACT (github-actions format) — read before changing its
|
|
23
|
+
// shape. The value is a SPACE-SEPARATED list of ALREADY-SHELL-SAFE tokens
|
|
24
|
+
// (`--include @gjsify/fs --include @gjsify/stream`) whose only supported
|
|
25
|
+
// consumption is an UNQUOTED `$INCLUDE_ARGS` expansion, where word splitting is
|
|
26
|
+
// the intent. It carries NO quoting of its own, deliberately:
|
|
34
27
|
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// BUILD SCRIPT instead of a dependency, so the closure walk has no edge
|
|
44
|
-
// to follow. Contributes EXTRA seeds (and may force a test tier). See
|
|
45
|
-
// the table for why this cannot be inferred and must be declared.
|
|
46
|
-
//
|
|
47
|
-
// 4. TEST_ONLY — every changed file under ONE workspace is a spec
|
|
48
|
-
// file, e2e fixture, or integration test. Seed = that workspace
|
|
49
|
-
// but SKIP the closure expansion (downstream consumers don't care
|
|
50
|
-
// about test code changes).
|
|
51
|
-
//
|
|
52
|
-
// 5. CODE (default) — `workspacesForChangedFiles` maps file → ws,
|
|
53
|
-
// then `affectedClosure` walks reverse-dep edges.
|
|
54
|
-
//
|
|
55
|
-
// Integration tests are gated separately: they run when any workspace
|
|
56
|
-
// in the closure appears as a `dependencies` entry of any
|
|
57
|
-
// `@gjsify/integration-*` workspace, OR on a `globalTrigger`. Same for
|
|
58
|
-
// e2e: any infra change OR explicit `tests/e2e/**` touch turns the e2e
|
|
59
|
-
// gate on.
|
|
60
|
-
//
|
|
61
|
-
// Output formats:
|
|
62
|
-
//
|
|
63
|
-
// --format=text (default) human-readable summary
|
|
64
|
-
// --format=json { global, workspaces[], runIntegration, runE2E, skipAll, reason }
|
|
65
|
-
// --format=globs one `@gjsify/<name>` per line
|
|
66
|
-
// --format=github-actions $GITHUB_OUTPUT key=value lines
|
|
67
|
-
//
|
|
68
|
-
// `include-args` CONTRACT (github-actions format) — read this before changing
|
|
69
|
-
// its shape. The value is a SPACE-SEPARATED list of ALREADY-SHELL-SAFE tokens:
|
|
70
|
-
//
|
|
71
|
-
// include-args=--include @gjsify/fs --include @gjsify/stream
|
|
72
|
-
//
|
|
73
|
-
// and its only supported consumption is an UNQUOTED `$INCLUDE_ARGS` expansion
|
|
74
|
-
// in a POSIX shell, where word splitting is exactly the intent. It carries NO
|
|
75
|
-
// quoting of its own, deliberately:
|
|
76
|
-
//
|
|
77
|
-
// * `main.yml` splices it into a `su testuser -c "… sh -c '…'"` nesting, and
|
|
78
|
-
// the result of a parameter expansion is NEVER re-scanned for quotes. When
|
|
79
|
-
// this emitted `--include '@gjsify/fs'`, the apostrophes became part of the
|
|
80
|
-
// glob, every pattern matched zero workspaces, and `foreach` exited 0 —
|
|
81
|
-
// CI "built" nothing on every selective PR run in 0.65 s (repo task #75).
|
|
82
|
-
// Pre-quoting a value for an unknown number of nesting levels cannot work;
|
|
83
|
-
// emitting tokens that need no quoting at any level can.
|
|
84
|
-
// * There are FIVE consumers of this one output — the build / browser-bundle
|
|
85
|
-
// / check / test steps splice it into a command line, and thirteen
|
|
28
|
+
// * `main.yml` splices it into a `su testuser -c "… sh -c '…'"` nesting, and the
|
|
29
|
+
// result of a parameter expansion is never re-scanned for quotes. When this
|
|
30
|
+
// emitted `--include '@gjsify/fs'` the apostrophes became part of the glob,
|
|
31
|
+
// every pattern matched zero workspaces, and `foreach` exited 0 — CI "built"
|
|
32
|
+
// nothing on every selective PR run, in 0.65 s. Pre-quoting for an unknown
|
|
33
|
+
// number of nesting levels cannot work; emitting tokens that need no quoting
|
|
34
|
+
// at any level can.
|
|
35
|
+
// * The output has two kinds of consumer: shell splices, and
|
|
86
36
|
// `contains(needs.changes.outputs.include-args, '@gjsify/…')` GitHub
|
|
87
|
-
// expressions
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// silently did nothing).
|
|
37
|
+
// expressions. Per-consumer unquoting would have to be repeated in the very
|
|
38
|
+
// nesting that produced the bug, and `contains()` is unaffected either way —
|
|
39
|
+
// it matched the quoted spelling too, which is why the gates kept working
|
|
40
|
+
// while the build silently did nothing.
|
|
92
41
|
//
|
|
93
42
|
// Shell-safety is ENFORCED, not assumed: `assertShellSafeWorkspaceName` rejects
|
|
94
|
-
// any
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// run. Failing towards "run everything" is the safe direction.
|
|
43
|
+
// any name outside `[A-Za-z0-9@._/-]`. npm names cannot contain those characters,
|
|
44
|
+
// so it never fires in practice; if it does, the non-zero exit leaves the classify
|
|
45
|
+
// step's outputs unset and CI falls back to a FULL run — failing towards "run
|
|
46
|
+
// everything" is the safe direction.
|
|
99
47
|
//
|
|
100
|
-
// `--changed-from-stdin`
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// idiom opens the relative file `./0` and the committed GJS classifier
|
|
105
|
-
// bundle died with `ENOENT … read '0'` on every `--changed-from-stdin` run.
|
|
48
|
+
// `--changed-from-stdin` reads paths from stdin instead of running `git diff`.
|
|
49
|
+
// Slurped via `utils/stdin.ts`, NOT `readFileSync(0)`: `@gjsify/fs` has no
|
|
50
|
+
// numeric-fd path, so the Node idiom opens the relative file `./0` and the
|
|
51
|
+
// committed GJS bundle died with `ENOENT … read '0'` on every such run.
|
|
106
52
|
import { spawnSync } from 'node:child_process';
|
|
107
|
-
// STATIC
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// step. That is why it survived from the command's first commit: every local
|
|
112
|
-
// run and every spec took the stdout branch. There is no reason for laziness
|
|
113
|
-
// here — the command already statically imports `node:child_process`, and the
|
|
114
|
-
// classifier bundle resolves `node:fs` through `@gjsify/fs` like any other.
|
|
53
|
+
// STATIC, never a lazy `require('node:fs')`: this package is ESM, so a bare
|
|
54
|
+
// `require` is a ReferenceError — and the only line that would execute it sits
|
|
55
|
+
// behind `if (process.env.GITHUB_OUTPUT)`, so it survives every local run and
|
|
56
|
+
// every spec and fires only inside a GitHub Actions step.
|
|
115
57
|
import { appendFileSync } from 'node:fs';
|
|
116
58
|
import { readStdinLines } from '../utils/stdin.js';
|
|
117
59
|
import { discoverWorkspaces, buildDependencyGraph, buildReverseDependencyGraph, affectedClosure, workspacesForChangedFiles, } from '@gjsify/workspace';
|
|
@@ -160,83 +102,61 @@ const GLOBAL_TRIGGERS = [
|
|
|
160
102
|
/^packages\/infra\/cli\//,
|
|
161
103
|
/^packages\/infra\/rolldown-plugin-gjsify\//,
|
|
162
104
|
/^packages\/infra\/resolve-npm\//,
|
|
163
|
-
// The test framework every spec imports
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// can break assertions anywhere. Treat it as a global trigger so a
|
|
168
|
-
// `@gjsify/unit` change re-runs the full suite.
|
|
105
|
+
// The test framework every spec imports, as a *devDependency* — which the
|
|
106
|
+
// prod-deps-only closure below does not walk, so a change to it would yield a
|
|
107
|
+
// near-empty closure and silently skip every downstream test, while a bug in a
|
|
108
|
+
// matcher can break assertions anywhere.
|
|
169
109
|
/^packages\/gjs\/unit\//,
|
|
170
|
-
// The composite action every CI job
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
110
|
+
// The composite action every CI job sets itself up with: a change alters the
|
|
111
|
+
// environment of the whole matrix. Listed EXPLICITLY — it previously forced a
|
|
112
|
+
// full run only by falling through to the "unmatched files" path, which is luck
|
|
113
|
+
// rather than intent.
|
|
174
114
|
/^\.github\/actions\//,
|
|
175
115
|
// Cross-cutting dep + lockfile + root config.
|
|
176
116
|
/^gjsify-lock\.json$/,
|
|
177
117
|
/^package\.json$/,
|
|
178
118
|
/^tsconfig[^/]*\.json$/,
|
|
179
|
-
// The workflow file itself
|
|
180
|
-
//
|
|
181
|
-
//
|
|
119
|
+
// The workflow file itself: a job-shape change is invisible until the workflow
|
|
120
|
+
// re-runs, so a path-filtered job cannot safely apply the new shape to an
|
|
121
|
+
// in-flight PR.
|
|
182
122
|
/^\.github\/workflows\/main\.yml$/,
|
|
183
123
|
/^scripts\/audit-runtimes\.mjs$/,
|
|
184
124
|
];
|
|
185
125
|
/**
|
|
186
126
|
* Inputs OWNED BY ANOTHER WORKFLOW — build-relevant, just not to `main.yml`.
|
|
187
127
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* The test
|
|
191
|
-
*
|
|
192
|
-
* every entry the answer is no, while the owning workflow has the path in its
|
|
193
|
-
* own `paths:` filter and would go red on a break.
|
|
194
|
-
*
|
|
195
|
-
* Adding a regex here is therefore a claim with two halves, and BOTH must
|
|
196
|
-
* hold: `main.yml` must not read the path, and some other workflow must.
|
|
128
|
+
* Adding a regex here is a claim with two halves and BOTH must hold: `main.yml`
|
|
129
|
+
* must not read the path, and some other workflow must have it in its own
|
|
130
|
+
* `paths:` filter so a break still reds a PR. The test is never "is this file
|
|
131
|
+
* unimportant" — none of these are.
|
|
197
132
|
*/
|
|
198
133
|
const OTHER_WORKFLOW_INPUTS = [
|
|
199
|
-
// Each sibling workflow's own definition. `main.yml` is deliberately
|
|
200
|
-
//
|
|
201
|
-
// invisible until the workflow re-runs.
|
|
134
|
+
// Each sibling workflow's own definition. `main.yml` is deliberately absent: it
|
|
135
|
+
// is a GLOBAL_TRIGGER instead.
|
|
202
136
|
/^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image|cancel-pr-runs)\.yml$/,
|
|
203
|
-
// `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// reds that workflow on the same PR. `main.yml` never executes any of it —
|
|
208
|
-
// it builds no prebuild and runs no emulated leg.
|
|
137
|
+
// `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and the
|
|
138
|
+
// classifier deciding which native packages a prebuild run rebuilds. It is in
|
|
139
|
+
// both of that workflow's `paths:` filters; `main.yml` builds no prebuild and
|
|
140
|
+
// runs no emulated leg.
|
|
209
141
|
/^\.github\/prebuild-toolchain\//,
|
|
210
|
-
// The REPO-SCOPED manifest-conformance rules (#847)
|
|
211
|
-
// `
|
|
212
|
-
// `
|
|
213
|
-
// --check --strict` on EVERY pull_request with no `paths:` filter at all,
|
|
214
|
-
// so this content is verified on every PR regardless of what is ignored
|
|
215
|
-
// here; `prebuilds.yml` additionally declares it a shared input (#843) and
|
|
216
|
-
// rebuilds every native package when it changes.
|
|
142
|
+
// The REPO-SCOPED manifest-conformance rules (#847), gated by
|
|
143
|
+
// `audit-runtimes.yml`, which runs on EVERY pull_request with no `paths:` filter;
|
|
144
|
+
// `prebuilds.yml` also treats it as a shared input (#843).
|
|
217
145
|
//
|
|
218
|
-
// The distinction
|
|
219
|
-
//
|
|
220
|
-
// `
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// other. Only the `scripts/`-side repo-scoped half is ignored, and that
|
|
224
|
-
// half is precisely the half `main.yml` does not load.
|
|
146
|
+
// The distinction is real, not a technicality: `main.yml` DOES run
|
|
147
|
+
// manifest-conformance code, but reaches it through
|
|
148
|
+
// `packages/infra/manifest-conformance/` — the PACKAGE, a normal workspace that
|
|
149
|
+
// maps and seeds like any other. Only the `scripts/`-side repo-scoped half is
|
|
150
|
+
// ignored, and that is precisely the half `main.yml` does not load.
|
|
225
151
|
/^scripts\/manifest-conformance\//,
|
|
226
|
-
// The authored status data (ADR 0016)
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// `status/`, which the grep for it confirms (the only hits are comments
|
|
230
|
-
// citing `open-todos.md` as a ledger) — while `audit-runtimes.yml` runs the
|
|
231
|
-
// `status-data` rule over exactly this data on EVERY pull_request with no
|
|
232
|
-
// `paths:` filter.
|
|
152
|
+
// The authored status data (ADR 0016) + its generator. Same two halves:
|
|
153
|
+
// `main.yml` never runs `status:generate` and never opens `status/`, while
|
|
154
|
+
// `audit-runtimes.yml` runs the `status-data` rule over it on every PR.
|
|
233
155
|
//
|
|
234
|
-
//
|
|
235
|
-
// generic `/\.md$/i`, so the
|
|
236
|
-
// showed up only on `status/status.json
|
|
237
|
-
// `unmatched`, and
|
|
238
|
-
// Editing one package's status prose paid for the whole matrix. Naming the
|
|
239
|
-
// directory makes the `.md` case intentional rather than incidental.
|
|
156
|
+
// Naming the directory makes the `.md` case intentional rather than incidental:
|
|
157
|
+
// `status/*.md` already fell into IGNORE via the generic `/\.md$/i`, so the gap
|
|
158
|
+
// showed up only on `status/status.json`, which matched nothing, landed in
|
|
159
|
+
// `unmatched`, and paid for a FULL matrix run over a file `main.yml` cannot read.
|
|
240
160
|
/^status\//,
|
|
241
161
|
/^scripts\/generate-status\.mjs$/,
|
|
242
162
|
];
|
|
@@ -247,33 +167,19 @@ const IGNORE = [
|
|
|
247
167
|
/^website\//,
|
|
248
168
|
/^docs\//,
|
|
249
169
|
...OTHER_WORKFLOW_INPUTS,
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
// @gjsify/node-gi (the Node-native GObject-Introspection engine, Axis 5) is
|
|
257
|
-
// NOT a gjsify workspace member — the GJS-first install/foreach tooling can't
|
|
258
|
-
// build its node-gyp addon, so the main workflow neither builds nor tests it.
|
|
259
|
-
// Its own `.github/workflows/node-gi.yml` is the source of truth (built +
|
|
260
|
-
// tested on Node). Without this carve-out its files map to no workspace and
|
|
261
|
-
// land in `unmatched`, forcing a conservative full run on every node-gi PR.
|
|
170
|
+
// Neither `@gjsify/node-gi` (the Node-native GI engine) nor `@gjsify/napi` (the
|
|
171
|
+
// N-API host over SpiderMonkey) is a gjsify workspace member: the GJS-first
|
|
172
|
+
// install/foreach tooling cannot build their node-gyp addons or Vala+C++/meson
|
|
173
|
+
// shim, so `main.yml` neither builds nor tests them and their own workflows are
|
|
174
|
+
// the source of truth. Without the carve-outs their files map to no workspace,
|
|
175
|
+
// land in `unmatched`, and force a full run on every node-gi/napi PR.
|
|
262
176
|
/^packages\/node-gi\//,
|
|
263
|
-
// @gjsify/napi (the N-API host over GJS's SpiderMonkey, the forward mirror of
|
|
264
|
-
// node-gi) is likewise NOT a gjsify workspace member — the GJS-first
|
|
265
|
-
// install/foreach tooling can't build its Vala+C++/meson shim or its node-gyp
|
|
266
|
-
// test addons, so the main workflow neither builds nor tests it. Its own
|
|
267
|
-
// `.github/workflows/napi.yml` is the source of truth. Without this carve-out
|
|
268
|
-
// its files map to no workspace and force a conservative full run on every
|
|
269
|
-
// napi PR.
|
|
270
177
|
/^packages\/napi\//,
|
|
271
178
|
// Flatpak build/distribution tooling (SDK-extension manifest + metainfo). No
|
|
272
179
|
// package-test consumers; its own `tests/e2e/flatpak-sdk-extension` runs on
|
|
273
|
-
// `tests/e2e/**`
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// counter-example. Worth revisiting here for the same reason.
|
|
180
|
+
// `tests/e2e/**` and global triggers. Worth revisiting: ignoring `.githooks/`
|
|
181
|
+
// for the same reason is what let a change walk past the suite guarding it (see
|
|
182
|
+
// SCRIPT_COUPLINGS), and IGNORE wins over every other rule.
|
|
277
183
|
/^flatpak\//,
|
|
278
184
|
/^LICENSE/,
|
|
279
185
|
/^\.gitignore$/,
|
|
@@ -289,43 +195,35 @@ const TEST_PATHS = [/\.spec\.[mc]?[tj]sx?$/, /^tests\/(e2e|integration)\//];
|
|
|
289
195
|
const SCRIPT_COUPLINGS = [
|
|
290
196
|
{
|
|
291
197
|
// `.githooks/pre-commit` is guarded by an e2e suite and reachable from no
|
|
292
|
-
// workspace at all —
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
// The same shape #1028 left a note about — whoever changes the GUARDED thing
|
|
299
|
-
// walks past the guard — and the reason it repeats is that the guard's
|
|
198
|
+
// workspace at all — not source, not a dependency, its consumer is git. So a
|
|
199
|
+
// change to it selected NOTHING and the e2e tier stayed off (that turns on
|
|
200
|
+
// for a `tests/e2e/**` touch): #1095 edited the hook's command line, merged
|
|
201
|
+
// green, and turned `main` red on the push run. Same shape #1028 noted —
|
|
202
|
+
// whoever changes the GUARDED thing walks past the guard, because the guard's
|
|
300
203
|
// trigger names the TEST's path instead of the thing under test.
|
|
301
204
|
//
|
|
302
|
-
// No extra seeds: there is no workspace to rebuild. The
|
|
303
|
-
// tier.
|
|
205
|
+
// No extra seeds: there is no workspace to rebuild. The tier is the point.
|
|
304
206
|
match: /^\.githooks\//,
|
|
305
207
|
seeds: [],
|
|
306
208
|
runE2E: true,
|
|
307
209
|
why: 'tests/e2e/git-hooks-cli-bundle-staleness drives .githooks/pre-commit',
|
|
308
210
|
},
|
|
309
211
|
{
|
|
310
|
-
// `templates/*` ARE workspaces
|
|
311
|
-
//
|
|
312
|
-
// `@gjsify/
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// the result into `dist-templates/`), so there is no dependency edge
|
|
320
|
-
// either. `dist-templates/` is a build-output cache candidate, so once
|
|
321
|
-
// it is stale it stays stale.
|
|
212
|
+
// `templates/*` ARE workspaces, so a template change already seeds its own
|
|
213
|
+
// `@gjsify/template-<name>` — not the consumer that matters. That consumer is
|
|
214
|
+
// `@gjsify/create-app`, unreachable twice over: it lives at
|
|
215
|
+
// `packages/infra/create-gjsify/`, so no directory-name search finds it, and
|
|
216
|
+
// it pulls the templates in through its own `scripts/process-template.mjs`
|
|
217
|
+
// build step (which resolves each `workspace:^` specifier and copies the
|
|
218
|
+
// result into `dist-templates/`), so there is no dependency edge either.
|
|
219
|
+
// `dist-templates/` is a build-output cache candidate, so once stale it
|
|
220
|
+
// stays stale.
|
|
322
221
|
match: /^templates\//,
|
|
323
222
|
seeds: ['@gjsify/create-app'],
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// also touching root `package.json` and tripping a global trigger.
|
|
223
|
+
// `tests/e2e/create-app` is the ONLY thing that would notice — it scaffolds a
|
|
224
|
+
// project out of `dist-templates/` and builds it. Without this a
|
|
225
|
+
// templates-only PR got neither the regenerated templates nor that suite;
|
|
226
|
+
// #853 passed only by also touching root `package.json`.
|
|
329
227
|
runE2E: true,
|
|
330
228
|
why: '@gjsify/create-app build → node scripts/process-template.mjs reads templates/',
|
|
331
229
|
},
|
|
@@ -342,11 +240,8 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
342
240
|
skipAll: true,
|
|
343
241
|
};
|
|
344
242
|
}
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
// even inside a GLOBAL_TRIGGERS directory: `packages/infra/cli/README.md`
|
|
348
|
-
// must NOT force a full run just because `packages/infra/cli/` is a global
|
|
349
|
-
// trigger. Ignore wins over global.
|
|
243
|
+
// Ignored files go FIRST, before the global-trigger check: ignore wins over
|
|
244
|
+
// global, so `packages/infra/cli/README.md` must not force a full run.
|
|
350
245
|
const remaining = [];
|
|
351
246
|
for (const f of files) {
|
|
352
247
|
if (IGNORE.some((re) => re.test(f)))
|
|
@@ -378,12 +273,10 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
378
273
|
}
|
|
379
274
|
}
|
|
380
275
|
}
|
|
381
|
-
// Map files → workspaces. Files outside any workspace stay in `unmatched`.
|
|
382
276
|
const { matched, unmatched } = workspacesForChangedFiles(workspaces, remaining);
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// and the declared seeds would never be consulted.
|
|
277
|
+
// Seeds no graph edge can reach. BEFORE the `unmatched` bail-out, so the table
|
|
278
|
+
// also works for a coupled directory that is not itself a workspace — such a
|
|
279
|
+
// file would otherwise force a full run and never consult the declared seeds.
|
|
387
280
|
const knownNames = new Set(workspaces.map((w) => w.name));
|
|
388
281
|
const couplingSeeds = new Set();
|
|
389
282
|
const couplingAccounted = new Set();
|
|
@@ -395,11 +288,10 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
395
288
|
for (const h of hits)
|
|
396
289
|
couplingAccounted.add(h);
|
|
397
290
|
for (const s of c.seeds) {
|
|
398
|
-
// A declared seed that is not a workspace means the table
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// never degrade quietly back into the bug it exists to prevent.
|
|
291
|
+
// A declared seed that is not a workspace means the table drifted from the
|
|
292
|
+
// tree (renamed, moved, removed). Fail towards the full run and SAY SO: a
|
|
293
|
+
// missing rebuild here is invisible, so this must never degrade quietly
|
|
294
|
+
// back into the bug the table exists to prevent.
|
|
403
295
|
if (!knownNames.has(s)) {
|
|
404
296
|
return {
|
|
405
297
|
global: true,
|
|
@@ -416,10 +308,8 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
416
308
|
if (c.runE2E)
|
|
417
309
|
couplingRunE2E = true;
|
|
418
310
|
}
|
|
419
|
-
// Unmatched-but-not-ignored
|
|
420
|
-
// the
|
|
421
|
-
// a script in `scripts/` we haven't carved out, a refs/-adjacent file.
|
|
422
|
-
// A file a coupling claimed is accounted for and does not count here.
|
|
311
|
+
// Unmatched-but-not-ignored (a new top-level dotfile, an uncarved `scripts/`
|
|
312
|
+
// file) falls back to the full run. A file a coupling claimed does not count.
|
|
423
313
|
const stillUnmatched = unmatched.filter((f) => !couplingAccounted.has(f));
|
|
424
314
|
if (stillUnmatched.length > 0) {
|
|
425
315
|
return {
|
|
@@ -433,14 +323,13 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
433
323
|
}
|
|
434
324
|
for (const s of couplingSeeds)
|
|
435
325
|
matched.add(s);
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
// extra seeds exist precisely because a downstream consumer DOES care.
|
|
326
|
+
// Every remaining file is a spec / e2e / integration path under ONE workspace, so
|
|
327
|
+
// skip the closure: test code has no downstream consumers. A coupling disables
|
|
328
|
+
// this — its extra seeds exist precisely because a consumer DOES care.
|
|
440
329
|
const testOnly = couplingSeeds.size === 0 && remaining.every((f) => TEST_PATHS.some((re) => re.test(f)));
|
|
441
330
|
if (testOnly && matched.size === 1) {
|
|
442
331
|
const only = [...matched][0];
|
|
443
|
-
//
|
|
332
|
+
// An e2e- or integration-only change still needs its own job.
|
|
444
333
|
const touchedE2E = remaining.some((f) => f.startsWith('tests/e2e/'));
|
|
445
334
|
const touchedIntegration = remaining.some((f) => f.startsWith('tests/integration/'));
|
|
446
335
|
return {
|
|
@@ -452,29 +341,23 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
452
341
|
skipAll: false,
|
|
453
342
|
};
|
|
454
343
|
}
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
// devDependency (the only such spec imports are self-imports, which are
|
|
467
|
-
// covered by the package being its own seed); the universal test framework
|
|
468
|
-
// `@gjsify/unit` is handled by GLOBAL_TRIGGERS above; and every push-to-
|
|
469
|
-
// main + the nightly cron still run the FULL suite as the backstop.
|
|
344
|
+
// PRODUCTION dependencies only. The closure answers "whose tests must re-run
|
|
345
|
+
// because a package they depend on changed", which is a RUNTIME relationship;
|
|
346
|
+
// walking devDependency edges conflated it with the build/test toolchain, so ANY
|
|
347
|
+
// single-package change fanned out to ~210 of 221 workspaces and selective CI
|
|
348
|
+
// bought almost nothing. Prod-only collapses a typical seed to a handful (sqlite
|
|
349
|
+
// 210→4, fetch 210→34) while keeping every real runtime dependent.
|
|
350
|
+
//
|
|
351
|
+
// Safe against under-selection: no `*.spec.ts` imports a CROSS-PACKAGE sibling
|
|
352
|
+
// via a devDependency (the only such imports are self-imports, covered by the
|
|
353
|
+
// package being its own seed), `@gjsify/unit` is a GLOBAL_TRIGGER above, and
|
|
354
|
+
// every push-to-main plus the nightly cron still run the FULL suite.
|
|
470
355
|
const reverse = buildReverseDependencyGraph(workspaces, { includeDev: false });
|
|
471
356
|
const closure = affectedClosure(reverse, [...matched]);
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
// as `dependencies`, but the wider walk also tolerates a devDep-declared
|
|
477
|
-
// edge. Over-running an integration suite is cheap; missing one is not.
|
|
357
|
+
// `includeDev: true`, deliberately broader than the reverse closure: integration
|
|
358
|
+
// packages declare the pillars they exercise as `dependencies`, but the wider
|
|
359
|
+
// walk tolerates a devDep-declared edge too. Over-running an integration suite is
|
|
360
|
+
// cheap; missing one is not.
|
|
478
361
|
const forward = buildDependencyGraph(workspaces, { includeDev: true });
|
|
479
362
|
let runIntegration = false;
|
|
480
363
|
for (const [from, deps] of forward.edges) {
|
|
@@ -488,8 +371,6 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
488
371
|
}
|
|
489
372
|
}
|
|
490
373
|
}
|
|
491
|
-
// A `tests/e2e/**` touch turns the tier on, and so does any coupling that
|
|
492
|
-
// declares e2e as its only real coverage (see SCRIPT_COUPLINGS).
|
|
493
374
|
const runE2E = remaining.some((f) => f.startsWith('tests/e2e/')) || couplingRunE2E;
|
|
494
375
|
return {
|
|
495
376
|
global: false,
|
|
@@ -504,33 +385,30 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
504
385
|
function isIntegrationWorkspace(name) {
|
|
505
386
|
return name.startsWith('@gjsify/integration-');
|
|
506
387
|
}
|
|
507
|
-
// ─── git diff + stdin ──────────────────────────────────────────────────────
|
|
508
388
|
function runGitDiff(cwd, base, head) {
|
|
509
|
-
// `
|
|
510
|
-
//
|
|
511
|
-
//
|
|
389
|
+
// `base...head` lists changed paths on `head` relative to the MERGE-BASE, which
|
|
390
|
+
// matches what a GitHub PR diff shows and survives stacked PRs without picking up
|
|
391
|
+
// commits from base.
|
|
512
392
|
const r = spawnSync('git', ['diff', '--name-only', `${base}...${head}`], {
|
|
513
393
|
cwd,
|
|
514
394
|
encoding: 'utf8',
|
|
515
395
|
});
|
|
516
396
|
if (r.status !== 0) {
|
|
517
|
-
//
|
|
518
|
-
//
|
|
397
|
+
// CI falls back to a full run via `continue-on-error: true` on the classify
|
|
398
|
+
// step.
|
|
519
399
|
process.stderr.write(`gjsify affected: git diff failed (${r.status}): ${r.stderr.trim()}\n`);
|
|
520
|
-
// Under GJS `process.exit()` is DEFERRED (no atexit — the call
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
// caller; on Node the exit(2) halts first and the throw is dead.
|
|
400
|
+
// Under GJS `process.exit()` is DEFERRED (no atexit — the call returns), so in
|
|
401
|
+
// this synchronous, value-returning helper a bare exit fell through to the
|
|
402
|
+
// `return` below and handed the caller a diff parsed from a failed git run.
|
|
403
|
+
// `return process.exit(…)` cannot help either: it would hand the caller the
|
|
404
|
+
// pending exit promise as a `string[]`. The throw is what stops the caller; on
|
|
405
|
+
// Node the exit(2) halts first and the throw is dead.
|
|
527
406
|
// oxlint-disable-next-line gjsify/deferred-process-exit -- the throw below IS the halt for the GJS path; see the comment above.
|
|
528
407
|
process.exit(2);
|
|
529
408
|
throw new Error(`gjsify affected: git diff failed (${r.status})`);
|
|
530
409
|
}
|
|
531
410
|
return r.stdout.split('\n').filter(Boolean);
|
|
532
411
|
}
|
|
533
|
-
// ─── Output ────────────────────────────────────────────────────────────────
|
|
534
412
|
function emit(format, r) {
|
|
535
413
|
if (format === 'json') {
|
|
536
414
|
process.stdout.write(JSON.stringify(r) + '\n');
|
|
@@ -542,8 +420,8 @@ function emit(format, r) {
|
|
|
542
420
|
return;
|
|
543
421
|
}
|
|
544
422
|
if (format === 'github-actions') {
|
|
545
|
-
// Unquoted, space-separated tokens — see the `include-args` CONTRACT
|
|
546
|
-
//
|
|
423
|
+
// Unquoted, space-separated tokens — see the `include-args` CONTRACT at the
|
|
424
|
+
// top of this file. Do NOT re-add quoting here.
|
|
547
425
|
const includeArgs = r.global
|
|
548
426
|
? ''
|
|
549
427
|
: r.workspaces.map((n) => `--include ${assertShellSafeWorkspaceName(n)}`).join(' ');
|
|
@@ -557,7 +435,6 @@ function emit(format, r) {
|
|
|
557
435
|
`reason=${r.reason}`,
|
|
558
436
|
];
|
|
559
437
|
if (out) {
|
|
560
|
-
// GitHub Actions: append to $GITHUB_OUTPUT.
|
|
561
438
|
for (const l of lines)
|
|
562
439
|
appendFileSync(out, `${l}\n`);
|
|
563
440
|
}
|
|
@@ -578,12 +455,10 @@ function emit(format, r) {
|
|
|
578
455
|
for (const name of r.workspaces)
|
|
579
456
|
process.stdout.write(` - ${name}\n`);
|
|
580
457
|
}
|
|
581
|
-
// A
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
// `include-args` contract rather than a real runtime branch. Returns the name
|
|
586
|
-
// so it can be used inline at the emit site.
|
|
458
|
+
// A name that survives an UNQUOTED shell expansion unchanged: no whitespace, no
|
|
459
|
+
// quote/escape characters, no glob metacharacters, no `$`, nothing the shell reads
|
|
460
|
+
// as an operator. npm names are a strict subset, so this is a machine-checked
|
|
461
|
+
// restatement of the `include-args` contract rather than a real runtime branch.
|
|
587
462
|
const SHELL_SAFE_WORKSPACE_NAME = /^[A-Za-z0-9@][A-Za-z0-9@._/-]*$/;
|
|
588
463
|
function assertShellSafeWorkspaceName(name) {
|
|
589
464
|
if (SHELL_SAFE_WORKSPACE_NAME.test(name))
|
|
@@ -592,10 +467,10 @@ function assertShellSafeWorkspaceName(name) {
|
|
|
592
467
|
` \`include-args\` token (the value is consumed by an UNQUOTED $INCLUDE_ARGS expansion — see the\n` +
|
|
593
468
|
` contract at the top of commands/affected.ts). Rename the workspace, or teach this command a\n` +
|
|
594
469
|
` transport the consumers can unquote. Refusing to emit a filter that would silently mis-match.\n`);
|
|
595
|
-
// NOT unreachable under GJS: `process.exit()` is DEFERRED there (no atexit
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
470
|
+
// NOT unreachable under GJS: `process.exit()` is DEFERRED there (no atexit — the
|
|
471
|
+
// call returns), so the throw below is what stops an unsafe name from reaching the
|
|
472
|
+
// emit site, and the exit code rides the scheduled teardown. On Node the exit(2)
|
|
473
|
+
// halts first and the throw is dead code.
|
|
599
474
|
// oxlint-disable-next-line gjsify/deferred-process-exit -- the throw below IS the halt for the GJS path; see the comment above.
|
|
600
475
|
process.exit(2);
|
|
601
476
|
throw new Error(`gjsify affected: unsafe workspace name ${name}`);
|