@smoothbricks/cli 0.3.3 โ 0.4.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.js +14 -0
- package/dist/github-ci/index.d.ts +12 -0
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +72 -4
- package/dist/monorepo/ci-workflow.d.ts +26 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -0
- package/dist/monorepo/ci-workflow.js +201 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +97 -5
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +4 -2
- package/dist/monorepo/publish-workflow.d.ts +7 -1
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +59 -15
- package/dist/monorepo/runtime.d.ts +3 -0
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +66 -2
- package/managed/raw/git-format-staged.yml +4 -0
- package/package.json +3 -3
- package/src/cli.ts +35 -5
- package/src/github-ci/index.ts +94 -8
- package/src/monorepo/__tests__/ci-workflow.test.ts +34 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +65 -2
- package/src/monorepo/ci-workflow.ts +229 -0
- package/src/monorepo/managed-files.ts +115 -5
- package/src/monorepo/package-policy.test.ts +24 -9
- package/src/monorepo/package-policy.ts +4 -2
- package/src/monorepo/publish-workflow.ts +73 -16
- package/src/monorepo/runtime.test.ts +30 -0
- package/src/monorepo/runtime.ts +94 -2
- package/src/release/__tests__/fixture-repo.test.ts +14 -13
- package/src/release/__tests__/helpers/fixture-repo.ts +85 -12
- package/managed/templates/github/workflows/ci.yml +0 -102
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
export enum CiWorkflowStepKind {
|
|
2
|
+
Checkout = 'checkout',
|
|
3
|
+
SetupDevenv = 'setup-devenv',
|
|
4
|
+
SetNxShas = 'set-nx-shas',
|
|
5
|
+
RestoreNxCache = 'restore-nx-cache',
|
|
6
|
+
Build = 'build',
|
|
7
|
+
Lint = 'lint',
|
|
8
|
+
UnitTests = 'unit-tests',
|
|
9
|
+
Deploy = 'deploy',
|
|
10
|
+
SaveNxCache = 'save-nx-cache',
|
|
11
|
+
UploadTraceDbs = 'upload-trace-dbs',
|
|
12
|
+
SaveNixDevenv = 'save-nix-devenv',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CiWorkflowStep {
|
|
16
|
+
kind: CiWorkflowStepKind;
|
|
17
|
+
name: string;
|
|
18
|
+
number: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CiWorkflowDefinitionOptions {
|
|
22
|
+
deploy: boolean;
|
|
23
|
+
deployProvider?: 'cloudflare';
|
|
24
|
+
pushBranches: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type CiWorkflowStepInput = Omit<CiWorkflowStep, 'number'>;
|
|
28
|
+
|
|
29
|
+
export function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkflowStep[] {
|
|
30
|
+
const steps: CiWorkflowStepInput[] = [
|
|
31
|
+
{ kind: CiWorkflowStepKind.Checkout, name: '๐ฅ Checkout' },
|
|
32
|
+
{ kind: CiWorkflowStepKind.SetupDevenv, name: '๐งฑ Setup Nix/devenv' },
|
|
33
|
+
{ kind: CiWorkflowStepKind.SetNxShas, name: '๐งญ Set Nx SHAs' },
|
|
34
|
+
{ kind: CiWorkflowStepKind.RestoreNxCache, name: '๐ง Restore Nx cache' },
|
|
35
|
+
{ kind: CiWorkflowStepKind.Build, name: '๐จ Build' },
|
|
36
|
+
{ kind: CiWorkflowStepKind.Lint, name: '๐ Lint' },
|
|
37
|
+
{ kind: CiWorkflowStepKind.UnitTests, name: '๐งช Unit Tests' },
|
|
38
|
+
];
|
|
39
|
+
if (options.deploy) {
|
|
40
|
+
steps.push({ kind: CiWorkflowStepKind.Deploy, name: '๐ Deploy Staging' });
|
|
41
|
+
}
|
|
42
|
+
steps.push(
|
|
43
|
+
{ kind: CiWorkflowStepKind.SaveNxCache, name: '๐พ Save Nx cache' },
|
|
44
|
+
{ kind: CiWorkflowStepKind.UploadTraceDbs, name: '๐ Upload trace DBs' },
|
|
45
|
+
{ kind: CiWorkflowStepKind.SaveNixDevenv, name: '๐งน Cleanup and cache Nix/devenv' },
|
|
46
|
+
);
|
|
47
|
+
return steps.map((step, index) => ({ ...step, number: index + 2 }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function renderCiWorkflowYaml(options: CiWorkflowDefinitionOptions): string {
|
|
51
|
+
const steps = defineCiWorkflow(options);
|
|
52
|
+
return `${renderCiWorkflowHeader(options)}${renderCiWorkflowSteps(steps, options)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderCiWorkflowHeader(options: CiWorkflowDefinitionOptions): string {
|
|
56
|
+
return `name: CI
|
|
57
|
+
|
|
58
|
+
on:
|
|
59
|
+
push:
|
|
60
|
+
branches:
|
|
61
|
+
${renderYamlList(options.pushBranches, 6)}
|
|
62
|
+
pull_request:
|
|
63
|
+
|
|
64
|
+
permissions:
|
|
65
|
+
actions: read
|
|
66
|
+
contents: read
|
|
67
|
+
statuses: write
|
|
68
|
+
|
|
69
|
+
defaults:
|
|
70
|
+
run:
|
|
71
|
+
working-directory: tooling/direnv
|
|
72
|
+
|
|
73
|
+
jobs:
|
|
74
|
+
main:
|
|
75
|
+
name: Validate
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
timeout-minutes: 45
|
|
78
|
+
env:
|
|
79
|
+
NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
|
|
80
|
+
GH_TOKEN: ${githubExpression('github.token')}
|
|
81
|
+
steps:
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function githubExpression(expression: string): string {
|
|
86
|
+
return `$${`{{ ${expression} }}`}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderCiWorkflowSteps(steps: CiWorkflowStep[], options: CiWorkflowDefinitionOptions): string {
|
|
90
|
+
const lines: string[] = [];
|
|
91
|
+
for (const step of steps) {
|
|
92
|
+
lines.push(...sectionLinesBefore(step));
|
|
93
|
+
lines.push(...commentLinesForStep(step));
|
|
94
|
+
lines.push(...yamlLinesForStep(step, options));
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sectionLinesBefore(step: CiWorkflowStep): string[] {
|
|
101
|
+
if (step.kind === CiWorkflowStepKind.SetNxShas) {
|
|
102
|
+
return [' # --- Nx -----------------------------------------------------------------', ''];
|
|
103
|
+
}
|
|
104
|
+
if (step.kind === CiWorkflowStepKind.SaveNixDevenv) {
|
|
105
|
+
return [' # --- Cleanup ------------------------------------------------------------', ''];
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function commentLinesForStep(step: CiWorkflowStep): string[] {
|
|
111
|
+
if (step.kind === CiWorkflowStepKind.Checkout) {
|
|
112
|
+
return [' # Step 1: GitHub adds "Set up job" automatically', ' # Step 2'];
|
|
113
|
+
}
|
|
114
|
+
if (step.kind === CiWorkflowStepKind.SetupDevenv) {
|
|
115
|
+
return [
|
|
116
|
+
' # Step 3. Composite action internals do not affect top-level job step',
|
|
117
|
+
' # anchors; update the nx-smart --step values below if top-level steps move.',
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
if (step.kind === CiWorkflowStepKind.SetNxShas) {
|
|
121
|
+
return [` # Step ${step.number}`, ' # Sets the base and head SHAs required for the nx affected commands'];
|
|
122
|
+
}
|
|
123
|
+
if (step.kind === CiWorkflowStepKind.SaveNxCache) {
|
|
124
|
+
return [
|
|
125
|
+
` # Step ${step.number}`,
|
|
126
|
+
" # Nx's database cache needs artifact files and .nx/workspace-data DB",
|
|
127
|
+
' # metadata restored together; GitHub Actions cache is only the archive',
|
|
128
|
+
' # transport. Save runs only after prior required steps succeed on the default',
|
|
129
|
+
' # branch, so PRs may restore shared cache but cannot publish it.',
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
return [` # Step ${step.number}`];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOptions): string[] {
|
|
136
|
+
switch (step.kind) {
|
|
137
|
+
case CiWorkflowStepKind.Checkout:
|
|
138
|
+
return [
|
|
139
|
+
` - name: ${step.name}`,
|
|
140
|
+
' uses: actions/checkout@v6.0.2',
|
|
141
|
+
' with:',
|
|
142
|
+
' filter: blob:none',
|
|
143
|
+
' fetch-depth: 0',
|
|
144
|
+
];
|
|
145
|
+
case CiWorkflowStepKind.SetupDevenv:
|
|
146
|
+
return [` - name: ${step.name}`, ' id: setup', ' uses: ./.github/actions/setup-devenv'];
|
|
147
|
+
case CiWorkflowStepKind.SetNxShas:
|
|
148
|
+
return [
|
|
149
|
+
` - name: ${step.name}`,
|
|
150
|
+
' uses: nrwl/nx-set-shas@v5.0.1',
|
|
151
|
+
' with:',
|
|
152
|
+
' workflow-id: ci.yml',
|
|
153
|
+
];
|
|
154
|
+
case CiWorkflowStepKind.RestoreNxCache:
|
|
155
|
+
return [` - name: ${step.name}`, ' id: nx-cache', ' uses: ./.github/actions/cache-nx'];
|
|
156
|
+
case CiWorkflowStepKind.Build:
|
|
157
|
+
return nxSmartStep(step, 'build', 'Build');
|
|
158
|
+
case CiWorkflowStepKind.Lint:
|
|
159
|
+
return nxSmartStep(step, 'lint', 'Lint');
|
|
160
|
+
case CiWorkflowStepKind.UnitTests:
|
|
161
|
+
return nxSmartStep(step, 'test', 'Unit Tests');
|
|
162
|
+
case CiWorkflowStepKind.Deploy:
|
|
163
|
+
return [
|
|
164
|
+
` - name: ${step.name}`,
|
|
165
|
+
' if:',
|
|
166
|
+
" ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}',",
|
|
167
|
+
' github.event.repository.default_branch) }}',
|
|
168
|
+
...deployEnvLines(options),
|
|
169
|
+
` run: smoo github-ci nx-deploy --configuration staging --mode affected --name "Deploy Staging" --step ${step.number}`,
|
|
170
|
+
];
|
|
171
|
+
case CiWorkflowStepKind.SaveNxCache:
|
|
172
|
+
return [
|
|
173
|
+
` - name: ${step.name}`,
|
|
174
|
+
' if:',
|
|
175
|
+
" ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}',",
|
|
176
|
+
" github.event.repository.default_branch) && steps.nx-cache.outputs.cache-hit != 'true' }}",
|
|
177
|
+
' uses: actions/cache/save@v5.0.5',
|
|
178
|
+
' with:',
|
|
179
|
+
' path: |',
|
|
180
|
+
' .nx/cache',
|
|
181
|
+
' .nx/workspace-data/*.db*',
|
|
182
|
+
' key: ${{ runner.os }}-nx-db-v1-${{ github.sha }}',
|
|
183
|
+
];
|
|
184
|
+
case CiWorkflowStepKind.UploadTraceDbs:
|
|
185
|
+
return [
|
|
186
|
+
` - name: ${step.name}`,
|
|
187
|
+
' if: ${{ always() }}',
|
|
188
|
+
' uses: actions/upload-artifact@v7.0.1',
|
|
189
|
+
' with:',
|
|
190
|
+
' name: trace-results-${{ github.run_id }}',
|
|
191
|
+
' path: packages/*/.trace-results.db',
|
|
192
|
+
' if-no-files-found: ignore',
|
|
193
|
+
' retention-days: 14',
|
|
194
|
+
' include-hidden-files: true',
|
|
195
|
+
];
|
|
196
|
+
case CiWorkflowStepKind.SaveNixDevenv:
|
|
197
|
+
return [
|
|
198
|
+
` - name: ${step.name}`,
|
|
199
|
+
' if: ${{ always() }}',
|
|
200
|
+
' uses: ./.github/actions/save-nix-devenv',
|
|
201
|
+
' with:',
|
|
202
|
+
' nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}',
|
|
203
|
+
' devenv-cache-hit: ${{ steps.setup.outputs.devenv-cache-hit }}',
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function deployEnvLines(options: CiWorkflowDefinitionOptions): string[] {
|
|
209
|
+
if (options.deployProvider !== 'cloudflare') {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
return [
|
|
213
|
+
' env:',
|
|
214
|
+
' CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}',
|
|
215
|
+
' CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function nxSmartStep(step: CiWorkflowStep, target: string, name: string): string[] {
|
|
220
|
+
return [
|
|
221
|
+
` - name: ${step.name}`,
|
|
222
|
+
` run: smoo github-ci nx-smart --target ${target} --name "${name}" --step ${step.number}`,
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderYamlList(values: string[], spaces: number): string {
|
|
227
|
+
const indent = ' '.repeat(spaces);
|
|
228
|
+
return values.map((value) => `${indent}- ${value}`).join('\n');
|
|
229
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
6
|
+
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
5
7
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
6
8
|
|
|
7
9
|
type ManagedKind = 'raw' | 'template' | 'generated';
|
|
@@ -21,10 +23,20 @@ export interface FileResult {
|
|
|
21
23
|
|
|
22
24
|
interface ManagedFileContext {
|
|
23
25
|
hasReleasePackages: boolean;
|
|
26
|
+
hasStagingDeployTargets: boolean;
|
|
27
|
+
hasProductionDeployTargets: boolean;
|
|
28
|
+
stagingDeployProvider?: 'cloudflare';
|
|
29
|
+
productionDeployProvider?: 'cloudflare';
|
|
30
|
+
ciPushBranches: string[];
|
|
24
31
|
nodeModulesCacheKey: string;
|
|
25
32
|
repoName: string;
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
interface DeployTargetInfo {
|
|
36
|
+
exists: boolean;
|
|
37
|
+
provider?: 'cloudflare';
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
const managedFiles: ManagedFile[] = [
|
|
29
41
|
{
|
|
30
42
|
kind: 'raw',
|
|
@@ -56,8 +68,8 @@ const managedFiles: ManagedFile[] = [
|
|
|
56
68
|
target: '.git-format-staged.yml',
|
|
57
69
|
},
|
|
58
70
|
{
|
|
59
|
-
kind: '
|
|
60
|
-
source: '
|
|
71
|
+
kind: 'generated',
|
|
72
|
+
source: 'ci-workflow',
|
|
61
73
|
target: '.github/workflows/ci.yml',
|
|
62
74
|
},
|
|
63
75
|
{
|
|
@@ -106,7 +118,7 @@ function applyManagedFile(
|
|
|
106
118
|
mode: 'update' | 'check' | 'diff',
|
|
107
119
|
context: ManagedFileContext,
|
|
108
120
|
): FileResult {
|
|
109
|
-
if (file.releasePackagesOnly === true && !context.hasReleasePackages) {
|
|
121
|
+
if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
|
|
110
122
|
return { target: file.target, action: 'skipped' };
|
|
111
123
|
}
|
|
112
124
|
const target = resolve(root, file.target);
|
|
@@ -138,8 +150,19 @@ function applyManagedFile(
|
|
|
138
150
|
|
|
139
151
|
function getManagedContent(file: ManagedFile, context: ManagedFileContext): string {
|
|
140
152
|
if (file.kind === 'generated') {
|
|
153
|
+
if (file.source === 'ci-workflow') {
|
|
154
|
+
return renderCiWorkflowYaml({
|
|
155
|
+
deploy: context.hasStagingDeployTargets,
|
|
156
|
+
deployProvider: context.stagingDeployProvider,
|
|
157
|
+
pushBranches: context.ciPushBranches,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
141
160
|
if (file.source === 'publish-workflow') {
|
|
142
|
-
return renderPublishWorkflowYaml({
|
|
161
|
+
return renderPublishWorkflowYaml({
|
|
162
|
+
deploy: context.hasProductionDeployTargets,
|
|
163
|
+
deployProvider: context.productionDeployProvider,
|
|
164
|
+
repoName: context.repoName,
|
|
165
|
+
});
|
|
143
166
|
}
|
|
144
167
|
throw new Error(`Unknown generated managed file source ${file.source}`);
|
|
145
168
|
}
|
|
@@ -155,18 +178,105 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
155
178
|
function getManagedFileContext(root: string): ManagedFileContext {
|
|
156
179
|
const packageJson = readPackageJson(join(root, 'package.json'));
|
|
157
180
|
const repoName = packageJson?.name ?? 'monorepo';
|
|
181
|
+
const ciPushBranches = getCiPushBranches(packageJson?.json);
|
|
182
|
+
const stagingDeploy = getDeployTargetInfo(root, 'staging');
|
|
183
|
+
const productionDeploy = getDeployTargetInfo(root, 'production');
|
|
158
184
|
const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
|
|
159
185
|
? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
|
|
160
186
|
: `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
|
|
161
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
hasReleasePackages: listReleasePackages(root, packageJson).length > 0,
|
|
189
|
+
hasStagingDeployTargets: stagingDeploy.exists,
|
|
190
|
+
hasProductionDeployTargets: productionDeploy.exists,
|
|
191
|
+
stagingDeployProvider: stagingDeploy.provider,
|
|
192
|
+
productionDeployProvider: productionDeploy.provider,
|
|
193
|
+
ciPushBranches,
|
|
194
|
+
nodeModulesCacheKey,
|
|
195
|
+
repoName,
|
|
196
|
+
};
|
|
162
197
|
}
|
|
163
198
|
|
|
164
199
|
function renderTemplate(context: ManagedFileContext, template: string): string {
|
|
165
200
|
return template
|
|
166
201
|
.replaceAll('{{REPO_NAME}}', context.repoName)
|
|
202
|
+
.replaceAll('__SMOO_CI_PUSH_BRANCHES__', renderYamlFlowList(context.ciPushBranches))
|
|
167
203
|
.replaceAll('{{NODE_MODULES_CACHE_KEY}}', context.nodeModulesCacheKey);
|
|
168
204
|
}
|
|
169
205
|
|
|
206
|
+
function getDeployTargetInfo(root: string, configuration: string): DeployTargetInfo {
|
|
207
|
+
const output = execFileSync('nx', ['show', 'projects', '--withTarget', 'deploy', '--json'], {
|
|
208
|
+
cwd: root,
|
|
209
|
+
encoding: 'utf8',
|
|
210
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
211
|
+
});
|
|
212
|
+
const projects: unknown = JSON.parse(output);
|
|
213
|
+
if (!Array.isArray(projects) || !projects.every((project) => typeof project === 'string')) {
|
|
214
|
+
return { exists: false };
|
|
215
|
+
}
|
|
216
|
+
let exists = false;
|
|
217
|
+
let provider: DeployTargetInfo['provider'];
|
|
218
|
+
for (const project of projects) {
|
|
219
|
+
const target = nxDeployTarget(root, project, configuration);
|
|
220
|
+
if (!target.exists) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
exists = true;
|
|
224
|
+
provider ??= target.provider;
|
|
225
|
+
}
|
|
226
|
+
return { exists, provider };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function nxDeployTarget(root: string, project: string, configuration: string): DeployTargetInfo {
|
|
230
|
+
const output = execFileSync('nx', ['show', 'project', project, '--json'], {
|
|
231
|
+
cwd: root,
|
|
232
|
+
encoding: 'utf8',
|
|
233
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
234
|
+
});
|
|
235
|
+
const parsed: unknown = JSON.parse(output);
|
|
236
|
+
const projectJson = recordValue(parsed);
|
|
237
|
+
const targets = recordValue(projectJson?.targets);
|
|
238
|
+
const deploy = recordValue(targets?.deploy);
|
|
239
|
+
const configurations = recordValue(deploy?.configurations);
|
|
240
|
+
const config = recordValue(configurations?.[configuration]);
|
|
241
|
+
if (!config) {
|
|
242
|
+
return { exists: false };
|
|
243
|
+
}
|
|
244
|
+
const command = deployCommand(deploy, config);
|
|
245
|
+
return { exists: true, provider: command.includes('wrangler ') ? 'cloudflare' : undefined };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function deployCommand(deploy: Record<string, unknown> | undefined, config: Record<string, unknown>): string {
|
|
249
|
+
const command = stringValue(config.command) ?? stringValue(recordValue(config.options)?.command);
|
|
250
|
+
return command ?? stringValue(recordValue(deploy?.options)?.command) ?? '';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stringValue(value: unknown): string | undefined {
|
|
254
|
+
return typeof value === 'string' ? value : undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getCiPushBranches(packageJson: unknown): string[] {
|
|
258
|
+
const configured = readCiPushBranches(packageJson);
|
|
259
|
+
return configured.length > 0 ? configured : ['main'];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readCiPushBranches(packageJson: unknown): string[] {
|
|
263
|
+
const rootPackage = recordValue(packageJson);
|
|
264
|
+
const smoo = recordValue(rootPackage?.smoo);
|
|
265
|
+
const github = recordValue(smoo?.github);
|
|
266
|
+
const branches = Array.isArray(github?.pushBranches) ? github.pushBranches : [];
|
|
267
|
+
return branches.filter((branch): branch is string => typeof branch === 'string' && branch.length > 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
271
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
272
|
+
? (value as Record<string, unknown>)
|
|
273
|
+
: undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderYamlFlowList(values: string[]): string {
|
|
277
|
+
return JSON.stringify(values);
|
|
278
|
+
}
|
|
279
|
+
|
|
170
280
|
function writeManagedFile(path: string, content: string, executable: boolean): void {
|
|
171
281
|
mkdirSync(dirname(path), { recursive: true });
|
|
172
282
|
writeFileSync(path, content, { mode: executable ? 0o755 : 0o644 });
|
|
@@ -5,8 +5,12 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import {
|
|
6
6
|
BOUNDED_TEST_EXECUTOR,
|
|
7
7
|
BOUNDED_TEST_KILL_AFTER_MS,
|
|
8
|
+
BOUNDED_TEST_PER_TEST_TIMEOUT_MS,
|
|
8
9
|
BOUNDED_TEST_TIMEOUT_MS,
|
|
9
10
|
} from '@smoothbricks/nx-plugin/bounded-test-policy';
|
|
11
|
+
|
|
12
|
+
const TIMEOUT_FLAG = `--timeout=${BOUNDED_TEST_PER_TEST_TIMEOUT_MS}`;
|
|
13
|
+
|
|
10
14
|
import {
|
|
11
15
|
applyFixableMonorepoDefaults,
|
|
12
16
|
applyNxPluginDefaults,
|
|
@@ -47,14 +51,17 @@ describe('root smoo monorepo policy', () => {
|
|
|
47
51
|
);
|
|
48
52
|
await writeJson(join(root, 'nx.json'), validNxJson());
|
|
49
53
|
|
|
50
|
-
expect(validateRootPackagePolicy(root)).toBe(
|
|
51
|
-
expect(validateNxReleaseConfig(root)).toBe(
|
|
54
|
+
expect(validateRootPackagePolicy(root)).toBe(7);
|
|
55
|
+
expect(validateNxReleaseConfig(root)).toBe(8);
|
|
52
56
|
|
|
53
57
|
applyFixableMonorepoDefaults(root);
|
|
54
58
|
|
|
55
59
|
const rootPackage = await readJson(join(root, 'package.json'));
|
|
56
60
|
const nxJson = await readJson(join(root, 'nx.json'));
|
|
57
61
|
expect(rootPackage.scripts).toEqual({
|
|
62
|
+
clean: 'nx run-many -t clean; nx reset',
|
|
63
|
+
'clean:node_modules':
|
|
64
|
+
'rm -rf node_modules && find e* t* p* -type d -name node_modules -print0 | xargs -0 rm -rvf',
|
|
58
65
|
'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
|
|
59
66
|
'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
|
|
60
67
|
lint: 'nx run-many -t lint',
|
|
@@ -62,7 +69,7 @@ describe('root smoo monorepo policy', () => {
|
|
|
62
69
|
});
|
|
63
70
|
expect(rootPackage).toMatchObject({ nx: { includedScripts: [] } });
|
|
64
71
|
expect(nxJson.namedInputs).toEqual(validNamedInputs());
|
|
65
|
-
expect(nxJson.targetDefaults).toEqual(
|
|
72
|
+
expect(nxJson.targetDefaults).toEqual(validTargetDefaults());
|
|
66
73
|
expect(nxJson.plugins).toEqual([
|
|
67
74
|
{
|
|
68
75
|
plugin: '@nx/js/typescript',
|
|
@@ -148,12 +155,12 @@ describe('root smoo monorepo policy', () => {
|
|
|
148
155
|
},
|
|
149
156
|
});
|
|
150
157
|
|
|
151
|
-
expect(validateNxReleaseConfig(root)).toBe(
|
|
158
|
+
expect(validateNxReleaseConfig(root)).toBe(6);
|
|
152
159
|
|
|
153
160
|
applyFixableMonorepoDefaults(root);
|
|
154
161
|
|
|
155
162
|
const nxJson = await readJson(join(root, 'nx.json'));
|
|
156
|
-
expect(nxJson.targetDefaults).toEqual(
|
|
163
|
+
expect(nxJson.targetDefaults).toEqual(validTargetDefaults());
|
|
157
164
|
expect(validateNxReleaseConfig(root)).toBe(0);
|
|
158
165
|
} finally {
|
|
159
166
|
await rm(root, { recursive: true, force: true });
|
|
@@ -595,7 +602,7 @@ describe('workspace package script policy', () => {
|
|
|
595
602
|
test: {
|
|
596
603
|
executor: BOUNDED_TEST_EXECUTOR,
|
|
597
604
|
options: {
|
|
598
|
-
command:
|
|
605
|
+
command: `bun test ${TIMEOUT_FLAG}`,
|
|
599
606
|
cwd: '{projectRoot}',
|
|
600
607
|
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
601
608
|
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
@@ -825,7 +832,7 @@ describe('workspace package script policy', () => {
|
|
|
825
832
|
test: {
|
|
826
833
|
executor: BOUNDED_TEST_EXECUTOR,
|
|
827
834
|
options: {
|
|
828
|
-
command:
|
|
835
|
+
command: `bun test ${TIMEOUT_FLAG} --pass-with-no-tests`,
|
|
829
836
|
cwd: '{projectRoot}',
|
|
830
837
|
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
831
838
|
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
@@ -970,7 +977,7 @@ describe('workspace package script policy', () => {
|
|
|
970
977
|
test: {
|
|
971
978
|
executor: BOUNDED_TEST_EXECUTOR,
|
|
972
979
|
options: {
|
|
973
|
-
command:
|
|
980
|
+
command: `bun test ${TIMEOUT_FLAG} --pass-with-no-tests`,
|
|
974
981
|
cwd: '{projectRoot}',
|
|
975
982
|
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
976
983
|
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
@@ -1076,6 +1083,7 @@ function validNxJson(): Record<string, unknown> {
|
|
|
1076
1083
|
},
|
|
1077
1084
|
releaseTag: { pattern: '{projectName}@{version}' },
|
|
1078
1085
|
changelog: {
|
|
1086
|
+
automaticFromRef: true,
|
|
1079
1087
|
workspaceChangelog: false,
|
|
1080
1088
|
projectChangelogs: {
|
|
1081
1089
|
createRelease: false,
|
|
@@ -1091,7 +1099,7 @@ function validConfiguredNxJson(): Record<string, unknown> {
|
|
|
1091
1099
|
return {
|
|
1092
1100
|
...validNxJson(),
|
|
1093
1101
|
namedInputs: validNamedInputs(),
|
|
1094
|
-
targetDefaults:
|
|
1102
|
+
targetDefaults: validTargetDefaults(),
|
|
1095
1103
|
plugins: [
|
|
1096
1104
|
{
|
|
1097
1105
|
plugin: '@nx/js/typescript',
|
|
@@ -1110,6 +1118,13 @@ function validConfiguredNxJson(): Record<string, unknown> {
|
|
|
1110
1118
|
};
|
|
1111
1119
|
}
|
|
1112
1120
|
|
|
1121
|
+
function validTargetDefaults(): Record<string, unknown> {
|
|
1122
|
+
return {
|
|
1123
|
+
build: { cache: true, outputs: ['{projectRoot}/dist'] },
|
|
1124
|
+
clean: { executor: '@smoothbricks/nx-plugin:clean-outputs', cache: false },
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1113
1128
|
function validNamedInputs(): Record<string, unknown> {
|
|
1114
1129
|
return {
|
|
1115
1130
|
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
@@ -48,10 +48,12 @@ export { SMOO_NX_RELEASE_TAG_PATTERN, SMOO_NX_VERSION_ACTIONS };
|
|
|
48
48
|
|
|
49
49
|
const extraCommitScopes = ['release'];
|
|
50
50
|
const rootScriptPolicy: Record<string, string> = {
|
|
51
|
+
clean: 'nx run-many -t clean; nx reset',
|
|
52
|
+
'clean:node_modules': 'rm -rf node_modules && find e* t* p* -type d -name node_modules -print0 | xargs -0 rm -rvf',
|
|
53
|
+
'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
|
|
54
|
+
'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
|
|
51
55
|
lint: 'nx run-many -t lint',
|
|
52
56
|
'lint:fix': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --unstaged',
|
|
53
|
-
'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
|
|
54
|
-
'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
export function applyFixableMonorepoDefaults(root: string): void {
|