@smoothbricks/cli 0.10.4 → 0.10.6
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 +5 -5
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +5 -3
- package/dist/lib/json.d.ts +5 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +18 -16
- 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 +3 -3
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +20 -8
- package/dist/monorepo/lockfile.d.ts +2 -1
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +1 -1
- package/dist/monorepo/managed-files.d.ts +30 -3
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +83 -63
- package/dist/monorepo/packs/index.js +2 -2
- 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/tool-validation.d.ts +2 -2
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +110 -16
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +6 -0
- package/managed/raw/envrc +5 -3
- package/managed/raw/patches/ttsc@0.19.3.patch +67 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +24 -7
- package/managed/raw/tooling/direnv/setup-environment.ts +23 -3
- 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 +2 -2
- package/src/cli.ts +6 -6
- package/src/github-ci/index.test.ts +3 -3
- package/src/github-ci/index.ts +6 -3
- package/src/lib/json.ts +5 -1
- package/src/monorepo/__tests__/ci-workflow.test.ts +37 -1
- package/src/monorepo/__tests__/publish-workflow.test.ts +28 -1
- package/src/monorepo/ci-workflow.ts +14 -4
- package/src/monorepo/github-runs-on.ts +45 -0
- package/src/monorepo/index.ts +20 -8
- package/src/monorepo/lockfile.ts +3 -2
- package/src/monorepo/managed-files.test.ts +49 -0
- package/src/monorepo/managed-files.ts +105 -64
- package/src/monorepo/packs/index.ts +2 -2
- package/src/monorepo/publish-workflow.ts +23 -15
- package/src/monorepo/tool-validation.test.ts +38 -16
- package/src/monorepo/tool-validation.ts +114 -16
- package/src/release/index.ts +6 -0
|
@@ -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.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@arethetypeswrong/core": "^0.18.2",
|
|
66
66
|
"@prettier/sync": "^0.6.1",
|
|
67
|
-
"@smoothbricks/nx-plugin": "0.3.
|
|
67
|
+
"@smoothbricks/nx-plugin": "0.3.4",
|
|
68
68
|
"@smoothbricks/validation": "0.1.4",
|
|
69
69
|
"commander": "^14.0.3",
|
|
70
70
|
"prettier": "^3.6.1",
|
package/src/cli.ts
CHANGED
|
@@ -59,18 +59,18 @@ function buildProgram(): Command {
|
|
|
59
59
|
);
|
|
60
60
|
monorepo.command('update').action(async () => {
|
|
61
61
|
const { updateManagedFiles } = await import('./monorepo/index.js');
|
|
62
|
-
updateManagedFiles(await findRepoRoot());
|
|
62
|
+
await updateManagedFiles(await findRepoRoot());
|
|
63
63
|
});
|
|
64
64
|
monorepo
|
|
65
65
|
.command('check')
|
|
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>')
|
|
@@ -84,12 +84,12 @@ function buildProgram(): Command {
|
|
|
84
84
|
.option('--stage', 'stage bun.lock when versions were resynced; quiet when clean')
|
|
85
85
|
.option(
|
|
86
86
|
'--mode <mode>',
|
|
87
|
-
'install: match package.json (CI); publish: map unpublished -next to last stable tag (pre-pack)',
|
|
88
|
-
'
|
|
87
|
+
'install: match package.json (default, CI); publish: map unpublished -next to last stable tag (pre-pack only)',
|
|
88
|
+
'install',
|
|
89
89
|
)
|
|
90
90
|
.action(async (options: { stage?: boolean; mode?: 'install' | 'publish' }) => {
|
|
91
91
|
const { syncBunLockfileVersions } = await import('./monorepo/index.js');
|
|
92
|
-
const mode = options.mode === '
|
|
92
|
+
const mode = options.mode === 'publish' ? 'publish' : 'install';
|
|
93
93
|
syncBunLockfileVersions(await findRepoRoot(), {
|
|
94
94
|
mode,
|
|
95
95
|
...(options.stage ? { log: false, stage: true } : {}),
|
|
@@ -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
|
|
|
@@ -65,7 +65,7 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
65
65
|
|
|
66
66
|
expect(expanded.unmatchedGlobs).toEqual([]);
|
|
67
67
|
expect(expanded.runs.map((run) => nxRunManyArgs(run))).toEqual([
|
|
68
|
-
['run-many', '-t', 'missing', '--projects=api', '--parallel=
|
|
68
|
+
['run-many', '-t', 'missing', '--projects=api', '--parallel=100%'],
|
|
69
69
|
]);
|
|
70
70
|
});
|
|
71
71
|
|
|
@@ -114,7 +114,7 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
114
114
|
'-t',
|
|
115
115
|
'test',
|
|
116
116
|
'--exclude=tag:ci:skip:test',
|
|
117
|
-
'--parallel=
|
|
117
|
+
'--parallel=100%',
|
|
118
118
|
]);
|
|
119
119
|
expect(nxRunManyArgs({ target: 'test', projects: projects.slice(0, 1) })).not.toContain(
|
|
120
120
|
'--exclude=tag:ci:skip:test',
|
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
|
|
|
@@ -262,7 +265,7 @@ export function nxRunManyArgs(run: NxTargetRun, configuration?: string): string[
|
|
|
262
265
|
if (configuration) {
|
|
263
266
|
nxArgs.push(`--configuration=${configuration}`);
|
|
264
267
|
}
|
|
265
|
-
nxArgs.push(
|
|
268
|
+
nxArgs.push(`--parallel=${NX_PARALLEL}`);
|
|
266
269
|
return nxArgs;
|
|
267
270
|
}
|
|
268
271
|
|
|
@@ -343,7 +346,7 @@ export async function githubCiNxDeploy(
|
|
|
343
346
|
const projectList = projects.join(',');
|
|
344
347
|
const targets = options.verify === true ? ['build', 'lint', 'test', 'deploy'] : ['deploy'];
|
|
345
348
|
for (const target of targets) {
|
|
346
|
-
const nxArgs = ['run-many', '-t', target, `--projects=${projectList}`,
|
|
349
|
+
const nxArgs = ['run-many', '-t', target, `--projects=${projectList}`, `--parallel=${NX_PARALLEL}`];
|
|
347
350
|
if (target === 'deploy') {
|
|
348
351
|
nxArgs.push(`--configuration=${options.configuration}`);
|
|
349
352
|
}
|
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 {
|
|
@@ -52,6 +54,8 @@ export interface PackageJson {
|
|
|
52
54
|
devDependencies?: StringMap;
|
|
53
55
|
peerDependencies?: StringMap;
|
|
54
56
|
optionalDependencies?: StringMap;
|
|
57
|
+
/** Bun patchedDependencies map (package@version → patch path). */
|
|
58
|
+
patchedDependencies?: StringMap;
|
|
55
59
|
engines?: StringMap;
|
|
56
60
|
scripts?: StringMap;
|
|
57
61
|
publishConfig?: PackagePublishConfig;
|
|
@@ -165,7 +169,7 @@ export function ensureScripts(pkg: PackageJson): StringMap {
|
|
|
165
169
|
/** Ensure package.json.dependencies (or other string-map dep field) exists. */
|
|
166
170
|
export function ensureDependencyMap(
|
|
167
171
|
pkg: PackageJson,
|
|
168
|
-
key: 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies',
|
|
172
|
+
key: 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' | 'patchedDependencies',
|
|
169
173
|
): StringMap {
|
|
170
174
|
const current = pkg[key];
|
|
171
175
|
if (isStringMap(current)) {
|
|
@@ -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(
|
|
@@ -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:');
|
|
@@ -612,3 +618,24 @@ function packageNameFromTag(tag: string): string {
|
|
|
612
618
|
}
|
|
613
619
|
return tag.slice(0, versionSeparator);
|
|
614
620
|
}
|
|
621
|
+
|
|
622
|
+
it('linux publish jobs use smoo.github.runsOn; macOS stays macos-latest', () => {
|
|
623
|
+
const rendered = renderPublishWorkflowYaml({
|
|
624
|
+
repoName: '@smoothbricks/codebase',
|
|
625
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
626
|
+
runsOn: [...nixosRunsOn],
|
|
627
|
+
});
|
|
628
|
+
expect(rendered).toContain(
|
|
629
|
+
`runs-on:
|
|
630
|
+
\${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
|
631
|
+
fromJSON('["nixos-latest-x64","self-hosted"]') || 'ubuntu-latest' }}`,
|
|
632
|
+
);
|
|
633
|
+
expect(rendered).toContain(' macos-platform:\n runs-on: macos-latest');
|
|
634
|
+
// Only the pre-publish linux job runs on self-hosted
|
|
635
|
+
expect(rendered.split('fromJSON(\'["nixos-latest-x64","self-hosted"]\')').length - 1).toBe(1);
|
|
636
|
+
expect(rendered).not.toContain(' linux-release-candidate:\n runs-on: ubuntu-latest');
|
|
637
|
+
// NPM publishing happens from GitHub runner
|
|
638
|
+
expect(rendered).toContain(
|
|
639
|
+
' publish-on-linux:\n needs: [linux-release-candidate, macos-platform]\n runs-on: ubuntu-latest',
|
|
640
|
+
);
|
|
641
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
|
|
2
2
|
|
|
3
|
+
import { renderRunsOnLine } from './github-runs-on.js';
|
|
4
|
+
|
|
3
5
|
export enum CiWorkflowStepKind {
|
|
4
6
|
Checkout = 'checkout',
|
|
5
7
|
SetupDevenv = 'setup-devenv',
|
|
@@ -26,6 +28,8 @@ export interface CiWorkflowDefinitionOptions {
|
|
|
26
28
|
deploy: boolean;
|
|
27
29
|
deployProvider?: 'cloudflare';
|
|
28
30
|
pushBranches: string[];
|
|
31
|
+
/** Default ubuntu-latest when omitted. */
|
|
32
|
+
runsOn?: string | string[];
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
type CiWorkflowStepInput = Omit<CiWorkflowStep, 'number'>;
|
|
@@ -80,7 +84,7 @@ defaults:
|
|
|
80
84
|
jobs:
|
|
81
85
|
main:
|
|
82
86
|
name: Validate
|
|
83
|
-
|
|
87
|
+
${renderRunsOnLine(options.runsOn)}
|
|
84
88
|
timeout-minutes: 45
|
|
85
89
|
env:
|
|
86
90
|
NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
|
|
@@ -150,7 +154,10 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
150
154
|
' fetch-depth: 0',
|
|
151
155
|
];
|
|
152
156
|
case CiWorkflowStepKind.SetupDevenv:
|
|
157
|
+
// One step everywhere. setup-devenv detects host-nix (GARM) vs ephemeral
|
|
158
|
+
// and skips GH install/cache steps internally — same for ci/publish/managed.
|
|
153
159
|
return [` - name: ${step.name}`, ' id: setup', ' uses: ./.github/actions/setup-devenv'];
|
|
160
|
+
|
|
154
161
|
case CiWorkflowStepKind.SetNxShas:
|
|
155
162
|
return [
|
|
156
163
|
` - name: ${step.name}`,
|
|
@@ -184,7 +191,7 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
184
191
|
// recursion guard, so the default token suffices.
|
|
185
192
|
return [
|
|
186
193
|
` - name: ${step.name}`,
|
|
187
|
-
" if:
|
|
194
|
+
" if: steps.managed-drift.outputs.drifted != '' && steps.managed-drift.outputs.drifted != '0'",
|
|
188
195
|
' run: gh workflow run managed-files.yml --ref "$GITHUB_REF_NAME"',
|
|
189
196
|
];
|
|
190
197
|
case CiWorkflowStepKind.Deploy:
|
|
@@ -212,7 +219,8 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
212
219
|
case CiWorkflowStepKind.UploadTraceDbs:
|
|
213
220
|
return [
|
|
214
221
|
` - name: ${step.name}`,
|
|
215
|
-
'
|
|
222
|
+
' # success() is the default; always() keeps traces after a red build/test.',
|
|
223
|
+
' if: always()',
|
|
216
224
|
' uses: actions/upload-artifact@v7.0.1',
|
|
217
225
|
' with:',
|
|
218
226
|
' name: trace-results-${{ github.run_id }}',
|
|
@@ -224,7 +232,9 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
224
232
|
case CiWorkflowStepKind.SaveNixDevenv:
|
|
225
233
|
return [
|
|
226
234
|
` - name: ${step.name}`,
|
|
227
|
-
'
|
|
235
|
+
' # always() still saves after a red job. Nix NAR is ephemeral-only;',
|
|
236
|
+
' # devenv eval-cache also saves on host-nix when setup missed.',
|
|
237
|
+
' if: always()',
|
|
228
238
|
' uses: ./.github/actions/save-nix-devenv',
|
|
229
239
|
' with:',
|
|
230
240
|
' nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Shared `runs-on:` emission for generated GitHub workflow YAML. */
|
|
2
|
+
|
|
3
|
+
export type WorkflowRunsOn = string | string[] | undefined;
|
|
4
|
+
|
|
5
|
+
export function isNixosRunner(runsOn: WorkflowRunsOn): boolean {
|
|
6
|
+
const labels = runsOn === undefined ? [] : typeof runsOn === 'string' ? [runsOn] : runsOn;
|
|
7
|
+
return labels.some((label) => label === 'nixos' || label.startsWith('nixos-'));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Trusted jobs use configured nixos self-hosted labels; fork PRs stay on
|
|
12
|
+
* ubuntu-latest (no access to the private runner fleet).
|
|
13
|
+
*/
|
|
14
|
+
export function githubUsesNixosRunnerExpr(): string {
|
|
15
|
+
return "(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Full ` runs-on: …` line(s), indented for a job under `jobs.<id>:`.
|
|
20
|
+
* Nixos labels emit the multiline prettier-stable fork-gate expression.
|
|
21
|
+
*/
|
|
22
|
+
export function renderRunsOnLine(runsOn: WorkflowRunsOn): string {
|
|
23
|
+
if (!isNixosRunner(runsOn)) {
|
|
24
|
+
let value: string;
|
|
25
|
+
if (runsOn === undefined) {
|
|
26
|
+
value = 'ubuntu-latest';
|
|
27
|
+
} else if (typeof runsOn === 'string') {
|
|
28
|
+
value = runsOn.length > 0 ? runsOn : 'ubuntu-latest';
|
|
29
|
+
} else if (runsOn.length === 0) {
|
|
30
|
+
value = 'ubuntu-latest';
|
|
31
|
+
} else if (runsOn.length === 1) {
|
|
32
|
+
value = runsOn[0] ?? 'ubuntu-latest';
|
|
33
|
+
} else {
|
|
34
|
+
value = `[${runsOn.map((label) => `'${label}'`).join(', ')}]`;
|
|
35
|
+
}
|
|
36
|
+
return ` runs-on: ${value}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const labels = typeof runsOn === 'string' ? [runsOn] : runsOn;
|
|
40
|
+
// Multiline matches prettier so checked-in workflows stay byte-identical.
|
|
41
|
+
const nixosJson = JSON.stringify(labels);
|
|
42
|
+
return ` runs-on:
|
|
43
|
+
\${{ ${githubUsesNixosRunnerExpr()} &&
|
|
44
|
+
fromJSON('${nixosJson}') || 'ubuntu-latest' }}`;
|
|
45
|
+
}
|
package/src/monorepo/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { applyManagedFiles, printResults, warnOnManagedFileDrift } from './manag
|
|
|
9
9
|
import { listValidCommitScopes, validatePublicTags } from './package-policy.js';
|
|
10
10
|
import { runInitPacks, runValidatePacks } from './packs/index.js';
|
|
11
11
|
import { syncRootRuntimeVersions } from './runtime.js';
|
|
12
|
+
import { applyToolConfigDefaults } from './tool-validation.js';
|
|
12
13
|
|
|
13
14
|
export interface InitOptions {
|
|
14
15
|
runtimeOnly?: boolean;
|
|
@@ -50,7 +51,7 @@ export async function initMonorepo(root: string, options: InitOptions): Promise<
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
printResults(applyManagedFiles(root, 'update'));
|
|
54
|
+
printResults(await applyManagedFiles(root, 'update'));
|
|
54
55
|
await runInitPacks({ root, syncRuntime: process.env.DEVENV_ROOT !== undefined || options.syncRuntime === true });
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -74,24 +75,35 @@ export async function validateMonorepo(root: string, options: ValidateOptions =
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export function updateManagedFiles(root: string): void {
|
|
78
|
-
printResults(applyManagedFiles(root, 'update'));
|
|
78
|
+
export async function updateManagedFiles(root: string): Promise<void> {
|
|
79
|
+
printResults(await applyManagedFiles(root, 'update'));
|
|
80
|
+
// Tool dependency policy (typescript API 6, @typescript/native for ttsc, nx, …)
|
|
81
|
+
// lives next to managed templates — update must install them, not only rewrite files.
|
|
82
|
+
await applyToolConfigDefaults(root);
|
|
83
|
+
syncBunLockfileVersions(root, { mode: 'install' });
|
|
84
|
+
console.log('installing workspace dependencies (bun install)');
|
|
85
|
+
const install = await $`bun install --no-summary`.cwd(root).nothrow();
|
|
86
|
+
if (install.exitCode !== 0) {
|
|
87
|
+
const stderr = decode(install.stderr).trim();
|
|
88
|
+
const stdout = decode(install.stdout).trim();
|
|
89
|
+
throw new Error(`bun install failed after monorepo update${stderr || stdout ? `:\n${stderr || stdout}` : ''}`);
|
|
90
|
+
}
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
export function checkManagedFiles(root: string, options: { warn?: boolean } = {}): void {
|
|
93
|
+
export async function checkManagedFiles(root: string, options: { warn?: boolean } = {}): Promise<void> {
|
|
82
94
|
if (options.warn === true) {
|
|
83
|
-
warnOnManagedFileDrift(root);
|
|
95
|
+
await warnOnManagedFileDrift(root);
|
|
84
96
|
return;
|
|
85
97
|
}
|
|
86
|
-
const results = applyManagedFiles(root, 'check');
|
|
98
|
+
const results = await applyManagedFiles(root, 'check');
|
|
87
99
|
printResults(results);
|
|
88
100
|
if (results.some((result) => result.action === 'drifted')) {
|
|
89
101
|
throw new Error('Managed monorepo files are out of date. Run: smoo monorepo update');
|
|
90
102
|
}
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
export function diffManagedFiles(root: string): void {
|
|
94
|
-
printResults(applyManagedFiles(root, 'diff'));
|
|
105
|
+
export async function diffManagedFiles(root: string): Promise<void> {
|
|
106
|
+
printResults(await applyManagedFiles(root, 'diff'));
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
export function validateCommitMessageFile(
|