@smoothbricks/cli 0.11.17 → 0.11.19
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 +45 -10
- package/dist/cli.js +10 -7
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +6 -20
- package/dist/lib/deploy-tags.d.ts +17 -3
- package/dist/lib/deploy-tags.d.ts.map +1 -1
- package/dist/lib/deploy-tags.js +23 -4
- package/dist/monorepo/ci-workflow.d.ts +56 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +199 -2
- package/dist/monorepo/managed-files.d.ts +28 -2
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +74 -22
- package/dist/release/github-release.d.ts +10 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +4 -5
- package/dist/secrets/commands.d.ts +52 -13
- package/dist/secrets/commands.d.ts.map +1 -1
- package/dist/secrets/commands.js +220 -34
- package/dist/secrets/index.d.ts +21 -1
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +110 -4
- package/dist/secrets/repository.d.ts +60 -0
- package/dist/secrets/repository.d.ts.map +1 -0
- package/dist/secrets/repository.js +145 -0
- package/dist/wrangler/cloudflare.d.ts +7 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -1
- package/dist/wrangler/cloudflare.js +10 -0
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +50 -24
- package/dist/wrangler/stage-secrets.d.ts +57 -0
- package/dist/wrangler/stage-secrets.d.ts.map +1 -0
- package/dist/wrangler/stage-secrets.js +178 -0
- package/managed/raw/tooling/direnv/devenv.smoo.nix +9 -1
- package/managed/raw/tooling/git-hooks/pre-push.sh +30 -29
- package/package.json +2 -2
- package/src/cli.ts +33 -10
- package/src/github-ci/index.test.ts +6 -7
- package/src/github-ci/index.ts +11 -19
- package/src/lib/deploy-tags.ts +22 -4
- package/src/monorepo/__tests__/ci-workflow.test.ts +161 -0
- package/src/monorepo/ci-workflow.ts +277 -2
- package/src/monorepo/managed-files.test.ts +96 -37
- package/src/monorepo/managed-files.ts +94 -23
- package/src/monorepo/package-policy.test.ts +1 -1
- package/src/release/__tests__/github-release.test.ts +8 -4
- package/src/release/github-release.ts +13 -5
- package/src/release/index.ts +4 -4
- package/src/secrets/commands.test.ts +28 -0
- package/src/secrets/commands.ts +237 -35
- package/src/secrets/index.test.ts +118 -1
- package/src/secrets/index.ts +108 -4
- package/src/secrets/repository.test.ts +98 -0
- package/src/secrets/repository.ts +164 -0
- package/src/wrangler/cloudflare.ts +18 -0
- package/src/wrangler/deploy-stage.test.ts +258 -11
- package/src/wrangler/deploy-stage.ts +70 -38
- package/src/wrangler/stage-secrets.ts +146 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
cargoCrossTestArchiveFile,
|
|
6
|
+
cargoCrossTestTripleFromArchiveTarget,
|
|
7
|
+
} from '@smoothbricks/nx-plugin/cross-check-policy';
|
|
4
8
|
import { MACOS_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
5
|
-
import {
|
|
9
|
+
import { PRODUCTION_PUSH_DEPLOY_TAG, stageDeploysProject } from '../lib/deploy-tags.js';
|
|
6
10
|
import {
|
|
7
11
|
ciPushBranches,
|
|
8
12
|
type NonEmptyArray,
|
|
@@ -17,9 +21,16 @@ import {
|
|
|
17
21
|
readValidatedPackageJson,
|
|
18
22
|
} from '../lib/json.js';
|
|
19
23
|
import { listReleasePackages, packageInfo } from '../lib/workspace.js';
|
|
20
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
loadNxProjects,
|
|
26
|
+
type NxProjects,
|
|
27
|
+
projectRootFromNxProjectJson,
|
|
28
|
+
targetNamesFromNxProjectJson,
|
|
29
|
+
targetNamesFromProjects,
|
|
30
|
+
} from '../nx/index.js';
|
|
21
31
|
import { resolvePrivateNpmWorkflowConfig } from '../release/private-npm.js';
|
|
22
|
-
import {
|
|
32
|
+
import type { DeploymentStage } from '../wrangler/stage.js';
|
|
33
|
+
import { type CiCrossTestArchive, privateNpmReadTokenJobEnv, renderCiWorkflowYaml } from './ci-workflow.js';
|
|
23
34
|
import { renderRunsOnLine } from './github-runs-on.js';
|
|
24
35
|
import { renderPrPreviewCleanupWorkflowYaml } from './pr-preview-cleanup-workflow.js';
|
|
25
36
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
@@ -181,6 +192,12 @@ export interface ManagedFileContext {
|
|
|
181
192
|
cargoCredentials?: PackageCargoCredentialsConfig;
|
|
182
193
|
/** Declared self-hosted Nx remote cache from the root smoo config; absent means local caching only. */
|
|
183
194
|
remoteCache?: PackageRemoteCacheConfig;
|
|
195
|
+
/**
|
|
196
|
+
* Cross-built test archives the graph can produce, one per foreign target
|
|
197
|
+
* triple the cargo workspace declares. Empty for a repository that declares
|
|
198
|
+
* none, which renders a CI workflow with no cross steps and no macOS job.
|
|
199
|
+
*/
|
|
200
|
+
crossTestArchives: CiCrossTestArchive[];
|
|
184
201
|
}
|
|
185
202
|
|
|
186
203
|
interface DeployTargetInfo {
|
|
@@ -326,6 +343,23 @@ export const managedFileTargetsForTest = managedFiles.map(({ target, executable
|
|
|
326
343
|
|
|
327
344
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
328
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Whether a repository of this shape has the file at all. A repository that owns
|
|
348
|
+
* no packages has nothing to publish, and one the stage flow does not deploy to
|
|
349
|
+
* Cloudflare has no preview stage to clean up.
|
|
350
|
+
*/
|
|
351
|
+
function managedFileApplies(file: ManagedFile, context: ManagedFileContext): boolean {
|
|
352
|
+
if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return file.cloudflareDeployOnly !== true || context.stagingDeployProvider === 'cloudflare';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Test seam: the managed targets a repository of this shape gets, in file order. */
|
|
359
|
+
export function managedFileTargetsForContext(context: ManagedFileContext): string[] {
|
|
360
|
+
return managedFiles.filter((file) => managedFileApplies(file, context)).map((file) => file.target);
|
|
361
|
+
}
|
|
362
|
+
|
|
329
363
|
export async function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): Promise<FileResult[]> {
|
|
330
364
|
const context = await getManagedFileContext(root);
|
|
331
365
|
return managedFiles.map((file) => applyManagedFile(root, file, mode, context));
|
|
@@ -337,10 +371,7 @@ function applyManagedFile(
|
|
|
337
371
|
mode: 'update' | 'check' | 'diff',
|
|
338
372
|
context: ManagedFileContext,
|
|
339
373
|
): FileResult {
|
|
340
|
-
if (file
|
|
341
|
-
return { target: file.target, action: 'skipped' };
|
|
342
|
-
}
|
|
343
|
-
if (file.cloudflareDeployOnly === true && context.stagingDeployProvider !== 'cloudflare') {
|
|
374
|
+
if (!managedFileApplies(file, context)) {
|
|
344
375
|
return { target: file.target, action: 'skipped' };
|
|
345
376
|
}
|
|
346
377
|
const target = resolve(root, file.target);
|
|
@@ -407,6 +438,9 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
407
438
|
deploySecrets: context.ciDeploySecrets,
|
|
408
439
|
e2eSecrets: context.ciE2eSecrets,
|
|
409
440
|
productionOnPush: context.hasProductionPushDeployTargets,
|
|
441
|
+
crossTestArchives: context.crossTestArchives,
|
|
442
|
+
macosRunsOn: context.macosRunsOn,
|
|
443
|
+
platformProducer: context.platformProducer,
|
|
410
444
|
});
|
|
411
445
|
}
|
|
412
446
|
if (file.source === 'publish-workflow') {
|
|
@@ -503,6 +537,7 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
|
|
|
503
537
|
? macosPlatformArchitecturesForTest(targetNames)
|
|
504
538
|
: [],
|
|
505
539
|
privateNpm,
|
|
540
|
+
crossTestArchives: crossTestArchivesForTest(nxProjects),
|
|
506
541
|
};
|
|
507
542
|
}
|
|
508
543
|
|
|
@@ -510,6 +545,40 @@ export function hasExactTargetForTest(targetNames: Iterable<string>, target: str
|
|
|
510
545
|
return [...targetNames].includes(target);
|
|
511
546
|
}
|
|
512
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Test seam: the cross-built test archives this graph can produce, read back
|
|
550
|
+
* out of the inferred `cargo-cross-test-archive-<triple>` targets.
|
|
551
|
+
*
|
|
552
|
+
* Derived, not configured. The triple is declared exactly once — in the cargo
|
|
553
|
+
* workspace's own `[workspace.metadata.smoothbricks.test] cross-targets`, where
|
|
554
|
+
* a Rust target triple belongs — and both halves of the workflow are rendered
|
|
555
|
+
* from the targets that declaration produced. A second copy in `smoo.github`
|
|
556
|
+
* could name a triple the graph cannot build, and CI would discover that only
|
|
557
|
+
* on the runner.
|
|
558
|
+
*
|
|
559
|
+
* The path is the target's own output location, so the artifact the Linux job
|
|
560
|
+
* uploads and the file the macOS job's runner opens are the same string
|
|
561
|
+
* computed once.
|
|
562
|
+
*/
|
|
563
|
+
export function crossTestArchivesForTest(projects: NxProjects): CiCrossTestArchive[] {
|
|
564
|
+
const archives: CiCrossTestArchive[] = [];
|
|
565
|
+
for (const project of Object.values(projects)) {
|
|
566
|
+
const projectRoot = projectRootFromNxProjectJson(project);
|
|
567
|
+
for (const targetName of targetNamesFromNxProjectJson(project)) {
|
|
568
|
+
const triple = cargoCrossTestTripleFromArchiveTarget(targetName);
|
|
569
|
+
if (triple === null) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const file = cargoCrossTestArchiveFile(triple);
|
|
573
|
+
archives.push({
|
|
574
|
+
triple,
|
|
575
|
+
path: projectRoot === undefined || projectRoot === '.' ? file : `${projectRoot}/${file}`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return archives.sort((left, right) => left.triple.localeCompare(right.triple));
|
|
580
|
+
}
|
|
581
|
+
|
|
513
582
|
/**
|
|
514
583
|
* The platform families the release workflow produces: the graph's families
|
|
515
584
|
* minus the ones the repository excludes. Excluding a family the graph does not
|
|
@@ -558,12 +627,12 @@ export function macosPlatformArchitecturesForTest(targetNames: Iterable<string>)
|
|
|
558
627
|
return [...architectures].sort((left, right) => left.localeCompare(right));
|
|
559
628
|
}
|
|
560
629
|
|
|
561
|
-
/** Test seam:
|
|
562
|
-
export function deployTargetInfoFromProjects(projects: NxProjects,
|
|
630
|
+
/** Test seam: what the stage flow deploys, and with which provider, from graph nodes. */
|
|
631
|
+
export function deployTargetInfoFromProjects(projects: NxProjects, stage: DeploymentStage): DeployTargetInfo {
|
|
563
632
|
let exists = false;
|
|
564
633
|
let provider: DeployTargetInfo['provider'];
|
|
565
634
|
for (const project of Object.values(projects)) {
|
|
566
|
-
const info = deployTargetInfoFromProject(project,
|
|
635
|
+
const info = deployTargetInfoFromProject(project, stage);
|
|
567
636
|
if (!info.exists) {
|
|
568
637
|
continue;
|
|
569
638
|
}
|
|
@@ -578,22 +647,24 @@ export function anyProjectHasTagForTest(projects: NxProjects, tag: string): bool
|
|
|
578
647
|
return Object.values(projects).some((project) => project.tags?.includes(tag));
|
|
579
648
|
}
|
|
580
649
|
|
|
581
|
-
|
|
650
|
+
/**
|
|
651
|
+
* A generated deploy job exists for the projects CI would actually deploy, which
|
|
652
|
+
* is `stageDeploysProject` and nothing else — the same rule `smoo github-ci
|
|
653
|
+
* nx-deploy` selects by, so the job and its selection cannot disagree. Reading
|
|
654
|
+
* the target's presence instead would render a deploy job, with cloud
|
|
655
|
+
* credentials, into a repository whose only wrangler manifests document a
|
|
656
|
+
* Durable Object binding for a published library's consumers.
|
|
657
|
+
*
|
|
658
|
+
* The command is read for the PROVIDER only: which cloud CLI runs says which
|
|
659
|
+
* credentials the job needs, and says nothing about whether CI runs it.
|
|
660
|
+
*/
|
|
661
|
+
function deployTargetInfoFromProject(project: NxProjectJson, stage: DeploymentStage): DeployTargetInfo {
|
|
582
662
|
const deploy = project.targets?.deploy;
|
|
583
|
-
if (!deploy) {
|
|
584
|
-
return { exists: false };
|
|
585
|
-
}
|
|
586
|
-
const baseCommandValue = deploy.options?.command ?? deploy.command;
|
|
587
|
-
const baseCommand = typeof baseCommandValue === 'string' ? baseCommandValue : '';
|
|
588
|
-
if (isStageDerivedDeploy(project.tags, baseCommand)) {
|
|
589
|
-
return { exists: true, provider: deployProvider(baseCommand) };
|
|
590
|
-
}
|
|
591
|
-
const config = deploy.configurations?.[configuration];
|
|
592
|
-
if (!config) {
|
|
663
|
+
if (!deploy || !stageDeploysProject(project.tags, stage)) {
|
|
593
664
|
return { exists: false };
|
|
594
665
|
}
|
|
595
|
-
const
|
|
596
|
-
return { exists: true, provider: deployProvider(typeof
|
|
666
|
+
const command = deploy.options?.command ?? deploy.command;
|
|
667
|
+
return { exists: true, provider: deployProvider(typeof command === 'string' ? command : '') };
|
|
597
668
|
}
|
|
598
669
|
|
|
599
670
|
function deployProvider(command: string): DeployTargetInfo['provider'] {
|
|
@@ -63,7 +63,7 @@ describe('root smoo monorepo policy', () => {
|
|
|
63
63
|
const rootPackage = await readJson(join(root, 'package.json'));
|
|
64
64
|
const nxJson = await readJson(join(root, 'nx.json'));
|
|
65
65
|
expect(rootPackage.scripts).toEqual({
|
|
66
|
-
'check:linux': 'tooling/devenv
|
|
66
|
+
'check:linux': 'tooling/devenv -P linux-cross shell -- nx run-many -t cargo-lint-cross',
|
|
67
67
|
clean: 'nx run-many -t clean; nx reset',
|
|
68
68
|
'clean:node_modules':
|
|
69
69
|
'rm -rf node_modules && find e* t* p* -type d -name node_modules -print0 | xargs -0 rm -rvf',
|
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import type { ReleasePackageInfo } from '../core.js';
|
|
4
4
|
import {
|
|
5
5
|
createOrUpdateGithubRelease,
|
|
6
|
+
DEPENDENCY_ONLY_RELEASE_NOTES,
|
|
6
7
|
type GithubReleaseWriteShell,
|
|
7
8
|
githubReleaseLookupExists,
|
|
8
9
|
nxProjectChangelogArgs,
|
|
@@ -63,10 +64,13 @@ describe('GitHub release helpers', () => {
|
|
|
63
64
|
).toBe('generated release notes');
|
|
64
65
|
});
|
|
65
66
|
|
|
66
|
-
it('
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
it('renders a dependency-only body when Nx generates no project changelog', () => {
|
|
68
|
+
// Nx bumps a project "because a dependency was bumped", and such a version
|
|
69
|
+
// carries no commits of its own — so no changelog is the correct output
|
|
70
|
+
// rather than a failure. Refusing here aborted a publish AFTER the new
|
|
71
|
+
// versions had been written to every manifest, leaving the release half
|
|
72
|
+
// done: one dependency-only package took down the whole run.
|
|
73
|
+
expect(projectChangelogContents({ projectChangelogs: {} }, 'pkg')).toBe(DEPENDENCY_ONLY_RELEASE_NOTES);
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
it('distinguishes missing releases from transient GitHub lookup failures', () => {
|
|
@@ -80,12 +80,20 @@ export function nxProjectChangelogArgs(pkg: ReleasePackageInfo, previousTag: str
|
|
|
80
80
|
return previousTag ? { ...base, from: previousTag } : { ...base, firstRelease: true };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The release body for a version that carries no commits of its own.
|
|
85
|
+
*
|
|
86
|
+
* Nx bumps a project when a dependency it depends on was bumped, and reports
|
|
87
|
+
* that reason itself ("because a dependency was bumped"). Such a version has no
|
|
88
|
+
* commits touching the project, so Nx generates no changelog for it — which is
|
|
89
|
+
* correct, not a failure. Treating the absence as fatal made one dependency-only
|
|
90
|
+
* package abort a whole publish after the versions had already been written.
|
|
91
|
+
*/
|
|
92
|
+
export const DEPENDENCY_ONLY_RELEASE_NOTES =
|
|
93
|
+
'_No changelog entries for this package: this version was released because one of its dependencies was bumped._';
|
|
94
|
+
|
|
83
95
|
export function projectChangelogContents(result: ProjectChangelogLookupResult, projectName: string): string {
|
|
84
|
-
|
|
85
|
-
if (!changelog) {
|
|
86
|
-
throw new Error(`Nx did not generate a project changelog for ${projectName}.`);
|
|
87
|
-
}
|
|
88
|
-
return changelog.contents;
|
|
96
|
+
return result.projectChangelogs?.[projectName]?.contents ?? DEPENDENCY_ONLY_RELEASE_NOTES;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
export function githubReleaseLookupExists(tag: string, exitCode: number, stdout: string, stderr: string): boolean {
|
package/src/release/index.ts
CHANGED
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
} from './core.js';
|
|
90
90
|
import {
|
|
91
91
|
createOrUpdateGithubRelease,
|
|
92
|
+
DEPENDENCY_ONLY_RELEASE_NOTES,
|
|
92
93
|
githubReleaseLookupExists,
|
|
93
94
|
importWorkspaceNx,
|
|
94
95
|
renderNxProjectChangelogContents,
|
|
@@ -1596,10 +1597,9 @@ async function writeReleasePreviewSummary(
|
|
|
1596
1597
|
lines.push(` - \`${pkg.name}\`: \`${currentByName.get(pkg.name)?.version ?? 'unknown'}\` -> \`${pkg.version}\``);
|
|
1597
1598
|
}
|
|
1598
1599
|
for (const pkg of previewPackages) {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1600
|
+
// A dependency-only bump has no commits of its own, so Nx generates no
|
|
1601
|
+
// changelog for it. The preview says so rather than refusing to render.
|
|
1602
|
+
const contents = releasePreviewChangelogs.get(pkg.projectName) ?? DEPENDENCY_ONLY_RELEASE_NOTES;
|
|
1603
1603
|
lines.push('', `### ${pkg.name} ${pkg.version}`, '', contents.trim());
|
|
1604
1604
|
}
|
|
1605
1605
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { secretsMissingInEnvironment } from './commands.js';
|
|
3
|
+
import { reconcileSecrets } from './index.js';
|
|
4
|
+
|
|
5
|
+
const sources = {
|
|
6
|
+
workerSecrets: { 'targets/billing': ['PAYMENTS_PUBLISHABLE_KEY', 'PAYMENTS_SECRET_KEY'] },
|
|
7
|
+
workflowSecrets: ['PAYMENTS_PUBLISHABLE_KEY', 'PAYMENTS_SECRET_KEY'],
|
|
8
|
+
localCommands: [],
|
|
9
|
+
secretNames: {
|
|
10
|
+
PAYMENTS_PUBLISHABLE_KEY: 'PAYMENTS_PUBLISHABLE_KEY',
|
|
11
|
+
PAYMENTS_SECRET_KEY: 'PAYMENTS_SECRET_KEY',
|
|
12
|
+
},
|
|
13
|
+
repositorySecrets: ['PAYMENTS_PUBLISHABLE_KEY', 'PAYMENTS_SECRET_KEY', 'RETIRED_TOKEN'],
|
|
14
|
+
environmentSecrets: { production: ['PAYMENTS_PUBLISHABLE_KEY'] },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('what `smoo secrets set --env` walks', () => {
|
|
18
|
+
it('offers a name the repository holds and the environment does not, and no undeclared one', () => {
|
|
19
|
+
// Overriding one stage's credentials is the reason to write at environment
|
|
20
|
+
// scope: skipping names the repository already holds would leave a live
|
|
21
|
+
// value unsettable. RETIRED_TOKEN is absent from the environment too, but
|
|
22
|
+
// nothing declares it - prompting for it would ask an operator to copy a
|
|
23
|
+
// stale value forward.
|
|
24
|
+
const walked = secretsMissingInEnvironment(reconcileSecrets(sources), 'production');
|
|
25
|
+
|
|
26
|
+
expect(walked.map((row) => row.name)).toEqual(['PAYMENTS_SECRET_KEY']);
|
|
27
|
+
});
|
|
28
|
+
});
|