@smoothbricks/cli 0.11.11 → 0.11.13
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/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/cargo-policy.d.ts +25 -1
- package/dist/monorepo/cargo-policy.d.ts.map +1 -1
- package/dist/monorepo/cargo-policy.js +396 -4
- 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.map +1 -1
- package/dist/monorepo/index.js +5 -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/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +13 -0
- 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 +15 -5
- package/managed/raw/tooling/direnv/devenv.smoo.nix +28 -2
- package/managed/raw/tooling/direnv/secret-references.ts +248 -51
- package/managed/templates/github/actions/save-nix-devenv/action.yml +2 -2
- package/managed/templates/github/actions/setup-devenv/action.yml +43 -5
- package/package.json +2 -2
- 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 +29 -0
- package/src/monorepo/cargo-policy.test.ts +206 -3
- package/src/monorepo/cargo-policy.ts +341 -2
- package/src/monorepo/ci-workflow.ts +180 -11
- package/src/monorepo/index.ts +5 -1
- package/src/monorepo/managed-files.ts +6 -0
- package/src/monorepo/packs/index.test.ts +32 -0
- package/src/monorepo/packs/index.ts +13 -0
- package/src/monorepo/publish-workflow.ts +24 -4
- package/src/monorepo/secret-references.test.ts +175 -2
package/src/monorepo/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { appendFileSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { printCommandOutput, run, runResult } from '../lib/run.js';
|
|
3
3
|
import { escapeRegex, getWorkspacePackages, getWorkspacePatterns, listReleasePackages } from '../lib/workspace.js';
|
|
4
4
|
import { readProjectTargets } from '../nx/index.js';
|
|
5
|
-
import { validateCargoCachePolicy } from './cargo-policy.js';
|
|
5
|
+
import { applyCargoFeatureUnification, validateCargoCachePolicy } from './cargo-policy.js';
|
|
6
6
|
import {
|
|
7
7
|
formatCommitMessage,
|
|
8
8
|
stagedDeletedPublicPackages,
|
|
@@ -112,6 +112,10 @@ export async function updateManagedFiles(root: string): Promise<void> {
|
|
|
112
112
|
// Tool dependency policy (typescript API 6, @typescript/native for ttsc, nx, …)
|
|
113
113
|
// lives next to managed templates — update must install them, not only rewrite files.
|
|
114
114
|
await applyToolConfigDefaults(root);
|
|
115
|
+
// Rust's half of the same job: a multi-crate Cargo workspace must resolve
|
|
116
|
+
// features once for every member, or each per-crate cargo invocation
|
|
117
|
+
// recompiles the shared graph for its own selection.
|
|
118
|
+
applyCargoFeatureUnification(root);
|
|
115
119
|
syncBunLockfileVersions(root, { mode: 'install' });
|
|
116
120
|
console.log('installing workspace dependencies (bun install)');
|
|
117
121
|
await run('bun', ['install', '--no-summary'], root);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type PackageCargoCredentialsConfig,
|
|
11
11
|
type PackageJson,
|
|
12
12
|
type PackagePrivateNpmConfig,
|
|
13
|
+
type PackageRemoteCacheConfig,
|
|
13
14
|
type PackageSmooGithub,
|
|
14
15
|
type PackageSmooGithubEnvironments,
|
|
15
16
|
type PackageSourceCheckoutConfig,
|
|
@@ -176,6 +177,8 @@ export interface ManagedFileContext {
|
|
|
176
177
|
sourceCheckouts?: PackageSourceCheckoutConfig[];
|
|
177
178
|
/** Declared Cargo private-dependency credentials from the root smoo config; absent means none. */
|
|
178
179
|
cargoCredentials?: PackageCargoCredentialsConfig;
|
|
180
|
+
/** Declared self-hosted Nx remote cache from the root smoo config; absent means local caching only. */
|
|
181
|
+
remoteCache?: PackageRemoteCacheConfig;
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
interface DeployTargetInfo {
|
|
@@ -391,6 +394,7 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
391
394
|
privateNpm: context.privateNpm,
|
|
392
395
|
sourceCheckouts: context.sourceCheckouts,
|
|
393
396
|
cargoCredentials: context.cargoCredentials,
|
|
397
|
+
remoteCache: context.remoteCache,
|
|
394
398
|
environments: context.ciEnvironments,
|
|
395
399
|
deploySecrets: context.ciDeploySecrets,
|
|
396
400
|
e2eSecrets: context.ciE2eSecrets,
|
|
@@ -414,6 +418,7 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
414
418
|
privateNpm: context.privateNpm,
|
|
415
419
|
sourceCheckouts: context.sourceCheckouts,
|
|
416
420
|
cargoCredentials: context.cargoCredentials,
|
|
421
|
+
remoteCache: context.remoteCache,
|
|
417
422
|
deploySecrets: context.ciDeploySecrets,
|
|
418
423
|
});
|
|
419
424
|
}
|
|
@@ -476,6 +481,7 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
|
|
|
476
481
|
platformTargetGlobs,
|
|
477
482
|
sourceCheckouts,
|
|
478
483
|
cargoCredentials,
|
|
484
|
+
remoteCache: manifest?.smoo?.remoteCache,
|
|
479
485
|
macosPlatformArchitectures: macosPlatformArchitecturesForTest(targetNames),
|
|
480
486
|
privateNpm,
|
|
481
487
|
};
|
|
@@ -221,6 +221,38 @@ describe('monorepo validation pack phases', () => {
|
|
|
221
221
|
}
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
it('validates and fixes cargo workspace feature unification through its own pack', async () => {
|
|
225
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-validate-cargo-'));
|
|
226
|
+
try {
|
|
227
|
+
await mkdir(join(root, 'crates/alpha'), { recursive: true });
|
|
228
|
+
await mkdir(join(root, 'crates/beta'), { recursive: true });
|
|
229
|
+
await mkdir(join(root, 'tooling/direnv'), { recursive: true });
|
|
230
|
+
await writeFile(
|
|
231
|
+
join(root, 'Cargo.toml'),
|
|
232
|
+
'[workspace]\nmembers = ["crates/*"]\n\n[profile.test]\nincremental = false\ndebug = 0\n',
|
|
233
|
+
);
|
|
234
|
+
await writeFile(join(root, 'crates/alpha/Cargo.toml'), '[package]\nname = "alpha"\n');
|
|
235
|
+
await writeFile(join(root, 'crates/beta/Cargo.toml'), '[package]\nname = "beta"\n');
|
|
236
|
+
await writeFile(join(root, 'tooling/direnv/devenv.smoo.nix'), 'languages.rust = {\n channel = "nightly";\n};\n');
|
|
237
|
+
const cargoPack = packsForTest.find((pack) => pack.name === 'cargo');
|
|
238
|
+
if (!cargoPack) {
|
|
239
|
+
throw new Error('cargo validation pack not found');
|
|
240
|
+
}
|
|
241
|
+
const runBuild = () => 0;
|
|
242
|
+
|
|
243
|
+
// A policy nothing calls is not a policy: validate must reach it.
|
|
244
|
+
expect(
|
|
245
|
+
await runValidatePacks({ root, syncRuntime: false }, { failFast: true }, { packs: [cargoPack], runBuild }),
|
|
246
|
+
).toEqual({ failures: 1, failedChecks: 1 });
|
|
247
|
+
|
|
248
|
+
expect(
|
|
249
|
+
await runValidatePacks({ root, syncRuntime: false }, { fix: true }, { packs: [cargoPack], runBuild }),
|
|
250
|
+
).toEqual({ failures: 0, failedChecks: 0 });
|
|
251
|
+
} finally {
|
|
252
|
+
await rm(root, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
224
256
|
it('propagates parsed target dependencies through the production adapter', () => {
|
|
225
257
|
const targetDependencies = new Map([
|
|
226
258
|
['build', ['compile-linux']],
|
|
@@ -2,6 +2,7 @@ import { chmodSync, existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { printCommandOutput, runResult, runStatus } from '../../lib/run.js';
|
|
4
4
|
import { type ProjectTargets, readProjectTargets } from '../../nx/index.js';
|
|
5
|
+
import { applyCargoFeatureUnification, validateCargoCachePolicy } from '../cargo-policy.js';
|
|
5
6
|
import { validateGoToolchainAgreement } from '../go-toolchain.js';
|
|
6
7
|
import { syncBunLockfileVersions, validateBunLockfileVersions } from '../lockfile.js';
|
|
7
8
|
import { validateDevenvModuleImport, warnOnManagedFileDrift } from '../managed-files.js';
|
|
@@ -120,6 +121,18 @@ const packs: MonorepoPack[] = [
|
|
|
120
121
|
return validateDevenvModuleImport(ctx.root);
|
|
121
122
|
},
|
|
122
123
|
},
|
|
124
|
+
{
|
|
125
|
+
// Cargo's build-cache and feature-unification conventions. `--fix` writes
|
|
126
|
+
// the workspace feature unification a multi-crate workspace needs; the rest
|
|
127
|
+
// of the policy is a verdict a human has to act on.
|
|
128
|
+
name: 'cargo',
|
|
129
|
+
fixPreBuild(ctx) {
|
|
130
|
+
applyCargoFeatureUnification(ctx.root);
|
|
131
|
+
},
|
|
132
|
+
validatePreBuild(ctx) {
|
|
133
|
+
return validateCargoCachePolicy(ctx.root);
|
|
134
|
+
},
|
|
135
|
+
},
|
|
123
136
|
{
|
|
124
137
|
name: 'publishing',
|
|
125
138
|
init(ctx) {
|
|
@@ -11,6 +11,7 @@ import { isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
|
|
|
11
11
|
import type {
|
|
12
12
|
PackageCargoCredentialsConfig,
|
|
13
13
|
PackagePrivateNpmConfig,
|
|
14
|
+
PackageRemoteCacheConfig,
|
|
14
15
|
PackageSmooGithub,
|
|
15
16
|
PackageSourceCheckoutConfig,
|
|
16
17
|
} from '../lib/json.js';
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
cargoCredentialStepLines,
|
|
22
23
|
type DeployStepSecretConfig,
|
|
23
24
|
deployStepSecretEnvLines,
|
|
25
|
+
remoteCacheJobEnvLines,
|
|
24
26
|
sourceCheckoutsStepLines,
|
|
25
27
|
} from './ci-workflow.js';
|
|
26
28
|
import { GITHUB_HOSTED_LINUX_RUNNER, renderRunsOnLine, type WorkflowRunsOn } from './github-runs-on.js';
|
|
@@ -125,6 +127,13 @@ export interface PublishWorkflowDefinitionOptions extends DeployStepSecretConfig
|
|
|
125
127
|
* become job env because every later cargo fetch resolves them.
|
|
126
128
|
*/
|
|
127
129
|
cargoCredentials?: PackageCargoCredentialsConfig;
|
|
130
|
+
/**
|
|
131
|
+
* Declared self-hosted Nx remote cache. Every job here runs Nx targets —
|
|
132
|
+
* the release-candidate build, the platform build, the publish gate — so
|
|
133
|
+
* each carries the server and access token and reuses what CI already
|
|
134
|
+
* built for the same hashes.
|
|
135
|
+
*/
|
|
136
|
+
remoteCache?: PackageRemoteCacheConfig;
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
export interface PublishWorkflowInputs {
|
|
@@ -466,7 +475,7 @@ jobs:
|
|
|
466
475
|
publish:
|
|
467
476
|
${options.release === false ? renderRunsOnLine(options.runsOn) : publishJobRunsOnLine(options)}
|
|
468
477
|
env:
|
|
469
|
-
GH_TOKEN: ${githubExpression('github.token')}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
478
|
+
GH_TOKEN: ${githubExpression('github.token')}${remoteCacheJobEnv(options)}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
470
479
|
steps:
|
|
471
480
|
`;
|
|
472
481
|
}
|
|
@@ -717,6 +726,17 @@ function cargoCredentialsJobEnv(options: PublishWorkflowDefinitionOptions): stri
|
|
|
717
726
|
return rendered === '' ? '' : `\n${rendered}`;
|
|
718
727
|
}
|
|
719
728
|
|
|
729
|
+
/**
|
|
730
|
+
* The remote cache is job env for the same reason: every Nx target in the job
|
|
731
|
+
* reads it, and a release build that missed the cache would rebuild what CI
|
|
732
|
+
* already built for the identical hashes. Only `${{ secrets.NAME }}` is
|
|
733
|
+
* rendered for the token. Malformed declarations throw here, not in CI.
|
|
734
|
+
*/
|
|
735
|
+
function remoteCacheJobEnv(options: PublishWorkflowDefinitionOptions): string {
|
|
736
|
+
const rendered = remoteCacheJobEnvLines(options.remoteCache).trimEnd();
|
|
737
|
+
return rendered === '' ? '' : `\n${rendered}`;
|
|
738
|
+
}
|
|
739
|
+
|
|
720
740
|
function renderSingleJobPublishWorkflowSteps(
|
|
721
741
|
steps: PublishWorkflowStep[],
|
|
722
742
|
options: PublishWorkflowDefinitionOptions,
|
|
@@ -796,7 +816,7 @@ ${renderRunsOnLine(options.runsOn)}
|
|
|
796
816
|
mode: ${githubExpression('steps.version.outputs.mode')}
|
|
797
817
|
release-sha: ${githubExpression('steps.release-state.outputs.sha')}
|
|
798
818
|
env:
|
|
799
|
-
GH_TOKEN: ${githubExpression('github.token')}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
819
|
+
GH_TOKEN: ${githubExpression('github.token')}${remoteCacheJobEnv(options)}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
800
820
|
steps:
|
|
801
821
|
${renderLinuxReleaseCandidateSteps(steps, options)}
|
|
802
822
|
|
|
@@ -806,7 +826,7 @@ ${renderMacosJobHeaderLines(options)}
|
|
|
806
826
|
contents: read
|
|
807
827
|
id-token: none
|
|
808
828
|
env:
|
|
809
|
-
GH_TOKEN: ${githubExpression('github.token')}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
829
|
+
GH_TOKEN: ${githubExpression('github.token')}${remoteCacheJobEnv(options)}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
810
830
|
${Object.entries(options.platformProducer?.env ?? {})
|
|
811
831
|
.map(([name, value]) => ` ${name}: ${JSON.stringify(value)}`)
|
|
812
832
|
.join('\n')}
|
|
@@ -821,7 +841,7 @@ ${publishJobRunsOnLine(options)}
|
|
|
821
841
|
id-token: write
|
|
822
842
|
env:
|
|
823
843
|
TTSC_TSGO_BINARY: ${githubExpression('github.workspace')}/node_modules/@typescript/native/bin/tsc
|
|
824
|
-
GH_TOKEN: ${githubExpression('github.token')}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
844
|
+
GH_TOKEN: ${githubExpression('github.token')}${remoteCacheJobEnv(options)}${cargoCredentialsJobEnv(options)}${privateNpmInstallJobEnv(options)}
|
|
825
845
|
steps:
|
|
826
846
|
${renderFinalLinuxPublishSteps(options)}
|
|
827
847
|
`;
|
|
@@ -136,14 +136,18 @@ async function resolveInBootstrap(options: {
|
|
|
136
136
|
|
|
137
137
|
/** Builds a fixture repository whose package.json and .npmrc are the resolver's real inputs. */
|
|
138
138
|
async function withFixture(
|
|
139
|
-
options: { secrets: Record<string, SecretSpec>; npmrc?: string },
|
|
139
|
+
options: { secrets: Record<string, SecretSpec>; npmrc?: string; remoteCache?: unknown },
|
|
140
140
|
run: (root: string) => Promise<void>,
|
|
141
141
|
): Promise<void> {
|
|
142
142
|
const root = await mkdtemp(join(tmpdir(), 'smoo-secret-references-'));
|
|
143
143
|
try {
|
|
144
144
|
await writeFile(
|
|
145
145
|
join(root, 'package.json'),
|
|
146
|
-
JSON.stringify({
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
name: 'fixture',
|
|
148
|
+
version: '0.0.0',
|
|
149
|
+
smoo: { secrets: options.secrets, remoteCache: options.remoteCache },
|
|
150
|
+
}),
|
|
147
151
|
);
|
|
148
152
|
if (options.npmrc !== undefined) {
|
|
149
153
|
await writeFile(join(root, '.npmrc'), options.npmrc);
|
|
@@ -249,6 +253,28 @@ describe('resolveSecretEnvironment', () => {
|
|
|
249
253
|
});
|
|
250
254
|
});
|
|
251
255
|
|
|
256
|
+
it('an unreachable cache-token provider does not block the install, while other secrets still refuse', async () => {
|
|
257
|
+
await withFixture(
|
|
258
|
+
{
|
|
259
|
+
secrets: {
|
|
260
|
+
NX_REMOTE_CACHE_TOKEN: { command: ['definitely-not-a-real-smoo-binary-xyz'] },
|
|
261
|
+
SMOO_TOKEN: { command: emit('tok-from-provider') },
|
|
262
|
+
},
|
|
263
|
+
remoteCache: { server: 'https://nx-cache.example.net', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
264
|
+
},
|
|
265
|
+
async (root) => {
|
|
266
|
+
// A cache is an optimization: its provider being down installs
|
|
267
|
+
// dependencies anyway. Every other declared secret keeps its refusal.
|
|
268
|
+
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
269
|
+
resolved: { SMOO_TOKEN: 'tok-from-provider' },
|
|
270
|
+
});
|
|
271
|
+
const withoutCacheDeclaration = await resolveInBootstrap({ root, env: { CI: 'true' } });
|
|
272
|
+
expect(withoutCacheDeclaration.error).toContain('SMOO_TOKEN');
|
|
273
|
+
expect(withoutCacheDeclaration.error).not.toContain('NX_REMOTE_CACHE_TOKEN');
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
252
278
|
it('a present variable is not reported while another refuses in CI', async () => {
|
|
253
279
|
await withFixture(
|
|
254
280
|
{
|
|
@@ -361,3 +387,150 @@ describe('resolveSecretEnvironment', () => {
|
|
|
361
387
|
}
|
|
362
388
|
});
|
|
363
389
|
});
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The remote cache half is exercised the way the managed shell runs it — the
|
|
393
|
+
* raw script as a program, its stdout `eval`-ed — because that round trip is
|
|
394
|
+
* the contract: shell text on stdout, guidance on stderr, and a status that
|
|
395
|
+
* never keeps a shell from opening.
|
|
396
|
+
*/
|
|
397
|
+
describe('remote cache shell export', () => {
|
|
398
|
+
const SERVER = 'https://nx-cache.example.net';
|
|
399
|
+
|
|
400
|
+
async function exportFor(root: string, env: Record<string, string>): Promise<{ stdout: string; stderr: string }> {
|
|
401
|
+
const proc = Bun.spawn({
|
|
402
|
+
cmd: ['bun', RAW_RESOLVER, root],
|
|
403
|
+
env: { PATH: process.env['PATH'], HOME: process.env['HOME'], ...env },
|
|
404
|
+
stdin: 'ignore',
|
|
405
|
+
stdout: 'pipe',
|
|
406
|
+
stderr: 'pipe',
|
|
407
|
+
});
|
|
408
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
409
|
+
new Response(proc.stdout).text(),
|
|
410
|
+
new Response(proc.stderr).text(),
|
|
411
|
+
proc.exited,
|
|
412
|
+
]);
|
|
413
|
+
// A cache is an optimization: nothing it does may fail shell entry.
|
|
414
|
+
expect(exitCode).toBe(0);
|
|
415
|
+
return { stdout, stderr };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The value a shell ends up with after eval-ing the script's output. */
|
|
419
|
+
async function evaluated(root: string, env: Record<string, string>, name: string): Promise<string> {
|
|
420
|
+
const { stdout } = await exportFor(root, env);
|
|
421
|
+
const shell = Bun.spawnSync({
|
|
422
|
+
cmd: ['sh', '-c', `${stdout}printf %s "\${${name}:-}"`],
|
|
423
|
+
env: { PATH: process.env['PATH'] ?? '/usr/bin:/bin' },
|
|
424
|
+
});
|
|
425
|
+
expect(shell.exitCode).toBe(0);
|
|
426
|
+
return shell.stdout.toString();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
it('exports the pair from an ambient token and nothing from an absent declaration', async () => {
|
|
430
|
+
await withFixture(
|
|
431
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
432
|
+
async (root) => {
|
|
433
|
+
expect((await exportFor(root, { NX_REMOTE_CACHE_TOKEN: 'ambient-token' })).stdout).toBe(
|
|
434
|
+
`export NX_SELF_HOSTED_REMOTE_CACHE_SERVER='${SERVER}'\nexport NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN='ambient-token'\n`,
|
|
435
|
+
);
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
await withFixture({ secrets: {} }, async (root) => {
|
|
439
|
+
expect(await exportFor(root, { NX_REMOTE_CACHE_TOKEN: 'ambient-token' })).toEqual({ stdout: '', stderr: '' });
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('exports nothing and names the variable when the token has no value', async () => {
|
|
444
|
+
await withFixture(
|
|
445
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
446
|
+
async (root) => {
|
|
447
|
+
const { stdout, stderr } = await exportFor(root, {});
|
|
448
|
+
// Half a pair is worse than none: Nx enables its cache on the server
|
|
449
|
+
// alone and then fails every task on 401.
|
|
450
|
+
expect(stdout).toBe('');
|
|
451
|
+
expect(stderr).toContain('NX_REMOTE_CACHE_TOKEN');
|
|
452
|
+
expect(stderr).toContain(SERVER);
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('reports a broken provider as a lost cache, never as a failed install', async () => {
|
|
458
|
+
await withFixture(
|
|
459
|
+
{
|
|
460
|
+
secrets: { NX_REMOTE_CACHE_TOKEN: { command: ['definitely-not-a-real-smoo-binary-xyz'] } },
|
|
461
|
+
remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
462
|
+
},
|
|
463
|
+
async (root) => {
|
|
464
|
+
const { stdout, stderr } = await exportFor(root, {});
|
|
465
|
+
expect(stdout).toBe('');
|
|
466
|
+
expect(stderr).toContain('NX_REMOTE_CACHE_TOKEN');
|
|
467
|
+
expect(stderr).toContain('remote cache off');
|
|
468
|
+
// Nothing here stopped an install, and the message may not say it did.
|
|
469
|
+
expect(stderr).not.toContain('dependencies were not installed');
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('leaves a server the environment already carries alone', async () => {
|
|
475
|
+
await withFixture(
|
|
476
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
477
|
+
async (root) => {
|
|
478
|
+
// A CI job env names the address its own runners reach; the public
|
|
479
|
+
// origin must not replace it.
|
|
480
|
+
expect(
|
|
481
|
+
await exportFor(root, {
|
|
482
|
+
NX_SELF_HOSTED_REMOTE_CACHE_SERVER: 'http://10.89.0.1:8765',
|
|
483
|
+
NX_REMOTE_CACHE_TOKEN: 'ambient-token',
|
|
484
|
+
}),
|
|
485
|
+
).toEqual({ stdout: '', stderr: '' });
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('resolves a provider-declared token, and only that one secret', async () => {
|
|
491
|
+
await withFixture(
|
|
492
|
+
{
|
|
493
|
+
secrets: {
|
|
494
|
+
NX_REMOTE_CACHE_TOKEN: { command: emit('provider-token') },
|
|
495
|
+
SMOO_OTHER: { command: ['definitely-not-a-real-smoo-binary-xyz'] },
|
|
496
|
+
},
|
|
497
|
+
remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
498
|
+
},
|
|
499
|
+
async (root) => {
|
|
500
|
+
// The unrunnable sibling proves no other declared secret is resolved
|
|
501
|
+
// here, and that none of them reaches the shell.
|
|
502
|
+
const { stdout } = await exportFor(root, {});
|
|
503
|
+
expect(stdout).toContain("export NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN='provider-token'");
|
|
504
|
+
expect(stdout).not.toContain('SMOO_OTHER');
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('survives eval with a token full of shell metacharacters', async () => {
|
|
510
|
+
const hostile = `it's $(touch /tmp/smoo-cache-pwned) \`x\` "q" \\`;
|
|
511
|
+
await withFixture(
|
|
512
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
513
|
+
async (root) => {
|
|
514
|
+
expect(
|
|
515
|
+
await evaluated(root, { NX_REMOTE_CACHE_TOKEN: hostile }, 'NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN'),
|
|
516
|
+
).toBe(hostile);
|
|
517
|
+
},
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('refuses a declaration Nx could not use, naming the field', async () => {
|
|
522
|
+
for (const remoteCache of [
|
|
523
|
+
{ server: `${SERVER}/`, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
524
|
+
{ server: '', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
525
|
+
{ server: SERVER, tokenSecret: 'has-dash' },
|
|
526
|
+
{ server: SERVER },
|
|
527
|
+
'https://nx-cache.example.net',
|
|
528
|
+
]) {
|
|
529
|
+
await withFixture({ secrets: {}, remoteCache }, async (root) => {
|
|
530
|
+
const { stdout, stderr } = await exportFor(root, { NX_REMOTE_CACHE_TOKEN: 'ambient-token' });
|
|
531
|
+
expect(stdout).toBe('');
|
|
532
|
+
expect(stderr).toContain('smoo.remoteCache');
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
});
|