@smoothbricks/cli 0.10.5 โ 0.10.7
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 +3 -2
- package/dist/github-ci/index.d.ts +1 -0
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +28 -5
- package/dist/lib/json.d.ts +3 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +5 -20
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +1 -0
- package/dist/monorepo/ci-workflow.d.ts +2 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +10 -4
- package/dist/monorepo/github-runs-on.d.ts +14 -0
- package/dist/monorepo/github-runs-on.d.ts.map +1 -0
- package/dist/monorepo/github-runs-on.js +43 -0
- package/dist/monorepo/index.d.ts +2 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +7 -7
- package/dist/monorepo/managed-files.d.ts +14 -3
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +54 -74
- package/dist/monorepo/package-policy.js +2 -2
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +3 -1
- package/dist/monorepo/publish-workflow.d.ts +3 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +17 -14
- package/dist/monorepo/runtime.d.ts +2 -2
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +11 -16
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +20 -23
- package/dist/nx/index.d.ts +7 -4
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +117 -56
- package/managed/raw/tooling/devenv +71 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +24 -7
- package/managed/templates/github/actions/cache-nix-devenv/action.yml +13 -11
- package/managed/templates/github/actions/save-nix-devenv/action.yml +34 -3
- package/managed/templates/github/actions/setup-devenv/action.yml +44 -15
- package/managed/templates/github/workflows/managed-files.yml +64 -12
- package/package.json +4 -4
- package/src/cli.ts +10 -3
- package/src/github-ci/index.test.ts +53 -4
- package/src/github-ci/index.ts +37 -5
- package/src/lib/json.ts +3 -1
- package/src/lib/workspace.ts +1 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +37 -1
- package/src/monorepo/__tests__/publish-workflow.test.ts +39 -4
- package/src/monorepo/ci-workflow.ts +14 -4
- package/src/monorepo/github-runs-on.ts +45 -0
- package/src/monorepo/index.ts +7 -7
- package/src/monorepo/managed-files.test.ts +58 -0
- package/src/monorepo/managed-files.ts +59 -76
- package/src/monorepo/package-policy.test.ts +31 -0
- package/src/monorepo/package-policy.ts +2 -2
- package/src/monorepo/packs/index.ts +3 -1
- package/src/monorepo/publish-workflow.ts +28 -15
- package/src/monorepo/runtime.test.ts +23 -17
- package/src/monorepo/runtime.ts +12 -17
- package/src/monorepo/tool-validation.test.ts +15 -8
- package/src/monorepo/tool-validation.ts +20 -25
- package/src/nx/index.test.ts +45 -14
- package/src/nx/index.ts +133 -64
- package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
|
@@ -1,44 +1,71 @@
|
|
|
1
1
|
name: Setup Nix/devenv
|
|
2
|
-
description:
|
|
2
|
+
description: >
|
|
3
|
+
Build the SmoothBricks devenv shell after checkout. Restores the small devenv/direnv actions/cache segment on every
|
|
4
|
+
runner (eval SQLite + shell scripts). On GARM/NixOS (NIX_REMOTE=daemon + /var/cache/ci) skips Nix install and the
|
|
5
|
+
large store NAR (host store). On ubuntu/macOS installs Nix and restores the NAR/profile cache.
|
|
3
6
|
|
|
4
7
|
outputs:
|
|
5
8
|
nix-cache-hit:
|
|
6
9
|
description: Whether the Nix profile/store cache restored from the exact primary key.
|
|
7
|
-
value: ${{ steps.nix-cache.outputs.cache-hit }}
|
|
10
|
+
value: ${{ steps.nix-cache.outputs.cache-hit || 'false' }}
|
|
8
11
|
devenv-cache-hit:
|
|
9
12
|
description: Whether the devenv/direnv cache restored from the exact primary key.
|
|
10
|
-
value: ${{ steps.devenv-cache.outputs.cache-hit }}
|
|
13
|
+
value: ${{ steps.devenv-cache.outputs.cache-hit || 'false' }}
|
|
14
|
+
host-runner:
|
|
15
|
+
description: Whether this job is on a host-nix runner (GARM) with shared store/caches.
|
|
16
|
+
value: ${{ steps.runner-kind.outputs.host }}
|
|
11
17
|
|
|
12
18
|
runs:
|
|
13
19
|
using: composite
|
|
14
20
|
steps:
|
|
15
21
|
# This action is intentionally post-checkout only. Repo-local actions do not
|
|
16
22
|
# exist before actions/checkout has populated the working tree.
|
|
23
|
+
|
|
24
|
+
# Runner image/profile owns paths (NIX_REMOTE, /var/cache/ci/*). Detect here
|
|
25
|
+
# so every workflow can `uses: setup-devenv` without forking step graphs.
|
|
26
|
+
- name: ๐ Runner kind
|
|
27
|
+
id: runner-kind
|
|
28
|
+
shell: bash
|
|
29
|
+
run: |
|
|
30
|
+
if [ "${NIX_REMOTE:-}" = daemon ] && [ -d /var/cache/ci ]; then
|
|
31
|
+
echo "host=true" >> "$GITHUB_OUTPUT"
|
|
32
|
+
echo "host-nix runner (shared store + /var/cache/ci)"
|
|
33
|
+
else
|
|
34
|
+
echo "host=false" >> "$GITHUB_OUTPUT"
|
|
35
|
+
echo "ephemeral runner (install Nix + GH caches)"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Devenv segment (own key): tooling/direnv/.devenv + .direnv โ includes
|
|
39
|
+
# nix-eval-cache.db. Independent of the nix NAR; restore on host and ephemeral
|
|
40
|
+
# whether or not the store cache hit. Missing store paths are fine: devenv
|
|
41
|
+
# verifies shell drv/out exist on eval-cache hit and invalidates + re-evals
|
|
42
|
+
# / realizes when GC removed them. Packages still come from host store, NAR,
|
|
43
|
+
# or Cachix โ this blob only skips evaluation.
|
|
44
|
+
- name: ๐ง Cache devenv and direnv
|
|
45
|
+
id: devenv-cache
|
|
46
|
+
uses: ./.github/actions/cache-nix-devenv
|
|
47
|
+
with:
|
|
48
|
+
segment: devenv
|
|
49
|
+
|
|
17
50
|
- name: โ๏ธ Install Nix
|
|
51
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
18
52
|
uses: DeterminateSystems/determinate-nix-action@v3
|
|
19
53
|
|
|
20
54
|
- name: ๐ง Cache Nix profiles and store NAR
|
|
21
55
|
id: nix-cache
|
|
56
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
22
57
|
uses: ./.github/actions/cache-nix-devenv
|
|
23
58
|
with:
|
|
24
59
|
segment: nix
|
|
25
60
|
|
|
26
61
|
- name: โป๏ธ Restore Nix store from NAR
|
|
62
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
27
63
|
shell: bash
|
|
28
64
|
working-directory: tooling/direnv
|
|
29
65
|
run: ./github-actions-bootstrap.sh restore-store
|
|
30
66
|
|
|
31
|
-
# Restore direnv state only when the Nix cache key is exact. Partial Nix
|
|
32
|
-
# restores can contain a NAR for a different shell closure, and .direnv
|
|
33
|
-
# points directly at derivations inside that closure.
|
|
34
|
-
- name: ๐ง Cache devenv and direnv
|
|
35
|
-
id: devenv-cache
|
|
36
|
-
if: ${{ steps.nix-cache.outputs.cache-hit == 'true' }}
|
|
37
|
-
uses: ./.github/actions/cache-nix-devenv
|
|
38
|
-
with:
|
|
39
|
-
segment: devenv
|
|
40
|
-
|
|
41
67
|
- name: โก Enable Cachix
|
|
68
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
42
69
|
uses: cachix/cachix-action@v17
|
|
43
70
|
with:
|
|
44
71
|
name: devenv
|
|
@@ -48,12 +75,14 @@ runs:
|
|
|
48
75
|
working-directory: tooling/direnv
|
|
49
76
|
run: ./github-actions-bootstrap.sh install-devenv
|
|
50
77
|
|
|
78
|
+
# node_modules + ttsc plugin binaries: GH Actions cache on ephemeral runners.
|
|
79
|
+
# Host runners keep those under the /var/cache/ci bind (bun / ttsc env).
|
|
51
80
|
- name: ๐ฆ Cache node_modules
|
|
81
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
52
82
|
uses: ./.github/actions/cache-node-modules
|
|
53
83
|
|
|
54
|
-
# Restores compiled ttsc native plugins into .cache/ttsc/plugins. TTSC_CACHE_DIR
|
|
55
|
-
# is set by devenv enterShell (+ bootstrap GITHUB_ENV for later steps).
|
|
56
84
|
- name: ๐ง Cache ttsc plugins
|
|
85
|
+
if: steps.runner-kind.outputs.host != 'true'
|
|
57
86
|
uses: ./.github/actions/cache-ttsc-plugins
|
|
58
87
|
|
|
59
88
|
- name: ๐ Build devenv shell
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
# change is upstream code that deserves a human look before it lands. Re-runs
|
|
10
10
|
# refresh the same branch in place.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
12
|
+
# REQUIRED SECRET: MANAGED_FILES_TOKEN
|
|
13
|
+
# Managed files include .github/workflows/*. The default GITHUB_TOKEN is
|
|
14
|
+
# server-side forbidden to create/update workflow files, even with
|
|
15
|
+
# contents:write. Configure a GitHub App installation token or classic/fine-
|
|
16
|
+
# grained PAT with:
|
|
17
|
+
# - contents: write
|
|
18
|
+
# - pull-requests: write
|
|
19
|
+
# - workflows: write
|
|
20
|
+
# as repository secret MANAGED_FILES_TOKEN. Without it, any update that
|
|
21
|
+
# touches workflow YAML fails at push (this is the usual "managed files
|
|
22
|
+
# workflow isn't doing anything" failure mode).
|
|
16
23
|
name: Managed files
|
|
17
24
|
|
|
18
25
|
on:
|
|
@@ -41,6 +48,7 @@ jobs:
|
|
|
41
48
|
filter: blob:none
|
|
42
49
|
fetch-depth: 0
|
|
43
50
|
persist-credentials: true
|
|
51
|
+
# Prefer MANAGED_FILES_TOKEN so push can update workflow files.
|
|
44
52
|
token: ${{ secrets.MANAGED_FILES_TOKEN || github.token }}
|
|
45
53
|
|
|
46
54
|
- name: ๐งฑ Setup Nix/devenv
|
|
@@ -53,27 +61,71 @@ jobs:
|
|
|
53
61
|
- name: ๐ค Push review branch and open PR
|
|
54
62
|
env:
|
|
55
63
|
GH_TOKEN: ${{ secrets.MANAGED_FILES_TOKEN || github.token }}
|
|
64
|
+
# secrets.* is empty when unset โ used to fail before a useless push.
|
|
65
|
+
MANAGED_FILES_TOKEN_CONFIGURED: ${{ secrets.MANAGED_FILES_TOKEN != '' }}
|
|
56
66
|
run: |
|
|
67
|
+
set -euo pipefail
|
|
57
68
|
cd "$(git rev-parse --show-toplevel)"
|
|
69
|
+
|
|
58
70
|
if [ -z "$(git status --porcelain)" ]; then
|
|
59
71
|
echo "Managed files are in sync; nothing to do."
|
|
60
72
|
exit 0
|
|
61
73
|
fi
|
|
74
|
+
|
|
75
|
+
echo "Pending managed-file changes:"
|
|
76
|
+
git status --porcelain
|
|
77
|
+
|
|
78
|
+
workflow_changes="$(git status --porcelain -- .github/workflows || true)"
|
|
79
|
+
if [ -n "$workflow_changes" ] && [ "${MANAGED_FILES_TOKEN_CONFIGURED}" != "true" ]; then
|
|
80
|
+
{
|
|
81
|
+
echo "::error title=MANAGED_FILES_TOKEN required::Managed update touches .github/workflows/* but repository secret MANAGED_FILES_TOKEN is not set."
|
|
82
|
+
echo ""
|
|
83
|
+
echo "GitHub refuses pushes that create/update workflow files when the credentials are the default GITHUB_TOKEN / a GitHub App without the workflows permission:"
|
|
84
|
+
echo " refusing to allow a GitHub App to create or update workflow ... without \`workflows\` permission"
|
|
85
|
+
echo ""
|
|
86
|
+
echo "Fix:"
|
|
87
|
+
echo " 1. Create a GitHub App (or PAT) with contents:write, pull-requests:write, workflows:write"
|
|
88
|
+
echo " 2. Install it on this repo"
|
|
89
|
+
echo " 3. Store an installation token or PAT as repository secret MANAGED_FILES_TOKEN"
|
|
90
|
+
echo " 4. Re-run this workflow"
|
|
91
|
+
echo ""
|
|
92
|
+
echo "Workflow files that would be updated:"
|
|
93
|
+
echo "$workflow_changes"
|
|
94
|
+
} >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
62
98
|
git config user.name "github-actions[bot]"
|
|
63
99
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
64
100
|
git switch -C chore/managed-files
|
|
65
101
|
git add -A
|
|
66
102
|
git commit -m "chore: regenerate smoo-managed files"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
|
|
104
|
+
if ! git push --force origin chore/managed-files 2> /tmp/managed-files-push.err; then
|
|
105
|
+
cat /tmp/managed-files-push.err >&2
|
|
106
|
+
if grep -q 'workflows' /tmp/managed-files-push.err; then
|
|
107
|
+
{
|
|
108
|
+
echo ""
|
|
109
|
+
echo "::error title=workflows permission missing::Push rejected because the token cannot update GitHub Actions workflow files."
|
|
110
|
+
echo "Set repository secret MANAGED_FILES_TOKEN to a GitHub App/PAT with contents:write + pull-requests:write + workflows:write, then re-run."
|
|
111
|
+
} >&2
|
|
112
|
+
fi
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if ! gh pr view chore/managed-files --json number --jq .number >/dev/null 2>&1; then
|
|
117
|
+
gh pr create \
|
|
118
|
+
--head chore/managed-files \
|
|
119
|
+
--base "${GITHUB_REF_NAME}" \
|
|
120
|
+
--title "chore: regenerate smoo-managed files" \
|
|
121
|
+
--body "Automated refresh of smoo-managed files after drift (usually a \`@smoothbricks/cli\` pin bump). One persistent PR; re-runs force-push this branch in place. Review the template changes โ do NOT enable auto-merge."
|
|
122
|
+
else
|
|
123
|
+
echo "PR already open for chore/managed-files; branch refreshed in place."
|
|
124
|
+
fi
|
|
74
125
|
|
|
75
126
|
- name: ๐งน Cleanup and cache Nix/devenv
|
|
76
|
-
|
|
127
|
+
# success() is default; always() still saves GH Nix cache after a red job.
|
|
128
|
+
if: always()
|
|
77
129
|
uses: ./.github/actions/save-nix-devenv
|
|
78
130
|
with:
|
|
79
131
|
nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@arethetypeswrong/core": "^0.18.2",
|
|
66
66
|
"@prettier/sync": "^0.6.1",
|
|
67
|
-
"@smoothbricks/nx-plugin": "0.3.
|
|
68
|
-
"@smoothbricks/validation": "0.1.
|
|
67
|
+
"@smoothbricks/nx-plugin": "0.3.5",
|
|
68
|
+
"@smoothbricks/validation": "0.1.5",
|
|
69
69
|
"commander": "^14.0.3",
|
|
70
70
|
"prettier": "^3.6.1",
|
|
71
71
|
"publint": "^0.3.18",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"sherif": "^1.11.1",
|
|
74
74
|
"toml-eslint-parser": "^1.0.3",
|
|
75
75
|
"tslib": "^2.8.1",
|
|
76
|
-
"typia": "13.
|
|
76
|
+
"typia": "13.2.0"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"fast-check": "^4.4.0"
|
package/src/cli.ts
CHANGED
|
@@ -66,11 +66,11 @@ function buildProgram(): Command {
|
|
|
66
66
|
.option('--warn', 'report drift as warnings (GitHub annotations) instead of failing')
|
|
67
67
|
.action(async (options: { warn?: boolean }) => {
|
|
68
68
|
const { checkManagedFiles } = await import('./monorepo/index.js');
|
|
69
|
-
checkManagedFiles(await findRepoRoot(), { warn: options.warn });
|
|
69
|
+
await checkManagedFiles(await findRepoRoot(), { warn: options.warn });
|
|
70
70
|
});
|
|
71
71
|
monorepo.command('diff').action(async () => {
|
|
72
72
|
const { diffManagedFiles } = await import('./monorepo/index.js');
|
|
73
|
-
diffManagedFiles(await findRepoRoot());
|
|
73
|
+
await diffManagedFiles(await findRepoRoot());
|
|
74
74
|
});
|
|
75
75
|
monorepo
|
|
76
76
|
.command('validate-commit-msg <commitMsgFile>')
|
|
@@ -348,10 +348,17 @@ function buildProgram(): Command {
|
|
|
348
348
|
.command('nx-run-many')
|
|
349
349
|
.requiredOption('--targets <targets>')
|
|
350
350
|
.option('--projects <projects>')
|
|
351
|
+
.option('--projects-with-targets <targets>', 'select projects owning any comma-separated target or target glob')
|
|
351
352
|
.option('--configuration <configuration>')
|
|
352
353
|
.option('--collect-outputs <directory>')
|
|
353
354
|
.action(
|
|
354
|
-
async (options: {
|
|
355
|
+
async (options: {
|
|
356
|
+
targets: string;
|
|
357
|
+
projects?: string;
|
|
358
|
+
projectsWithTargets?: string;
|
|
359
|
+
configuration?: string;
|
|
360
|
+
collectOutputs?: string;
|
|
361
|
+
}) => {
|
|
355
362
|
const { githubCiNxRunMany } = await import('./github-ci/index.js');
|
|
356
363
|
await githubCiNxRunMany(await findRepoRoot(), options);
|
|
357
364
|
},
|
|
@@ -38,7 +38,7 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
38
38
|
['test', ['api', 'desktop']],
|
|
39
39
|
]);
|
|
40
40
|
expect(expanded.runs.map((run) => nxRunManyArgs(run))).toEqual([
|
|
41
|
-
['run-many', '-t', 'test', '--projects=api,desktop', '--parallel=
|
|
41
|
+
['run-many', '-t', 'test', '--projects=api,desktop', '--parallel=100%'],
|
|
42
42
|
]);
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -53,6 +53,28 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
53
53
|
]);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it('selects test owners from target globs and fails if any selected project lacks test', () => {
|
|
57
|
+
const platformProjects = projects.map((project) =>
|
|
58
|
+
project.project === 'mobile' ? { ...project, targets: [...project.targets, 'test'] } : project,
|
|
59
|
+
);
|
|
60
|
+
const expanded = expandNxTargetRuns(platformProjects, {
|
|
61
|
+
targets: 'test',
|
|
62
|
+
projectsWithTargets: '*-macos,*-ios',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(expanded.runs.map((run) => [run.target, run.projects.map((project) => project.project)])).toEqual([
|
|
66
|
+
['test', ['desktop', 'mobile']],
|
|
67
|
+
]);
|
|
68
|
+
const [testRun] = expanded.runs;
|
|
69
|
+
if (!testRun) {
|
|
70
|
+
throw new Error('Expected a test target run.');
|
|
71
|
+
}
|
|
72
|
+
expect(nxRunManyArgs(testRun)).toEqual(['run-many', '-t', 'test', '--projects=desktop,mobile', '--parallel=100%']);
|
|
73
|
+
expect(() => expandNxTargetRuns(projects, { targets: 'test', projectsWithTargets: '*-macos,*-ios' })).toThrow(
|
|
74
|
+
'Nx target test is missing for project(s) selected by --projects-with-targets *-macos,*-ios: mobile.',
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
56
78
|
it('reports a zero-match glob as an empty run set', () => {
|
|
57
79
|
expect(expandNxTargetRuns(projects, { targets: '*-windows' })).toEqual({
|
|
58
80
|
runs: [],
|
|
@@ -65,7 +87,7 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
65
87
|
|
|
66
88
|
expect(expanded.unmatchedGlobs).toEqual([]);
|
|
67
89
|
expect(expanded.runs.map((run) => nxRunManyArgs(run))).toEqual([
|
|
68
|
-
['run-many', '-t', 'missing', '--projects=api', '--parallel=
|
|
90
|
+
['run-many', '-t', 'missing', '--projects=api', '--parallel=100%'],
|
|
69
91
|
]);
|
|
70
92
|
});
|
|
71
93
|
|
|
@@ -114,7 +136,7 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
114
136
|
'-t',
|
|
115
137
|
'test',
|
|
116
138
|
'--exclude=tag:ci:skip:test',
|
|
117
|
-
'--parallel=
|
|
139
|
+
'--parallel=100%',
|
|
118
140
|
]);
|
|
119
141
|
expect(nxRunManyArgs({ target: 'test', projects: projects.slice(0, 1) })).not.toContain(
|
|
120
142
|
'--exclude=tag:ci:skip:test',
|
|
@@ -155,6 +177,21 @@ describe('collected Nx outputs', () => {
|
|
|
155
177
|
});
|
|
156
178
|
});
|
|
157
179
|
|
|
180
|
+
it('runs test for projects selected by platform target ownership', async () => {
|
|
181
|
+
await withNxRunManyFixture(async ({ root }) => {
|
|
182
|
+
await writeFile(join(root, 'packages/app/test-target.ts'), "await Bun.write('test-ran.txt', 'tested');\n");
|
|
183
|
+
await writeFile(join(root, 'packages/app/build-target.ts'), "throw new Error('build must not run here');\n");
|
|
184
|
+
|
|
185
|
+
await githubCiNxRunMany(root, {
|
|
186
|
+
targets: 'test',
|
|
187
|
+
projectsWithTargets: '*-macos,*-ios',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await expect(readFile(join(root, 'test-ran.txt'), 'utf8')).resolves.toBe('tested');
|
|
191
|
+
await expect(readFile(join(root, 'build-ran.txt'), 'utf8')).rejects.toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
158
195
|
it('applies an empty manifest after artifact transport omits its empty workspace directory', async () => {
|
|
159
196
|
await withOutputFixture(async ({ root, artifact }) => {
|
|
160
197
|
const manifest = await collectNxOutputs(root, artifact, [], SOURCE_SHA);
|
|
@@ -472,7 +509,19 @@ async function withNxRunManyFixture(
|
|
|
472
509
|
join(root, 'packages/app/package.json'),
|
|
473
510
|
JSON.stringify({
|
|
474
511
|
name: '@fixture/app',
|
|
475
|
-
nx: {
|
|
512
|
+
nx: {
|
|
513
|
+
name: 'app',
|
|
514
|
+
targets: {
|
|
515
|
+
'build-macos': {
|
|
516
|
+
executor: 'nx:run-commands',
|
|
517
|
+
options: { command: 'bun packages/app/build-target.ts', cwd: '.' },
|
|
518
|
+
},
|
|
519
|
+
test: {
|
|
520
|
+
executor: 'nx:run-commands',
|
|
521
|
+
options: { command: 'bun packages/app/test-target.ts', cwd: '.' },
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
},
|
|
476
525
|
}),
|
|
477
526
|
);
|
|
478
527
|
await $`git init --quiet`.cwd(root);
|
package/src/github-ci/index.ts
CHANGED
|
@@ -27,6 +27,9 @@ const parseNxProjectDeployConfigurations = typia.json.createIsParse<NxProjectDep
|
|
|
27
27
|
|
|
28
28
|
type NxSmartMode = 'auto' | 'affected' | 'run-many';
|
|
29
29
|
|
|
30
|
+
/** Nx workers = host cores (CI runners sized for full machine use). */
|
|
31
|
+
const NX_PARALLEL = '100%';
|
|
32
|
+
|
|
30
33
|
export async function cleanupGithubCiCache(root: string): Promise<void> {
|
|
31
34
|
const githubOutput = process.env.GITHUB_OUTPUT;
|
|
32
35
|
const markCacheReady = async (ready: boolean): Promise<void> => {
|
|
@@ -139,7 +142,7 @@ export function nxSmartArgs(target: string, mode: 'affected' | 'run-many', confi
|
|
|
139
142
|
if (configuration) {
|
|
140
143
|
args.push(`--configuration=${configuration}`);
|
|
141
144
|
}
|
|
142
|
-
args.push(`--exclude=tag:ci:skip:${target}`,
|
|
145
|
+
args.push(`--exclude=tag:ci:skip:${target}`, `--parallel=${NX_PARALLEL}`);
|
|
143
146
|
return args;
|
|
144
147
|
}
|
|
145
148
|
|
|
@@ -165,6 +168,7 @@ export interface NxRunManyOptions {
|
|
|
165
168
|
configuration?: string;
|
|
166
169
|
collectOutputs?: string;
|
|
167
170
|
allowEmptyProjects?: boolean;
|
|
171
|
+
projectsWithTargets?: string;
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
export interface ExpandedNxTargetRuns {
|
|
@@ -173,10 +177,18 @@ export interface ExpandedNxTargetRuns {
|
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
export function expandNxTargetRuns(projects: ProjectTargets[], options: NxRunManyOptions): ExpandedNxTargetRuns {
|
|
176
|
-
const
|
|
177
|
-
if (options.projects !== undefined &&
|
|
180
|
+
const namedProjects = selectProjects(projects, options.projects);
|
|
181
|
+
if (options.projects !== undefined && namedProjects.length === 0 && options.allowEmptyProjects !== true) {
|
|
178
182
|
throw new Error(`No Nx projects matched --projects ${options.projects}.`);
|
|
179
183
|
}
|
|
184
|
+
const selectedProjects = selectProjectsWithTargets(namedProjects, options.projectsWithTargets);
|
|
185
|
+
if (
|
|
186
|
+
options.projectsWithTargets !== undefined &&
|
|
187
|
+
selectedProjects.length === 0 &&
|
|
188
|
+
options.allowEmptyProjects !== true
|
|
189
|
+
) {
|
|
190
|
+
throw new Error(`No Nx projects own targets matching --projects-with-targets ${options.projectsWithTargets}.`);
|
|
191
|
+
}
|
|
180
192
|
const selectedTargetNames = [...new Set(selectedProjects.flatMap((project) => project.targets))].sort((a, b) =>
|
|
181
193
|
a.localeCompare(b),
|
|
182
194
|
);
|
|
@@ -192,6 +204,16 @@ export function expandNxTargetRuns(projects: ProjectTargets[], options: NxRunMan
|
|
|
192
204
|
unmatchedGlobs.push(targetPattern);
|
|
193
205
|
}
|
|
194
206
|
for (const target of targets) {
|
|
207
|
+
if (!isGlob && options.projectsWithTargets !== undefined) {
|
|
208
|
+
const missingProjects = selectedProjects
|
|
209
|
+
.filter((project) => !project.targets.includes(target))
|
|
210
|
+
.map((project) => project.project);
|
|
211
|
+
if (missingProjects.length > 0) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Nx target ${target} is missing for project(s) selected by --projects-with-targets ${options.projectsWithTargets}: ${missingProjects.join(', ')}.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
195
217
|
if (addedTargets.has(target)) {
|
|
196
218
|
continue;
|
|
197
219
|
}
|
|
@@ -262,7 +284,7 @@ export function nxRunManyArgs(run: NxTargetRun, configuration?: string): string[
|
|
|
262
284
|
if (configuration) {
|
|
263
285
|
nxArgs.push(`--configuration=${configuration}`);
|
|
264
286
|
}
|
|
265
|
-
nxArgs.push(
|
|
287
|
+
nxArgs.push(`--parallel=${NX_PARALLEL}`);
|
|
266
288
|
return nxArgs;
|
|
267
289
|
}
|
|
268
290
|
|
|
@@ -318,6 +340,16 @@ function selectProjects(projects: ProjectTargets[], selectors: string | undefine
|
|
|
318
340
|
.sort((left, right) => left.project.localeCompare(right.project));
|
|
319
341
|
}
|
|
320
342
|
|
|
343
|
+
function selectProjectsWithTargets(projects: ProjectTargets[], selectors: string | undefined): ProjectTargets[] {
|
|
344
|
+
if (selectors === undefined) {
|
|
345
|
+
return projects;
|
|
346
|
+
}
|
|
347
|
+
const patterns = commaSeparatedValues(selectors);
|
|
348
|
+
return projects.filter((project) =>
|
|
349
|
+
project.targets.some((target) => patterns.some((pattern) => new Bun.Glob(pattern).match(target))),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
321
353
|
function commaSeparatedValues(value: string): string[] {
|
|
322
354
|
return value
|
|
323
355
|
.split(',')
|
|
@@ -343,7 +375,7 @@ export async function githubCiNxDeploy(
|
|
|
343
375
|
const projectList = projects.join(',');
|
|
344
376
|
const targets = options.verify === true ? ['build', 'lint', 'test', 'deploy'] : ['deploy'];
|
|
345
377
|
for (const target of targets) {
|
|
346
|
-
const nxArgs = ['run-many', '-t', target, `--projects=${projectList}`,
|
|
378
|
+
const nxArgs = ['run-many', '-t', target, `--projects=${projectList}`, `--parallel=${NX_PARALLEL}`];
|
|
347
379
|
if (target === 'deploy') {
|
|
348
380
|
nxArgs.push(`--configuration=${options.configuration}`);
|
|
349
381
|
}
|
package/src/lib/json.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface PackagePublishConfig {
|
|
|
23
23
|
|
|
24
24
|
export interface PackageSmooGithub {
|
|
25
25
|
pushBranches?: string[];
|
|
26
|
+
/** GitHub Actions runs-on for managed CI (string or label list). Default: ubuntu-latest. */
|
|
27
|
+
runsOn?: string | string[];
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface PackageSmooConfig {
|
|
@@ -78,7 +80,7 @@ export type NxDependsOn = string | NxDependsOnObject;
|
|
|
78
80
|
/** Nx target options are executor-specific open bags. */
|
|
79
81
|
export type NxTargetOptions = Record<string, unknown>;
|
|
80
82
|
|
|
81
|
-
export type NxInput = string |
|
|
83
|
+
export type NxInput = string | object;
|
|
82
84
|
|
|
83
85
|
export interface NxTargetConfig {
|
|
84
86
|
executor?: string;
|
package/src/lib/workspace.ts
CHANGED
|
@@ -129,6 +129,7 @@ function listWorkspacePackageJsonPaths(root: string, workspacePatterns: string[]
|
|
|
129
129
|
const paths: string[] = [];
|
|
130
130
|
for (const pattern of workspacePatterns) {
|
|
131
131
|
if (!pattern.endsWith('/*')) {
|
|
132
|
+
paths.push(join(root, pattern, 'package.json'));
|
|
132
133
|
continue;
|
|
133
134
|
}
|
|
134
135
|
const parent = join(root, pattern.slice(0, -2));
|
|
@@ -5,9 +5,15 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { CiWorkflowStepKind, defineCiWorkflow, renderCiWorkflowYaml } from '../ci-workflow.js';
|
|
7
7
|
|
|
8
|
+
const nixosRunsOn = ['nixos-latest-x64', 'self-hosted'] as const;
|
|
9
|
+
|
|
8
10
|
describe('CI workflow definition', () => {
|
|
9
11
|
it('renders the checked-in local CI workflow copy', async () => {
|
|
10
|
-
const rendered = renderCiWorkflowYaml({
|
|
12
|
+
const rendered = renderCiWorkflowYaml({
|
|
13
|
+
deploy: false,
|
|
14
|
+
pushBranches: ['main'],
|
|
15
|
+
runsOn: [...nixosRunsOn],
|
|
16
|
+
});
|
|
11
17
|
const packageRoot = join(import.meta.dir, '..', '..', '..');
|
|
12
18
|
|
|
13
19
|
await expect(readFile(join(packageRoot, '..', '..', '.github/workflows/ci.yml'), 'utf8')).resolves.toBe(rendered);
|
|
@@ -25,6 +31,13 @@ describe('CI workflow definition', () => {
|
|
|
25
31
|
'smoo github-ci nx-deploy --configuration staging --mode affected --name "Deploy Staging" --step 11',
|
|
26
32
|
);
|
|
27
33
|
expect(rendered).toContain("# Step 12\n # Nx's database cache needs artifact files");
|
|
34
|
+
expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
|
|
35
|
+
expect(rendered).toContain('id: setup');
|
|
36
|
+
expect(rendered).not.toContain('Setup Nix/devenv (fork)');
|
|
37
|
+
expect(rendered).not.toContain('Setup Nix/devenv (nixos)');
|
|
38
|
+
expect(rendered).not.toContain('github-actions-bootstrap.sh');
|
|
39
|
+
expect(rendered).toContain('uses: ./.github/actions/save-nix-devenv');
|
|
40
|
+
expect(rendered).toContain('runs-on: ubuntu-latest');
|
|
28
41
|
});
|
|
29
42
|
|
|
30
43
|
it('adds Cloudflare credentials for Wrangler-backed deploys', () => {
|
|
@@ -44,4 +57,27 @@ describe('CI workflow definition', () => {
|
|
|
44
57
|
expect(restoreKey).toBe('${{ runner.os }}-${{ runner.arch }}-nx-db-v1-${{ github.sha }}');
|
|
45
58
|
expect(saveKey).toBe(restoreKey);
|
|
46
59
|
});
|
|
60
|
+
|
|
61
|
+
it('nixos config: fork PRs on ubuntu; one setup-devenv (composite owns host path)', () => {
|
|
62
|
+
const rendered = renderCiWorkflowYaml({
|
|
63
|
+
deploy: false,
|
|
64
|
+
pushBranches: ['main'],
|
|
65
|
+
runsOn: [...nixosRunsOn],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(rendered).toContain(
|
|
69
|
+
"runs-on:\n ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&\n fromJSON('[\"nixos-latest-x64\",\"self-hosted\"]') || 'ubuntu-latest' }}",
|
|
70
|
+
);
|
|
71
|
+
expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
|
|
72
|
+
expect(rendered).toContain('id: setup');
|
|
73
|
+
expect(rendered).not.toContain('Setup Nix/devenv (fork)');
|
|
74
|
+
expect(rendered).not.toContain('Setup Nix/devenv (nixos)');
|
|
75
|
+
expect(rendered).not.toContain('github-actions-bootstrap.sh');
|
|
76
|
+
expect(rendered).not.toContain('determinate-nix-action');
|
|
77
|
+
expect(rendered).toContain('if: always()');
|
|
78
|
+
expect(rendered).toContain('uses: ./.github/actions/save-nix-devenv');
|
|
79
|
+
// workflow yaml does not embed composite internals
|
|
80
|
+
expect(rendered).not.toContain('cache-node-modules');
|
|
81
|
+
expect(rendered).not.toContain('cache-ttsc-plugins');
|
|
82
|
+
});
|
|
47
83
|
});
|
|
@@ -19,11 +19,14 @@ import {
|
|
|
19
19
|
runPublishWorkflow,
|
|
20
20
|
} from '../publish-workflow.js';
|
|
21
21
|
|
|
22
|
+
const nixosRunsOn = ['nixos-latest-x64', 'self-hosted'] as const;
|
|
23
|
+
|
|
22
24
|
describe('publish workflow definition', () => {
|
|
23
25
|
it('renders the checked-in local publish workflow copy', async () => {
|
|
24
26
|
const rendered = renderPublishWorkflowYaml({
|
|
25
27
|
repoName: '@smoothbricks/codebase',
|
|
26
28
|
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
29
|
+
runsOn: [...nixosRunsOn],
|
|
27
30
|
});
|
|
28
31
|
const packageRoot = join(import.meta.dir, '..', '..', '..');
|
|
29
32
|
await expect(readFile(join(packageRoot, '..', '..', '.github/workflows/publish.yml'), 'utf8')).resolves.toBe(
|
|
@@ -35,6 +38,7 @@ describe('publish workflow definition', () => {
|
|
|
35
38
|
const rendered = renderPublishWorkflowYaml({
|
|
36
39
|
repoName: '@smoothbricks/codebase',
|
|
37
40
|
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
41
|
+
runsOn: [...nixosRunsOn],
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
await expect(
|
|
@@ -95,7 +99,7 @@ describe('publish workflow definition', () => {
|
|
|
95
99
|
|
|
96
100
|
expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length: 15 }, (_, index) => index + 1));
|
|
97
101
|
expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length: 19 }, (_, index) => index + 1));
|
|
98
|
-
expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length:
|
|
102
|
+
expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length: 8 }, (_, index) => index + 1));
|
|
99
103
|
expect(stepAnchorNumbers(finalJob)).toEqual(Array.from({ length: 14 }, (_, index) => index + 1));
|
|
100
104
|
expect(singleJob).toContain('# Step 14\n - name: ๐ฆ Publish release (${{ steps.version.outputs.mode }})');
|
|
101
105
|
expect(singleJob).toContain('# Step 15\n - name: ๐งน Cleanup and cache Nix/devenv');
|
|
@@ -105,7 +109,7 @@ describe('publish workflow definition', () => {
|
|
|
105
109
|
);
|
|
106
110
|
expect(linuxCandidate).toContain('# Step 19\n - name: ๐งน Cleanup and cache Nix/devenv');
|
|
107
111
|
expect(macosPlatform).toContain('# Step 5\n - name: ๐ Build selected macOS and iOS release outputs');
|
|
108
|
-
expect(macosPlatform).toContain('# Step
|
|
112
|
+
expect(macosPlatform).toContain('# Step 8\n - name: ๐งน Cleanup and cache Nix/devenv');
|
|
109
113
|
expect(finalJob).toContain('# Step 7\n - name: ๐งฏ Repair pending releases');
|
|
110
114
|
expect(finalJob).toContain('# Step 8\n - name: โป๏ธ Restore validated release state');
|
|
111
115
|
expect(finalJob).toContain('# Step 9\n - name: ๐ฆ Apply verified Linux outputs');
|
|
@@ -125,6 +129,7 @@ describe('publish workflow definition', () => {
|
|
|
125
129
|
const native = renderPublishWorkflowYaml({
|
|
126
130
|
repoName: '@smoothbricks/codebase',
|
|
127
131
|
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
132
|
+
runsOn: [...nixosRunsOn],
|
|
128
133
|
});
|
|
129
134
|
const linuxCandidate = native.slice(
|
|
130
135
|
native.indexOf(' linux-release-candidate:'),
|
|
@@ -137,8 +142,9 @@ describe('publish workflow definition', () => {
|
|
|
137
142
|
expect(linuxOnly).toContain(
|
|
138
143
|
`smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects`,
|
|
139
144
|
);
|
|
140
|
-
expect(native).toContain(' linux-release-candidate
|
|
145
|
+
expect(native).toContain(' linux-release-candidate:');
|
|
141
146
|
expect(native).toContain(' macos-platform:\n runs-on: macos-latest');
|
|
147
|
+
expect(native).toContain(' publish-on-linux:');
|
|
142
148
|
expect(native).toContain(' publish-on-linux:\n needs: [linux-release-candidate, macos-platform]');
|
|
143
149
|
expect(linuxCandidate).not.toContain('needs:');
|
|
144
150
|
expect(macosPlatform).not.toContain('needs:');
|
|
@@ -150,12 +156,20 @@ describe('publish workflow definition', () => {
|
|
|
150
156
|
expect(linuxCandidate).toContain(
|
|
151
157
|
`smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects`,
|
|
152
158
|
);
|
|
153
|
-
expect(macosPlatform).not.toContain('smoo github-ci nx-run-many');
|
|
154
159
|
expect(foldedRunCommand(macosPlatform, '๐ Build selected macOS and iOS release outputs')).toBe(
|
|
155
160
|
`smoo release build-platform-outputs --bump "\${{ inputs.bump }}" --ref "\${{ github.sha }}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(
|
|
156
161
|
',',
|
|
157
162
|
)}" --output "\${{ runner.temp }}/macos-platform-outputs"`,
|
|
158
163
|
);
|
|
164
|
+
expect(macosPlatform).toContain(
|
|
165
|
+
`run: smoo github-ci nx-run-many --targets test --projects-with-targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}"`,
|
|
166
|
+
);
|
|
167
|
+
expect(macosPlatform.indexOf('- name: ๐ Build selected macOS and iOS release outputs')).toBeLessThan(
|
|
168
|
+
macosPlatform.indexOf('- name: ๐งช Unit test macOS and iOS packages'),
|
|
169
|
+
);
|
|
170
|
+
expect(macosPlatform.indexOf('- name: ๐งช Unit test macOS and iOS packages')).toBeLessThan(
|
|
171
|
+
macosPlatform.indexOf('- name: ๐ค Upload macOS platform outputs'),
|
|
172
|
+
);
|
|
159
173
|
expect(finalJob).not.toContain('smoo release version');
|
|
160
174
|
expect(finalJob).not.toContain('nx-run-many');
|
|
161
175
|
expect(foldedRunCommand(finalJob, '๐งฏ Repair pending releases')).toBe(
|
|
@@ -612,3 +626,24 @@ function packageNameFromTag(tag: string): string {
|
|
|
612
626
|
}
|
|
613
627
|
return tag.slice(0, versionSeparator);
|
|
614
628
|
}
|
|
629
|
+
|
|
630
|
+
it('linux publish jobs use smoo.github.runsOn; macOS stays macos-latest', () => {
|
|
631
|
+
const rendered = renderPublishWorkflowYaml({
|
|
632
|
+
repoName: '@smoothbricks/codebase',
|
|
633
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
634
|
+
runsOn: [...nixosRunsOn],
|
|
635
|
+
});
|
|
636
|
+
expect(rendered).toContain(
|
|
637
|
+
`runs-on:
|
|
638
|
+
\${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
|
639
|
+
fromJSON('["nixos-latest-x64","self-hosted"]') || 'ubuntu-latest' }}`,
|
|
640
|
+
);
|
|
641
|
+
expect(rendered).toContain(' macos-platform:\n runs-on: macos-latest');
|
|
642
|
+
// Only the pre-publish linux job runs on self-hosted
|
|
643
|
+
expect(rendered.split('fromJSON(\'["nixos-latest-x64","self-hosted"]\')').length - 1).toBe(1);
|
|
644
|
+
expect(rendered).not.toContain(' linux-release-candidate:\n runs-on: ubuntu-latest');
|
|
645
|
+
// NPM publishing happens from GitHub runner
|
|
646
|
+
expect(rendered).toContain(
|
|
647
|
+
' publish-on-linux:\n needs: [linux-release-candidate, macos-platform]\n runs-on: ubuntu-latest',
|
|
648
|
+
);
|
|
649
|
+
});
|