@smoothbricks/cli 0.11.12 → 0.11.14
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 +104 -20
- package/dist/cli.js +6 -1
- package/dist/lib/json.d.ts +51 -0
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +111 -67
- package/dist/monorepo/ci-workflow.d.ts +27 -2
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +151 -11
- package/dist/monorepo/index.d.ts +2 -0
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +1 -1
- package/dist/monorepo/managed-files.d.ts +3 -1
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +3 -0
- package/dist/monorepo/packed-package.d.ts +3 -3
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +11 -6
- package/dist/monorepo/packs/index.d.ts +8 -0
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +6 -5
- package/dist/monorepo/publish-workflow.d.ts +8 -1
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +20 -6
- package/managed/raw/tooling/direnv/devenv.smoo.nix +28 -4
- package/managed/raw/tooling/direnv/secret-references.ts +138 -53
- package/managed/templates/github/actions/save-nix-devenv/action.yml +2 -2
- package/managed/templates/github/actions/setup-devenv/action.yml +54 -5
- package/package.json +2 -2
- package/src/cli.ts +10 -1
- package/src/lib/json.ts +52 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +183 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +32 -0
- package/src/monorepo/ci-workflow.ts +180 -11
- package/src/monorepo/index.ts +6 -1
- package/src/monorepo/managed-files.ts +6 -0
- package/src/monorepo/packed-package.ts +21 -6
- package/src/monorepo/packs/index.ts +14 -5
- package/src/monorepo/publish-workflow.ts +32 -5
- package/src/monorepo/secret-references.test.ts +28 -2
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider-neutral local secret resolution for smoo-managed repositories.
|
|
3
3
|
*
|
|
4
|
-
* Reads the root package.json `smoo.secrets` map
|
|
5
|
-
* `PackageSecretCommand
|
|
6
|
-
* packages/cli/src/lib/json.ts. This file is
|
|
7
|
-
* from `tooling/direnv/setup-environment.ts`
|
|
8
|
-
* workspace package and no Typia transform exist
|
|
9
|
-
* hand-validated here against
|
|
10
|
-
* aligned; smoo's Typia validation fails the manifest at
|
|
11
|
-
* anything this file would reject at runtime.
|
|
4
|
+
* Reads the root package.json `smoo.secrets` map and `smoo.remoteCache` block
|
|
5
|
+
* — the bootstrap twins of `PackageSecretCommand`, `PackageSmooConfig.secrets`
|
|
6
|
+
* and `PackageRemoteCacheConfig` in packages/cli/src/lib/json.ts. This file is
|
|
7
|
+
* a managed raw script: it runs from `tooling/direnv/setup-environment.ts`
|
|
8
|
+
* BEFORE `bun install`, when no workspace package and no Typia transform exist
|
|
9
|
+
* yet, so both shapes are hand-validated here against exactly what json.ts
|
|
10
|
+
* declares. Keep them aligned; smoo's Typia validation fails the manifest at
|
|
11
|
+
* generation time for anything this file would reject at runtime.
|
|
12
12
|
*
|
|
13
13
|
* Routing per declared variable, in first-match order:
|
|
14
14
|
*
|
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
* Failures aggregate so a single direnv reload surfaces every problem.
|
|
27
27
|
* Error text names variables and exit codes only: secret values, provider
|
|
28
28
|
* stdout/stderr, and command arguments are never echoed.
|
|
29
|
+
*
|
|
30
|
+
* The variable `smoo.remoteCache.tokenSecret` names is never resolved here,
|
|
31
|
+
* nor anywhere at shell entry. A remote cache is an optimization, and shell
|
|
32
|
+
* entry happens on every direnv reload and every `devenv shell -- <command>`:
|
|
33
|
+
* a provider command run there is a credential prompt on each of them. Nx
|
|
34
|
+
* reads NX_SELF_HOSTED_REMOTE_CACHE_SERVER and _ACCESS_TOKEN from the
|
|
35
|
+
* environment it runs in — CI injects them, a developer exports the token
|
|
36
|
+
* once in the terminal that wants the cache — and every nested shell inherits
|
|
37
|
+
* them. Absent, Nx keeps to its local cache.
|
|
29
38
|
*/
|
|
30
39
|
import { existsSync, readFileSync } from 'node:fs';
|
|
31
40
|
import { join } from 'node:path';
|
|
@@ -69,7 +78,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
69
78
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
function isNonemptyEnvValue(value: string | undefined):
|
|
81
|
+
function isNonemptyEnvValue(value: string | undefined): value is string {
|
|
73
82
|
return value !== undefined && value.length > 0;
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -80,9 +89,9 @@ function isNonemptyEnvValue(value: string | undefined): boolean {
|
|
|
80
89
|
*/
|
|
81
90
|
export function parseSmooSecrets(packageJson: unknown): Readonly<Record<string, SecretSpec>> {
|
|
82
91
|
if (!isRecord(packageJson)) return {};
|
|
83
|
-
const smoo = packageJson
|
|
92
|
+
const smoo = packageJson.smoo;
|
|
84
93
|
if (!isRecord(smoo)) return {};
|
|
85
|
-
const secrets = smoo
|
|
94
|
+
const secrets = smoo.secrets;
|
|
86
95
|
if (secrets === undefined) return {};
|
|
87
96
|
if (!isRecord(secrets)) {
|
|
88
97
|
throw new Error('smoo.secrets must map environment variable names to { command: [string, ...] }');
|
|
@@ -103,7 +112,7 @@ function parseSecretSpec(name: string, spec: unknown): SecretSpec {
|
|
|
103
112
|
if (!isRecord(spec)) {
|
|
104
113
|
throw new Error(`smoo.secrets.${name}: each entry must be an object with a command array`);
|
|
105
114
|
}
|
|
106
|
-
const raw = spec
|
|
115
|
+
const raw = spec.command;
|
|
107
116
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
108
117
|
throw new Error(`smoo.secrets.${name}.command: must be an array of at least one string`);
|
|
109
118
|
}
|
|
@@ -116,6 +125,49 @@ function parseSecretSpec(name: string, spec: unknown): SecretSpec {
|
|
|
116
125
|
return { command: [first, ...rest] };
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The declared remote cache as a local shell needs it. `internalServer` is
|
|
130
|
+
* deliberately absent: it is the address a managed runner reaches inside its
|
|
131
|
+
* own network, and a shell that loads this file is by definition outside it,
|
|
132
|
+
* so a developer machine always takes the public `server`.
|
|
133
|
+
*/
|
|
134
|
+
export interface RemoteCacheSpec {
|
|
135
|
+
readonly server: string;
|
|
136
|
+
/** Variable holding the cache token: an ambient value, or a `smoo.secrets` entry resolved here. */
|
|
137
|
+
readonly tokenSecret: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validates the `smoo.remoteCache` block out of an already-parsed
|
|
142
|
+
* package.json. Absent `smoo` or `smoo.remoteCache` is no cache; a malformed
|
|
143
|
+
* declaration throws naming the field.
|
|
144
|
+
*/
|
|
145
|
+
export function parseSmooRemoteCache(packageJson: unknown): RemoteCacheSpec | null {
|
|
146
|
+
if (!isRecord(packageJson)) return null;
|
|
147
|
+
const smoo = packageJson.smoo;
|
|
148
|
+
if (!isRecord(smoo)) return null;
|
|
149
|
+
const remoteCache = smoo.remoteCache;
|
|
150
|
+
if (remoteCache === undefined) return null;
|
|
151
|
+
if (!isRecord(remoteCache)) {
|
|
152
|
+
throw new Error('smoo.remoteCache must be an object with { server, tokenSecret }');
|
|
153
|
+
}
|
|
154
|
+
const server = remoteCache.server;
|
|
155
|
+
const tokenSecret = remoteCache.tokenSecret;
|
|
156
|
+
// A trailing slash is refused rather than trimmed: Nx appends
|
|
157
|
+
// `/v1/cache/<hash>`, so the doubled slash is a route that answers 404
|
|
158
|
+
// forever, and silently repairing the manifest here would leave managed CI
|
|
159
|
+
// — which reads the same field — disagreeing with this shell.
|
|
160
|
+
if (typeof server !== 'string' || server.length === 0 || server.endsWith('/')) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
'smoo.remoteCache.server must be an origin with no trailing slash, e.g. https://nx-cache.example.net',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (typeof tokenSecret !== 'string' || !ENV_NAME.test(tokenSecret)) {
|
|
166
|
+
throw new Error('smoo.remoteCache.tokenSecret must name the environment variable holding the cache token');
|
|
167
|
+
}
|
|
168
|
+
return { server, tokenSecret };
|
|
169
|
+
}
|
|
170
|
+
|
|
119
171
|
/** Every `${VAR}` referenced from `.npmrc` text; an absent file contributes nothing. */
|
|
120
172
|
export function registryAuthEnvNames(npmrcText: string | null): ReadonlySet<string> {
|
|
121
173
|
const names = new Set<string>();
|
|
@@ -131,47 +183,64 @@ type SecretOutcome =
|
|
|
131
183
|
| { readonly name: string; readonly kind: 'resolved'; readonly value: string }
|
|
132
184
|
| { readonly name: string; readonly kind: 'failed'; readonly guidance: string };
|
|
133
185
|
|
|
186
|
+
/**
|
|
187
|
+
* One variable's routing, the four rules in the header applied in order. It is
|
|
188
|
+
* a value rather than a throw so each caller presents a failure in its own
|
|
189
|
+
* terms: an install refuses, the remote cache turns itself off.
|
|
190
|
+
*/
|
|
191
|
+
async function routeSecret(
|
|
192
|
+
name: string,
|
|
193
|
+
spec: SecretSpec,
|
|
194
|
+
context: {
|
|
195
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
196
|
+
readonly registryIntentEnvs: ReadonlySet<string>;
|
|
197
|
+
readonly run: SecretCommandRunner;
|
|
198
|
+
},
|
|
199
|
+
): Promise<SecretOutcome> {
|
|
200
|
+
const { env } = context;
|
|
201
|
+
if (isNonemptyEnvValue(env[name])) {
|
|
202
|
+
return { name, kind: 'env-wins' };
|
|
203
|
+
}
|
|
204
|
+
if (isNonemptyEnvValue(env.CI)) {
|
|
205
|
+
return {
|
|
206
|
+
name,
|
|
207
|
+
kind: 'failed',
|
|
208
|
+
guidance:
|
|
209
|
+
'CI does not run secret provider commands — inject this variable into the job environment from the CI secret store',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (isNonemptyEnvValue(env.COWSHED_WORKSPACE_TOKEN) && context.registryIntentEnvs.has(name)) {
|
|
213
|
+
return {
|
|
214
|
+
name,
|
|
215
|
+
kind: 'failed',
|
|
216
|
+
guidance:
|
|
217
|
+
'referenced from .npmrc and absent in this cowshed workspace — enroll registry credentials through the cowshed gateway; gateway-managed workspaces never resolve .npmrc registry variables via local provider commands',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const value = (await context.run(spec.command)).replace(/\r?\n$/, '');
|
|
222
|
+
if (value.length === 0) {
|
|
223
|
+
return {
|
|
224
|
+
name,
|
|
225
|
+
kind: 'failed',
|
|
226
|
+
guidance:
|
|
227
|
+
'provider command produced no output — run it locally to see why (its arguments and output are never logged here)',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return { name, kind: 'resolved', value };
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return { name, kind: 'failed', guidance: describeCommandFailure(error) };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
134
236
|
export async function resolveSecrets(request: SecretResolutionRequest): Promise<Readonly<Record<string, string>>> {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
237
|
+
const context = {
|
|
238
|
+
env: request.env,
|
|
239
|
+
registryIntentEnvs: request.registryIntentEnvs,
|
|
240
|
+
run: request.runCommand ?? runSecretCommand,
|
|
241
|
+
};
|
|
139
242
|
const outcomes = await Promise.all(
|
|
140
|
-
Object.entries(request.secrets).map(async ([name, spec])
|
|
141
|
-
if (isNonemptyEnvValue(env[name])) {
|
|
142
|
-
return { name, kind: 'env-wins' };
|
|
143
|
-
}
|
|
144
|
-
if (ci) {
|
|
145
|
-
return {
|
|
146
|
-
name,
|
|
147
|
-
kind: 'failed',
|
|
148
|
-
guidance:
|
|
149
|
-
'CI does not run secret provider commands — inject this variable into the job environment from the CI secret store',
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
if (cowshed && request.registryIntentEnvs.has(name)) {
|
|
153
|
-
return {
|
|
154
|
-
name,
|
|
155
|
-
kind: 'failed',
|
|
156
|
-
guidance:
|
|
157
|
-
'referenced from .npmrc and absent in this cowshed workspace — enroll registry credentials through the cowshed gateway; gateway-managed workspaces never resolve .npmrc registry variables via local provider commands',
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
const value = (await run(spec.command)).replace(/\r?\n$/, '');
|
|
162
|
-
if (value.length === 0) {
|
|
163
|
-
return {
|
|
164
|
-
name,
|
|
165
|
-
kind: 'failed',
|
|
166
|
-
guidance:
|
|
167
|
-
'provider command produced no output — run it locally to see why (its arguments and output are never logged here)',
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return { name, kind: 'resolved', value };
|
|
171
|
-
} catch (error) {
|
|
172
|
-
return { name, kind: 'failed', guidance: describeCommandFailure(error) };
|
|
173
|
-
}
|
|
174
|
-
}),
|
|
243
|
+
Object.entries(request.secrets).map(async ([name, spec]) => routeSecret(name, spec, context)),
|
|
175
244
|
);
|
|
176
245
|
const failures = outcomes.filter(
|
|
177
246
|
(outcome): outcome is Extract<SecretOutcome, { kind: 'failed' }> => outcome.kind === 'failed',
|
|
@@ -242,11 +311,28 @@ const runSecretCommand: SecretCommandRunner = async (argv) => {
|
|
|
242
311
|
return stdout;
|
|
243
312
|
};
|
|
244
313
|
|
|
314
|
+
/**
|
|
315
|
+
* The declared secrets an install needs, resolved before it runs. The cache
|
|
316
|
+
* token is deliberately not among them: a remote cache is an optimization, so
|
|
317
|
+
* its credential is resolved by the shell's cache export — where an
|
|
318
|
+
* unreachable secret provider costs a stderr line — while every secret an
|
|
319
|
+
* install actually depends on still refuses loudly here. Which variable that
|
|
320
|
+
* is comes from `smoo.remoteCache.tokenSecret` rather than a second flag, so
|
|
321
|
+
* the two declarations cannot disagree.
|
|
322
|
+
*/
|
|
245
323
|
export async function resolveSecretEnvironment(
|
|
246
324
|
options: SecretResolutionOptions,
|
|
247
325
|
): Promise<Readonly<Record<string, string>>> {
|
|
326
|
+
const packageJson = readPackageJson(options.root);
|
|
327
|
+
const cacheToken = parseSmooRemoteCache(packageJson)?.tokenSecret;
|
|
328
|
+
const required: Record<string, SecretSpec> = {};
|
|
329
|
+
for (const [name, spec] of Object.entries(parseSmooSecrets(packageJson))) {
|
|
330
|
+
if (name !== cacheToken) {
|
|
331
|
+
required[name] = spec;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
248
334
|
return resolveSecrets({
|
|
249
|
-
secrets:
|
|
335
|
+
secrets: required,
|
|
250
336
|
registryIntentEnvs: registryAuthEnvNames(readNpmrcText(options.root)),
|
|
251
337
|
env: options.env ?? process.env,
|
|
252
338
|
runCommand: options.runCommand,
|
|
@@ -273,7 +359,6 @@ function readPackageJson(root: string): unknown {
|
|
|
273
359
|
}
|
|
274
360
|
return parsed;
|
|
275
361
|
}
|
|
276
|
-
|
|
277
362
|
function readNpmrcText(root: string): string | null {
|
|
278
363
|
const npmrcPath = join(root, '.npmrc');
|
|
279
364
|
if (!existsSync(npmrcPath)) return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: Save Nix/devenv caches
|
|
2
2
|
description: >
|
|
3
|
-
Save the devenv/direnv eval-cache segment after the job, on every runner where setup missed and build-shell
|
|
4
|
-
nix-eval-cache.db. The Nix store cache saves itself in setup-devenv's post phase (ephemeral runners only).
|
|
3
|
+
Save the devenv/direnv eval-cache segment after the job, on every ephemeral runner where setup missed and build-shell
|
|
4
|
+
produced nix-eval-cache.db. The Nix store cache saves itself in setup-devenv's post phase (ephemeral runners only).
|
|
5
5
|
|
|
6
6
|
inputs:
|
|
7
7
|
devenv-cache-hit:
|
|
@@ -8,7 +8,7 @@ description: >
|
|
|
8
8
|
outputs:
|
|
9
9
|
devenv-cache-hit:
|
|
10
10
|
description: Whether the devenv/direnv cache restored from the exact primary key.
|
|
11
|
-
value: ${{ steps.devenv-cache.outputs.cache-hit || 'false' }}
|
|
11
|
+
value: ${{ steps.runner-kind.outputs.host == 'true' || steps.devenv-cache.outputs.cache-hit || 'false' }}
|
|
12
12
|
host-runner:
|
|
13
13
|
description: Whether this job is on a host-nix runner with shared store/caches.
|
|
14
14
|
value: ${{ steps.runner-kind.outputs.host }}
|
|
@@ -95,14 +95,63 @@ runs:
|
|
|
95
95
|
} >> "$GITHUB_ENV"
|
|
96
96
|
echo "nx cache : $NX_CACHE_ROOT"
|
|
97
97
|
|
|
98
|
+
# Host runners keep devenv's state on the shared bind, next to the Nx cache:
|
|
99
|
+
# .devenv (eval SQLite, gc roots, profile) and .direnv survive the job, so
|
|
100
|
+
# the next job on this host evaluates nothing and downloads nothing. The
|
|
101
|
+
# Actions cache segment below is the ephemeral runners' substitute for this.
|
|
102
|
+
# Scoped per job as well as lane: two jobs of one run are parallel, and one
|
|
103
|
+
# eval-cache.db has one writer.
|
|
104
|
+
#
|
|
105
|
+
# The eval cache keys on canonical absolute paths, and act's host executor
|
|
106
|
+
# checks out under <cache>/<random 8 chars>/hostexecutor - a new path every
|
|
107
|
+
# job by design (act/runner/run_context.go: MustRandName), so a restored
|
|
108
|
+
# cache could never match. The workspace is therefore bind-mounted at a
|
|
109
|
+
# stable path (a symlink would not do: Nix canonicalises) and devenv is
|
|
110
|
+
# evaluated from there; every later step still runs in the checkout, which
|
|
111
|
+
# is the same tree. DEVENV_WORKDIR names the path the devenv steps use.
|
|
112
|
+
# The mount target is the instance's own /work, not the shared bind: only
|
|
113
|
+
# the path's spelling must be stable, and a bind onto the idmapped cache
|
|
114
|
+
# dataset failed inside the job (move_mount: ENOENT) where a container-fs
|
|
115
|
+
# target does not. If the mount is refused the job goes on from the
|
|
116
|
+
# checkout and pays the evaluation: this is an optimisation, never a gate.
|
|
117
|
+
- name: 📂 Locate devenv state on the shared bind
|
|
118
|
+
if: steps.runner-kind.outputs.host == 'true'
|
|
119
|
+
shell: bash
|
|
120
|
+
env:
|
|
121
|
+
DEVENV_STATE_REPO: ${{ github.repository }}
|
|
122
|
+
DEVENV_STATE_LANE: ${{ github.workflow }}
|
|
123
|
+
DEVENV_STATE_JOB: ${{ github.job }}
|
|
124
|
+
run: |
|
|
125
|
+
lane=$(printf '%s' "$DEVENV_STATE_LANE" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9._-' '-')
|
|
126
|
+
root="/var/cache/ci/devenv/$DEVENV_STATE_REPO/${lane:-default}/$DEVENV_STATE_JOB"
|
|
127
|
+
mkdir -p "$root/.devenv" "$root/.direnv"
|
|
128
|
+
rm -rf tooling/direnv/.devenv tooling/direnv/.direnv
|
|
129
|
+
ln -s "$root/.devenv" tooling/direnv/.devenv
|
|
130
|
+
ln -s "$root/.direnv" tooling/direnv/.direnv
|
|
131
|
+
stable="/work/$DEVENV_STATE_REPO/${lane:-default}/$DEVENV_STATE_JOB"
|
|
132
|
+
if mkdir -p "$stable" && sudo mount --bind "$GITHUB_WORKSPACE" "$stable"; then
|
|
133
|
+
echo "DEVENV_WORKDIR=$stable" >> "$GITHUB_ENV"
|
|
134
|
+
echo "devenv state: $root (workspace bind-mounted at $stable)"
|
|
135
|
+
else
|
|
136
|
+
echo "DEVENV_WORKDIR=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
|
|
137
|
+
echo "::warning::stable devenv path unavailable (bind mount refused); evaluating from the checkout"
|
|
138
|
+
echo "devenv state: $root (workspace at $GITHUB_WORKSPACE)"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
- name: 📂 Devenv runs in the checkout
|
|
142
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
143
|
+
shell: bash
|
|
144
|
+
run: echo "DEVENV_WORKDIR=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
|
|
145
|
+
|
|
98
146
|
# Devenv segment (own key): tooling/direnv/.devenv + .direnv — includes
|
|
99
|
-
# nix-eval-cache.db. Independent of the store cache;
|
|
100
|
-
#
|
|
147
|
+
# nix-eval-cache.db. Independent of the store cache; restored on ephemeral
|
|
148
|
+
# runners whether or not the store cache hit. Missing store paths are fine:
|
|
101
149
|
# devenv verifies shell drv/out exist on eval-cache hit and invalidates +
|
|
102
150
|
# re-evals / realizes when GC removed them. Packages still come from host
|
|
103
151
|
# store, the store cache, or Cachix — this blob only skips evaluation.
|
|
104
152
|
- name: 🧊 Cache devenv and direnv
|
|
105
153
|
id: devenv-cache
|
|
154
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
106
155
|
uses: ./.github/actions/cache-nix-devenv
|
|
107
156
|
|
|
108
157
|
# Single-user Nix unpacked into /nix in seconds: no installer volume, no
|
|
@@ -169,7 +218,7 @@ runs:
|
|
|
169
218
|
# until the check existed.
|
|
170
219
|
- name: 🧰 Install devenv
|
|
171
220
|
shell: bash
|
|
172
|
-
working-directory: tooling/direnv
|
|
221
|
+
working-directory: ${{ env.DEVENV_WORKDIR }}/tooling/direnv
|
|
173
222
|
env:
|
|
174
223
|
SMOO_HOST_RUNNER: ${{ steps.runner-kind.outputs.host }}
|
|
175
224
|
run: ./github-actions-bootstrap.sh install-devenv
|
|
@@ -186,5 +235,5 @@ runs:
|
|
|
186
235
|
|
|
187
236
|
- name: 🐚 Build devenv shell
|
|
188
237
|
shell: bash
|
|
189
|
-
working-directory: tooling/direnv
|
|
238
|
+
working-directory: ${{ env.DEVENV_WORKDIR }}/tooling/direnv
|
|
190
239
|
run: ./github-actions-bootstrap.sh build-shell
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@arethetypeswrong/core": "^0.18.2",
|
|
67
|
-
"@smoothbricks/nx-plugin": "0.4.
|
|
67
|
+
"@smoothbricks/nx-plugin": "0.4.8",
|
|
68
68
|
"@smoothbricks/validation": "0.1.8",
|
|
69
69
|
"commander": "^14.0.3",
|
|
70
70
|
"make-synchronized": "^0.8.0",
|
package/src/cli.ts
CHANGED
|
@@ -89,15 +89,24 @@ function buildProgram(): Command {
|
|
|
89
89
|
.option('--fail-fast', 'stop after the first failing validation pack')
|
|
90
90
|
.option('--only-if-new-workspace-package', 'skip validation unless a new workspace package manifest is staged')
|
|
91
91
|
.option('--verbose', 'print validation progress and successful checks')
|
|
92
|
+
.option(
|
|
93
|
+
'--projects <names>',
|
|
94
|
+
'comma-separated Nx project names to build and pack-validate (a release selection); default: every project',
|
|
95
|
+
)
|
|
92
96
|
.action(
|
|
93
97
|
async (options: {
|
|
94
98
|
fix?: boolean;
|
|
95
99
|
failFast?: boolean;
|
|
96
100
|
onlyIfNewWorkspacePackage?: boolean;
|
|
97
101
|
verbose?: boolean;
|
|
102
|
+
projects?: string;
|
|
98
103
|
}) => {
|
|
99
104
|
const { validateMonorepo } = await import('./monorepo/index.js');
|
|
100
|
-
|
|
105
|
+
const projects = options.projects
|
|
106
|
+
?.split(',')
|
|
107
|
+
.map((name) => name.trim())
|
|
108
|
+
.filter((name) => name.length > 0);
|
|
109
|
+
await validateMonorepo(await findRepoRoot(), { ...options, projects });
|
|
101
110
|
},
|
|
102
111
|
);
|
|
103
112
|
monorepo.command('update').action(async () => {
|
package/src/lib/json.ts
CHANGED
|
@@ -134,6 +134,19 @@ export interface PackageCargoGitOrigin {
|
|
|
134
134
|
* origin is fetched as declared.
|
|
135
135
|
*/
|
|
136
136
|
internalMirror?: string;
|
|
137
|
+
/**
|
|
138
|
+
* SSH spellings of the same forge that managed CI rewrites onto
|
|
139
|
+
* `internalMirror` as well, e.g. `ssh://forgejo@forge.example.net:2223/`
|
|
140
|
+
* as Cargo and uv pin a git dependency. A runner holds the read token the
|
|
141
|
+
* mirror accepts and no SSH key at all, so every spelling a lockfile can
|
|
142
|
+
* carry needs its own rewrite: git matches `insteadOf` values as literal
|
|
143
|
+
* URL prefixes and infers no spelling from another — not the SSH host from
|
|
144
|
+
* the HTTPS one, and not the userless form from the one carrying a user.
|
|
145
|
+
* Each entry is a credential-free `ssh://` origin without a path; the SSH
|
|
146
|
+
* user is part of the spelling and stays in it. Requires
|
|
147
|
+
* `internalMirror`, since the entry is a rewrite source and nothing else.
|
|
148
|
+
*/
|
|
149
|
+
sshOrigins?: string[];
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
/**
|
|
@@ -153,6 +166,43 @@ export interface PackagePrivateNpmConfig {
|
|
|
153
166
|
publishTokenEnv?: string;
|
|
154
167
|
}
|
|
155
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Declared self-hosted Nx remote cache (`smoo.remoteCache`). Nx enables its
|
|
171
|
+
* HTTP cache on a nonempty NX_SELF_HOSTED_REMOTE_CACHE_SERVER and reads the
|
|
172
|
+
* credential from NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN; this declaration
|
|
173
|
+
* is what puts that pair in a managed CI job's environment and in a developer
|
|
174
|
+
* shell (tooling/direnv/secret-references.ts). Absent means every workspace
|
|
175
|
+
* keeps its own local cache and nothing is shared.
|
|
176
|
+
*
|
|
177
|
+
* The pair is emitted whole or not at all, because Nx accepts only 200 or 404
|
|
178
|
+
* from a cache server: a server without a working credential fails every task
|
|
179
|
+
* with 401 rather than missing quietly.
|
|
180
|
+
*/
|
|
181
|
+
export interface PackageRemoteCacheConfig {
|
|
182
|
+
/**
|
|
183
|
+
* Origin every runner and developer machine can reach, e.g.
|
|
184
|
+
* `https://nx-cache.example.net`. Credential-free, no path, and no trailing
|
|
185
|
+
* slash — Nx appends `/v1/cache/<hash>`, so a trailing slash requests
|
|
186
|
+
* `//v1/cache/<hash>`, which is a different route and answers 404 forever.
|
|
187
|
+
*/
|
|
188
|
+
server: string;
|
|
189
|
+
/**
|
|
190
|
+
* Origin an internal runner reaches instead of `server`, e.g.
|
|
191
|
+
* `http://10.89.0.1:8765` across a container bridge. Same declaration as a
|
|
192
|
+
* git origin's `internalMirror`: declaring it says this repository's managed
|
|
193
|
+
* runners are inside that network, so managed CI uses it and shells outside
|
|
194
|
+
* keep `server`. Omitted means CI uses `server` too.
|
|
195
|
+
*/
|
|
196
|
+
internalServer?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Repository secret holding the cache token, and the same variable name a
|
|
199
|
+
* developer shell resolves locally (an ambient value or a `smoo.secrets`
|
|
200
|
+
* entry). A read-only token here keeps an untrusted context reading the
|
|
201
|
+
* cache without being able to publish into it.
|
|
202
|
+
*/
|
|
203
|
+
tokenSecret: RepositorySecretName;
|
|
204
|
+
}
|
|
205
|
+
|
|
156
206
|
/** Local bootstrap fallback; existing environment values always take precedence. */
|
|
157
207
|
export interface PackageSecretCommand {
|
|
158
208
|
/** Executed directly, without a shell; stdout supplies the secret value. */
|
|
@@ -162,6 +212,8 @@ export interface PackageSecretCommand {
|
|
|
162
212
|
export interface PackageSmooConfig {
|
|
163
213
|
github?: PackageSmooGithub;
|
|
164
214
|
privateNpm?: PackagePrivateNpmConfig;
|
|
215
|
+
/** Declared self-hosted Nx remote cache; absent means local caching only. */
|
|
216
|
+
remoteCache?: PackageRemoteCacheConfig;
|
|
165
217
|
/** Provider-neutral local secret commands. CI supplies these variables externally. */
|
|
166
218
|
secrets?: Record<string, PackageSecretCommand>;
|
|
167
219
|
}
|