@sidebase/base-config 0.2.0 → 0.2.2

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/docs/migration.md CHANGED
@@ -334,10 +334,10 @@ the managed file and is reverted on the next sync.
334
334
  warns you. `streamctl check` stays green, the file looks deliberate, and a package whose
335
335
  install scripts are blocked still installs.
336
336
 
337
- Be precise about the consequence, because it is narrower than it first appears. The usual
338
- suspects (`sharp`, `@tailwindcss/oxide`, `unrs-resolver`, `@parcel/watcher`) ship their
339
- native binary as a prebuilt optional dependency, so on a platform with a prebuild they keep
340
- working whether or not they are approved, cold install included. Verified across pnpm
337
+ The consequence is narrower than it first appears. The usual suspects (`sharp`,
338
+ `@tailwindcss/oxide`, `unrs-resolver`, `@parcel/watcher`) ship their native binary as a
339
+ prebuilt optional dependency, so on a platform with a prebuild they keep working whether or
340
+ not they are approved, cold install included. Verified across pnpm
341
341
  10.28.1, 10.29.1 and 10.29.3. Where the loss is real:
342
342
 
343
343
  - architectures with no prebuild, where the binary genuinely has to be compiled
@@ -546,13 +546,12 @@ too, so on first sync the payload appends its block to whatever your repository
546
546
  Your existing lines stay above it. Nothing is overwritten and nothing is reported.
547
547
 
548
548
  Most of the leftovers are harmless. Ignore patterns are additive, so a duplicated `node_modules`
549
- or `.env` above the block is genuinely inert -- it changes nothing about what the build context
550
- carries. Delete them anyway, for a reason that is not tidiness: the detector below is the only
551
- thing that will ever tell you a payload entry has started shadowing something of yours, and it
552
- is not a one-shot check. A future payload version can add an entry that collides with a line you
553
- kept above the block, and a file with leftover duplicates reads `AFFECTED` forever, so it can no
554
- longer distinguish that from the noise. Clearing them is what keeps the check able to answer next
555
- time.
549
+ or `.env` above the block is inert; it changes nothing about what the build context carries.
550
+ Delete them anyway, and not for tidiness. The detector below is the only thing that will ever
551
+ tell you a payload entry has started shadowing something of yours, and it is not a one-shot
552
+ check. A future payload version can add an entry that collides with a line you kept above the
553
+ block. A file with leftover duplicates reads `AFFECTED` forever, so it can no longer distinguish
554
+ that from the noise. Clearing them keeps the check able to answer next time.
556
555
 
557
556
  **The case that bites is a negation.** `.dockerignore` is last-match-wins, and the managed block
558
557
  is appended at the BOTTOM, so every payload entry beats anything you wrote above it. A repository
@@ -822,5 +821,5 @@ keeps its own command this way.
822
821
 
823
822
  Nothing here is destructive if you work on a branch. `sync` rewrites tracked files, so
824
823
  `git diff` shows everything it did and `git restore` undoes it. Keep the migration on its
825
- own branch and its own commit so that if the sync diff turns out to be larger than
826
- expected, reverting it is one operation rather than an archaeology exercise.
824
+ own branch and its own commit, so that if the sync diff turns out to be larger than
825
+ expected, reverting it is one operation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sidebase/base-config",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Shared @sidebase base configuration for Nuxt repos: ESLint / Prisma / tsconfig factories on npm, plus the streamctl file-sync preset payload",
5
5
  "keywords": [
6
6
  "sidebase",
@@ -10,6 +10,15 @@ follow. They are the same across every repo in the fleet.
10
10
  - Never commit secrets, credentials, or generated build artifacts.
11
11
  - Keep all code, comments and identifiers in English.
12
12
 
13
+ ## Comments & wording
14
+
15
+ - Keep comments short. Say WHY the code is the way it is, not what it plainly does.
16
+ - ASCII only: no em dash, en dash, arrow, ellipsis, curly quote, or emoji.
17
+ - One idea per sentence. Cut filler openers ("It is worth noting") and inflated words
18
+ ("leverage", "utilize", "robust", "seamless").
19
+ - Do not leave a comment describing an alternative you rejected, or internal ticket
20
+ shorthand a reader cannot resolve.
21
+
13
22
  ## Types & correctness
14
23
 
15
24
  - Use precise types; avoid `any` and unchecked casts. Parse and validate external input at the boundary instead of trusting it downstream.
@@ -12,16 +12,15 @@ tests
12
12
  .env
13
13
  .env.*
14
14
  !.env.example
15
- # The build-stage COPY globs `.npmrc*`; keep token-bearing backups
16
- # (`.npmrc.local`, `.npmrc.bak`, ...) out of the build context. `.npmrc` itself
17
- # is intentionally allowed through.
15
+ # The build stage globs `.npmrc*`, so keep token-bearing backups like
16
+ # `.npmrc.local` out of the build context. `.npmrc` itself is allowed through.
18
17
  .npmrc.*
19
18
  .DS_Store
20
- # Local dev state written by `pnpm db` / `docker compose up`. `prisma/pglite-data`
21
- # is tens of MB; `prisma/local.postgres_data` is root-owned, which fails a rootless
22
- # `docker build` outright. Patterns are anchored to the context root here (unlike
23
- # .gitignore), so `**/local.*` is required to reach the nested paths.
19
+ # Local dev state from `pnpm db` or `docker compose up`. `prisma/pglite-data` is
20
+ # tens of MB, and root-owned `prisma/local.postgres_data` fails a rootless build.
21
+ # Root-anchored so `src/path/local.ts` still reaches the build context.
24
22
  prisma/client
25
23
  prisma/pglite-data
26
24
  pgliteHealthz
27
- **/local.*
25
+ local.*
26
+ prisma/local.*
@@ -1,49 +1,30 @@
1
- # Managed by streamctl (full-own, render: automation-upgrade). OFF BY DEFAULT: this
2
- # file only lands when a repo sets `automation: { upgradePr: true }` in
3
- # streamctl.config.ts. See the payload README "Upgrade-PR workflow" for enabling.
4
- #
5
- # TOKEN: the PR is opened with `secrets.STREAMCTL_PR_TOKEN` (a GitHub App
6
- # installation token or a machine-account PAT), NOT the default `GITHUB_TOKEN`.
7
- # A PR created with `GITHUB_TOKEN` does NOT trigger `pull_request` workflows, so
8
- # this repo's own `check` gate would never run on the bot's PR. The bot identity
9
- # for the fleet is still being decided; treat this file as inert and provide the
10
- # secret before enabling. No registry token is needed: the payload is public npm.
11
- # A repo with OTHER private dependencies must opt this file out
12
- # (`files: { ".github/workflows/streamctl-upgrade.yml": "off" }`) and wire its own auth.
13
- #
14
- # Actions are pinned by full commit SHA (this workflow handles a token); the version
15
- # tag is in the trailing comment.
1
+ # Managed by streamctl. Enabled by `automation.upgradePr`.
2
+ # Repos with private dependencies should own this workflow.
3
+ # Workflow updates need the configured token with Contents, Pull requests, and Workflows write.
16
4
  name: streamctl upgrade
17
5
 
18
6
  on:
19
7
  schedule:
20
- - cron: "0 6 * * 1" # Mondays 06:00 UTC
8
+ - cron: "0 6 * * 1"
21
9
  workflow_dispatch:
22
10
 
23
- # Least privilege: every PR/issue write below uses STREAMCTL_PR_TOKEN, so the
24
- # default token only needs to read the repo for checkout.
25
11
  permissions:
26
- contents: read
12
+ contents: write
13
+ issues: write
14
+ pull-requests: write
27
15
 
28
16
  env:
17
+ ALLOW_WORKFLOW_UPDATES: "${ALLOW_WORKFLOW_UPDATES}"
29
18
  NODE_VERSION: "${NODE_VERSION}"
30
19
 
31
20
  jobs:
32
21
  upgrade:
33
22
  runs-on: ubuntu-latest
34
23
  steps:
35
- # A bot identity is REQUIRED for the PR (see the header). To mint an App token
36
- # in-workflow instead of storing a PAT, uncomment and wire the App credentials,
37
- # then use `${{ steps.bot.outputs.token }}` in place of STREAMCTL_PR_TOKEN below:
38
- #
39
- # - id: bot
40
- # uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
41
- # with:
42
- # app-id: ${{ secrets.STREAMCTL_APP_ID }}
43
- # private-key: ${{ secrets.STREAMCTL_APP_PRIVATE_KEY }}
44
-
45
24
  - name: Checkout
46
25
  uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
26
+ with:
27
+ persist-credentials: false
47
28
 
48
29
  - name: Set up pnpm
49
30
  uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
@@ -59,72 +40,362 @@ jobs:
59
40
  - name: Install dependencies
60
41
  run: pnpm install --frozen-lockfile
61
42
 
62
- # Exit codes drive the branch: 4 = an update is available (proceed); 0 = up to
63
- # date (stop, no PR); 3 = the repo already has drift (open an issue, not a PR).
43
+ - name: Capture managed-file state
44
+ run: pnpm streamctl status --json > "$RUNNER_TEMP/streamctl-status.json"
45
+
64
46
  - id: check
65
47
  name: Check for a payload update
66
48
  run: |
67
49
  set +e
68
- pnpm streamctl check --fail-on outdated --json > streamctl-check.json
69
- echo "code=$?" >> "$GITHUB_OUTPUT"
50
+ pnpm streamctl check --fail-on any --json > "$RUNNER_TEMP/streamctl-check.json"
51
+ code=$?
70
52
  set -e
71
- cat streamctl-check.json
53
+ echo "code=$code" >> "$GITHUB_OUTPUT"
54
+ echo "target=$(jq -r '.error.details.updateAvailable.latest // empty' "$RUNNER_TEMP/streamctl-check.json")" >> "$GITHUB_OUTPUT"
55
+ cat "$RUNNER_TEMP/streamctl-check.json"
56
+
57
+ - name: Fail on an unexpected check result
58
+ if: ${{ steps.check.outputs.code != '0' && steps.check.outputs.code != '3' && steps.check.outputs.code != '4' }}
59
+ run: exit 1
72
60
 
73
- - name: Open a drift issue (repo already diverged)
61
+ - name: Require an update target
62
+ if: ${{ steps.check.outputs.code == '4' }}
63
+ env:
64
+ TARGET: ${{ steps.check.outputs.target }}
65
+ run: test -n "$TARGET"
66
+
67
+ - name: Ensure workflow labels
68
+ if: ${{ steps.check.outputs.code == '3' || steps.check.outputs.code == '4' }}
69
+ env:
70
+ GH_TOKEN: ${{ github.token }}
71
+ run: |
72
+ gh label create streamctl --color 0E8A16 --description "Managed by streamctl" --force
73
+ gh label create needs-interactive-upgrade --color D93F0B --description "Manual upgrade work required" --force
74
+
75
+ - name: Require the PR token
76
+ if: ${{ steps.check.outputs.code == '4' && env.ALLOW_WORKFLOW_UPDATES == 'true' }}
77
+ env:
78
+ PR_TOKEN: ${{ secrets.${PR_TOKEN_SECRET} }}
79
+ run: test -n "$PR_TOKEN"
80
+
81
+ - name: Open or update the drift issue
74
82
  if: ${{ steps.check.outputs.code == '3' }}
75
83
  env:
76
- GH_TOKEN: ${{ secrets.STREAMCTL_PR_TOKEN }}
84
+ GH_TOKEN: ${{ github.token }}
77
85
  run: |
78
- gh issue create \
79
- --title "streamctl: managed files have drifted" \
80
- --label streamctl \
81
- --body "\`streamctl check\` reports pre-existing drift, so the automated upgrade was skipped. Run \`pnpm streamctl sync\` locally, commit the result, then re-run this workflow."
86
+ title="streamctl: managed files have drifted"
87
+ body="The automated upgrade was skipped because managed files already differ. Run \`pnpm streamctl sync\` locally, commit the result, then rerun this workflow. Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
88
+ issue_number=$(gh issue list --state open --label streamctl --search "\"$title\" in:title" --json number --jq '.[0].number // empty')
89
+ if [[ -n "$issue_number" ]]; then
90
+ gh issue edit "$issue_number" --body "$body"
91
+ else
92
+ gh issue create --title "$title" --label streamctl --body "$body"
93
+ fi
82
94
 
83
- # Only upgrade when an update is actually available (check exit 4).
84
95
  - id: upgrade
85
96
  name: Upgrade the payload
86
97
  if: ${{ steps.check.outputs.code == '4' }}
98
+ env:
99
+ CI: "false"
100
+ TARGET: ${{ steps.check.outputs.target }}
87
101
  run: |
88
102
  set +e
89
- pnpm streamctl upgrade --json > streamctl-upgrade.json
90
- echo "code=$?" >> "$GITHUB_OUTPUT"
103
+ pnpm streamctl upgrade --to "$TARGET" --yes --json > "$RUNNER_TEMP/streamctl-upgrade.raw"
104
+ code=$?
91
105
  set -e
92
- cat streamctl-upgrade.json
106
+ tail -n 1 "$RUNNER_TEMP/streamctl-upgrade.raw" > "$RUNNER_TEMP/streamctl-upgrade.json"
107
+ echo "code=$code" >> "$GITHUB_OUTPUT"
108
+ cat "$RUNNER_TEMP/streamctl-upgrade.raw"
109
+
110
+ - name: Fail on an unexpected upgrade result
111
+ if: ${{ steps.upgrade.outputs.code != '' && steps.upgrade.outputs.code != '0' && steps.upgrade.outputs.code != '2' }}
112
+ run: exit 1
93
113
 
94
- # exit 2 = the chained sync hit conflicts and ROLLED BACK. Re-run bumping only the
95
- # pin + devDependency (no sync) so the PR carries a reviewable diff a human then
96
- # reconciles with `streamctl sync --interactive`.
97
- - name: Prepare a plan-only bump (interactive sync required)
114
+ - id: conflicts
115
+ name: Classify upgrade conflicts
98
116
  if: ${{ steps.upgrade.outputs.code == '2' }}
99
- run: pnpm streamctl upgrade --no-install --json > streamctl-upgrade.json
117
+ run: |
118
+ jq -r --slurpfile status "$RUNNER_TEMP/streamctl-status.json" '
119
+ (.error.details.conflicted // [])[]
120
+ | select((
121
+ (.kind == "edit" or .kind == "adoption")
122
+ and (.path as $path | any($status[0].data.files[];
123
+ .path == $path and .strategy == "full" and .state == "in-sync"
124
+ ))
125
+ ) | not)
126
+ | .path
127
+ ' "$RUNNER_TEMP/streamctl-upgrade.json" | sort -u > "$RUNNER_TEMP/streamctl-unsafe.txt"
128
+ if [[ ! -s "$RUNNER_TEMP/streamctl-unsafe.txt" ]] && jq -e '
129
+ .error.code == "CONFLICTS_PENDING"
130
+ and .error.details.rolledBack == true
131
+ and (.error.details.conflicted | length > 0)
132
+ ' "$RUNNER_TEMP/streamctl-upgrade.json" > /dev/null; then
133
+ echo "safe=true" >> "$GITHUB_OUTPUT"
134
+ else
135
+ echo "safe=false" >> "$GITHUB_OUTPUT"
136
+ fi
100
137
 
101
- - name: Open the upgrade PR (clean reconcile)
102
- if: ${{ steps.upgrade.outputs.code == '0' }}
138
+ - id: force
139
+ name: Apply safe generated replacements
140
+ if: ${{ steps.conflicts.outputs.safe == 'true' }}
141
+ env:
142
+ CI: "false"
143
+ TARGET: ${{ steps.check.outputs.target }}
144
+ run: |
145
+ set +e
146
+ pnpm streamctl upgrade --to "$TARGET" --force --json > "$RUNNER_TEMP/streamctl-force.raw"
147
+ code=$?
148
+ set -e
149
+ tail -n 1 "$RUNNER_TEMP/streamctl-force.raw" > "$RUNNER_TEMP/streamctl-force.json"
150
+ echo "code=$code" >> "$GITHUB_OUTPUT"
151
+ cat "$RUNNER_TEMP/streamctl-force.raw"
152
+
153
+ - name: Fail when safe replacement fails
154
+ if: ${{ steps.conflicts.outputs.safe == 'true' && steps.force.outputs.code != '0' }}
155
+ run: exit 1
156
+
157
+ - id: outcome
158
+ name: Record the upgrade outcome
159
+ if: ${{ steps.check.outputs.code == '4' }}
160
+ env:
161
+ FORCE_CODE: ${{ steps.force.outputs.code }}
162
+ UPGRADE_CODE: ${{ steps.upgrade.outputs.code }}
163
+ run: |
164
+ if [[ "$UPGRADE_CODE" == "0" || "$FORCE_CODE" == "0" ]]; then
165
+ echo "completed=true" >> "$GITHUB_OUTPUT"
166
+ else
167
+ echo "completed=false" >> "$GITHUB_OUTPUT"
168
+ fi
169
+
170
+ - id: workflow_changes
171
+ name: Exclude workflow changes by default
172
+ if: ${{ steps.outcome.outputs.completed == 'true' }}
173
+ run: |
174
+ jq -r '
175
+ (.data.plan // .error.details.plan // [])[]
176
+ | .path
177
+ | select(startswith(".github/workflows/"))
178
+ ' "$RUNNER_TEMP/streamctl-upgrade.json" | sort -u > "$RUNNER_TEMP/streamctl-workflows.txt"
179
+ echo "blocked=false" >> "$GITHUB_OUTPUT"
180
+ if [[ "$ALLOW_WORKFLOW_UPDATES" == "true" || ! -s "$RUNNER_TEMP/streamctl-workflows.txt" ]]; then
181
+ exit 0
182
+ fi
183
+ while IFS= read -r path; do
184
+ if git cat-file -e "HEAD:$path" 2>/dev/null; then
185
+ git restore --source=HEAD --worktree -- "$path"
186
+ else
187
+ rm -f -- "$path"
188
+ fi
189
+ done < "$RUNNER_TEMP/streamctl-workflows.txt"
190
+ echo "blocked=true" >> "$GITHUB_OUTPUT"
191
+
192
+ - id: manual
193
+ name: Prepare the manual upgrade
194
+ if: ${{ steps.upgrade.outputs.code == '2' && steps.conflicts.outputs.safe == 'false' }}
195
+ env:
196
+ CI: "false"
197
+ TARGET: ${{ steps.check.outputs.target }}
198
+ run: |
199
+ set +e
200
+ pnpm streamctl upgrade --to "$TARGET" --force --json
201
+ code=$?
202
+ set -e
203
+ # Faults and malformed files cannot be forced; fall back to the pin bump.
204
+ if [[ "$code" != "0" ]]; then
205
+ pnpm streamctl upgrade --to "$TARGET" --no-install --json
206
+ pnpm install --lockfile-only --ignore-scripts
207
+ echo "applied=false" >> "$GITHUB_OUTPUT"
208
+ exit 0
209
+ fi
210
+ : > "$RUNNER_TEMP/streamctl-workflows.txt"
211
+ if [[ "$ALLOW_WORKFLOW_UPDATES" != "true" ]]; then
212
+ jq -r '
213
+ (.data.plan // .error.details.plan // [])[]
214
+ | .path
215
+ | select(startswith(".github/workflows/"))
216
+ ' "$RUNNER_TEMP/streamctl-upgrade.json" | sort -u > "$RUNNER_TEMP/streamctl-workflows.txt"
217
+ fi
218
+ sort -u "$RUNNER_TEMP/streamctl-unsafe.txt" "$RUNNER_TEMP/streamctl-workflows.txt" > "$RUNNER_TEMP/streamctl-held.txt"
219
+ while IFS= read -r path; do
220
+ if git cat-file -e "HEAD:$path" 2>/dev/null; then
221
+ git restore --source=HEAD --worktree -- "$path"
222
+ else
223
+ rm -f -- "$path"
224
+ fi
225
+ done < "$RUNNER_TEMP/streamctl-held.txt"
226
+ echo "applied=true" >> "$GITHUB_OUTPUT"
227
+
228
+ - name: Validate the completed upgrade
229
+ if: ${{ steps.outcome.outputs.completed == 'true' && steps.workflow_changes.outputs.blocked == 'false' }}
230
+ run: |
231
+ pnpm streamctl check
232
+ pnpm run --if-present lint
233
+ pnpm run --if-present typecheck
234
+ pnpm run --if-present test
235
+ pnpm run --if-present build
236
+
237
+ - name: Validate the partial upgrade
238
+ if: ${{ steps.workflow_changes.outputs.blocked == 'true' }}
239
+ run: |
240
+ set +e
241
+ pnpm streamctl check --fail-on drift --json > "$RUNNER_TEMP/streamctl-partial-check.raw"
242
+ code=$?
243
+ set -e
244
+ tail -n 1 "$RUNNER_TEMP/streamctl-partial-check.raw" > "$RUNNER_TEMP/streamctl-partial-check.json"
245
+ test "$code" -eq 3
246
+ jq -Rsc 'split("\n") | map(select(length > 0)) | sort' \
247
+ "$RUNNER_TEMP/streamctl-workflows.txt" > "$RUNNER_TEMP/streamctl-workflows.json"
248
+ jq -e --slurpfile expected "$RUNNER_TEMP/streamctl-workflows.json" '
249
+ .error.code == "DRIFT_DETECTED"
250
+ and (.error.details.structuralFaults | length == 0)
251
+ and (.error.details.versionSkew | length == 0)
252
+ and (([.error.details.drift[].path] | sort) == $expected[0])
253
+ ' "$RUNNER_TEMP/streamctl-partial-check.json" > /dev/null
254
+ pnpm run --if-present lint
255
+ pnpm run --if-present typecheck
256
+ pnpm run --if-present test
257
+ pnpm run --if-present build
258
+
259
+ - name: Prepare the completed-upgrade description
260
+ if: ${{ steps.outcome.outputs.completed == 'true' && steps.workflow_changes.outputs.blocked == 'false' }}
261
+ env:
262
+ TARGET: ${{ steps.check.outputs.target }}
263
+ run: |
264
+ from=$(jq -r '.data.payload.pinned' "$RUNNER_TEMP/streamctl-status.json")
265
+ {
266
+ echo "## Summary"
267
+ echo
268
+ printf -- '- Updated the streamctl payload from `%s` to `%s`.\n' "$from" "$TARGET"
269
+ echo "- Reconciled all managed files."
270
+ echo "- Updated dependencies and the lockfile."
271
+ echo
272
+ echo "## Validation"
273
+ echo
274
+ echo '- `pnpm streamctl check`'
275
+ echo "- Available lint, typecheck, test, and build scripts"
276
+ } > "$RUNNER_TEMP/streamctl-pr.md"
277
+
278
+ - name: Open the completed upgrade PR
279
+ if: ${{ steps.outcome.outputs.completed == 'true' && steps.workflow_changes.outputs.blocked == 'false' }}
103
280
  uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
104
281
  with:
105
- token: ${{ secrets.STREAMCTL_PR_TOKEN }}
106
- branch: chore/streamctl-upgrade
107
- commit-message: "chore: upgrade the streamctl payload"
108
- title: "chore: upgrade the streamctl payload"
109
- body: |
110
- Automated payload upgrade: the managed files were reconciled cleanly by
111
- `streamctl upgrade`. Review the diff; `streamctl-upgrade.json` (in the run
112
- logs) has the full sync summary and dependency bumps.
282
+ token: ${{ env.ALLOW_WORKFLOW_UPDATES == 'true' && secrets.${PR_TOKEN_SECRET} || github.token }}
283
+ branch: chore/streamctl-upgrade-${{ steps.check.outputs.target }}
284
+ commit-message: "chore: upgrade streamctl payload to ${{ steps.check.outputs.target }}"
285
+ title: "chore: upgrade streamctl payload to ${{ steps.check.outputs.target }}"
286
+ body-path: ${{ runner.temp }}/streamctl-pr.md
113
287
  labels: streamctl
114
288
 
115
- - name: Open a plan-only PR (conflicts need interactive sync)
116
- if: ${{ steps.upgrade.outputs.code == '2' }}
289
+ - name: Prepare the resolve instructions
290
+ if: ${{ steps.workflow_changes.outputs.blocked == 'true' || steps.manual.outputs.applied != '' }}
291
+ env:
292
+ TARGET: ${{ steps.check.outputs.target }}
293
+ run: |
294
+ branch="chore/streamctl-upgrade-$TARGET"
295
+ {
296
+ echo "## Resolve"
297
+ echo
298
+ echo '```sh'
299
+ printf 'git fetch origin %s\n' "$branch"
300
+ printf 'git switch --create %s --track origin/%s\n' "$branch" "$branch"
301
+ echo 'pnpm install --frozen-lockfile'
302
+ echo 'pnpm streamctl sync --interactive'
303
+ echo 'pnpm streamctl check'
304
+ echo 'pnpm run --if-present lint'
305
+ echo 'pnpm run --if-present typecheck'
306
+ echo 'pnpm run --if-present test'
307
+ echo 'pnpm run --if-present build'
308
+ echo 'git add .'
309
+ echo 'git commit -m "chore: finish streamctl upgrade"'
310
+ echo 'git push'
311
+ echo '```'
312
+ } > "$RUNNER_TEMP/streamctl-resolve.md"
313
+
314
+ - name: Prepare the workflow-update description
315
+ if: ${{ steps.workflow_changes.outputs.blocked == 'true' }}
316
+ env:
317
+ TARGET: ${{ steps.check.outputs.target }}
318
+ run: |
319
+ from=$(jq -r '.data.payload.pinned' "$RUNNER_TEMP/streamctl-status.json")
320
+ {
321
+ echo "## Status"
322
+ echo
323
+ echo "This update omits GitHub Actions workflow changes. Do not merge this pull request yet."
324
+ echo
325
+ printf -- '- Updated the streamctl payload from `%s` to `%s`.\n' "$from" "$TARGET"
326
+ echo "- Applied every other safe managed-file change."
327
+ echo "- Updated dependencies and the lockfile."
328
+ echo
329
+ echo "## Omitted workflow files"
330
+ echo
331
+ sed 's|^|- `|; s|$|`|' "$RUNNER_TEMP/streamctl-workflows.txt"
332
+ echo
333
+ cat "$RUNNER_TEMP/streamctl-resolve.md"
334
+ } > "$RUNNER_TEMP/streamctl-workflow-pr.md"
335
+
336
+ - name: Open the workflow-update PR
337
+ if: ${{ steps.workflow_changes.outputs.blocked == 'true' }}
338
+ uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
339
+ with:
340
+ token: ${{ env.ALLOW_WORKFLOW_UPDATES == 'true' && secrets.${PR_TOKEN_SECRET} || github.token }}
341
+ branch: chore/streamctl-upgrade-${{ steps.check.outputs.target }}
342
+ commit-message: "chore: prepare streamctl payload ${{ steps.check.outputs.target }}"
343
+ title: "chore: upgrade streamctl payload to ${{ steps.check.outputs.target }}"
344
+ body-path: ${{ runner.temp }}/streamctl-workflow-pr.md
345
+ draft: true
346
+ labels: |
347
+ streamctl
348
+ needs-interactive-upgrade
349
+
350
+ - name: Prepare the manual-upgrade description
351
+ if: ${{ steps.upgrade.outputs.code == '2' && steps.conflicts.outputs.safe == 'false' }}
352
+ env:
353
+ APPLIED: ${{ steps.manual.outputs.applied }}
354
+ TARGET: ${{ steps.check.outputs.target }}
355
+ run: |
356
+ from=$(jq -r '.data.payload.pinned' "$RUNNER_TEMP/streamctl-status.json")
357
+ {
358
+ echo "## Status"
359
+ echo
360
+ echo "This update needs manual conflict resolution. Do not merge this pull request yet."
361
+ echo
362
+ printf -- '- Updated the streamctl payload from `%s` to `%s`.\n' "$from" "$TARGET"
363
+ if [[ "$APPLIED" == "true" ]]; then
364
+ echo "- Applied every safe managed-file change."
365
+ echo "- Left conflicting managed files unchanged."
366
+ else
367
+ echo "- Did not update managed files because \`streamctl upgrade --force\` failed. See the workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
368
+ fi
369
+ echo "- Updated dependencies and the lockfile."
370
+ echo
371
+ echo "## Conflicts"
372
+ echo
373
+ jq -r --arg applied "$APPLIED" --rawfile unsafe "$RUNNER_TEMP/streamctl-unsafe.txt" '
374
+ ($unsafe | split("\n")) as $held
375
+ | .error.details.conflicted[]
376
+ | select($applied != "true" or (.path | IN($held[])))
377
+ | "- `\(.path)` (`\(.kind)`): \(.reason)"
378
+ ' "$RUNNER_TEMP/streamctl-upgrade.json"
379
+ echo
380
+ if [[ -s "$RUNNER_TEMP/streamctl-workflows.txt" ]]; then
381
+ echo "## Omitted workflow files"
382
+ echo
383
+ sed 's|^|- `|; s|$|`|' "$RUNNER_TEMP/streamctl-workflows.txt"
384
+ echo
385
+ fi
386
+ cat "$RUNNER_TEMP/streamctl-resolve.md"
387
+ } > "$RUNNER_TEMP/streamctl-conflict-pr.md"
388
+
389
+ - name: Open the manual upgrade PR
390
+ if: ${{ steps.upgrade.outputs.code == '2' && steps.conflicts.outputs.safe == 'false' }}
117
391
  uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
118
392
  with:
119
- token: ${{ secrets.STREAMCTL_PR_TOKEN }}
120
- branch: chore/streamctl-upgrade
121
- commit-message: "chore: bump the streamctl payload pin (interactive sync required)"
122
- title: "chore: streamctl payload upgrade needs interactive sync"
123
- body: |
124
- The automated `streamctl upgrade` hit conflicts and rolled the sync back, so
125
- only the payload pin + devDependency were bumped here. Finish the upgrade by
126
- hand: check out this branch, run `pnpm streamctl sync --interactive`, resolve
127
- each conflict, and push.
393
+ token: ${{ env.ALLOW_WORKFLOW_UPDATES == 'true' && secrets.${PR_TOKEN_SECRET} || github.token }}
394
+ branch: chore/streamctl-upgrade-${{ steps.check.outputs.target }}
395
+ commit-message: "chore: prepare streamctl payload ${{ steps.check.outputs.target }}"
396
+ title: "chore: upgrade streamctl payload to ${{ steps.check.outputs.target }}"
397
+ body-path: ${{ runner.temp }}/streamctl-conflict-pr.md
398
+ draft: true
128
399
  labels: |
129
400
  streamctl
130
401
  needs-interactive-upgrade
@@ -11,9 +11,7 @@ pglite-debug.log
11
11
  prisma/*.db
12
12
  prisma/migrations/dev
13
13
  .DS_Store
14
- # streamctl local scratch dirs/files (e.g. local.concept) stay untracked.
15
- local.*
16
- local.*/
17
- # CLAUDE.md is managed by streamctl; a user-global gitignore that ignores the
18
- # filename everywhere would silently untrack it. Force-allow it here.
19
- !CLAUDE.md
14
+ # streamctl scratch. Root-anchored so `src/path/local.ts` stays tracked.
15
+ /local.*
16
+ /local.*/
17
+ prisma/local.*
@@ -3,8 +3,9 @@
3
3
  "browser": true
4
4
  },
5
5
  "ignorePatterns": [
6
- "local.*",
7
- "local.*/"
6
+ "/local.*",
7
+ "/local.*/",
8
+ "prisma/local.*"
8
9
  ],
9
10
  "plugins": [
10
11
  "unicorn",
@@ -1,22 +1,15 @@
1
- # Managed by streamctl (full-own, render: security). pnpm's own settings file.
1
+ # Managed by streamctl. pnpm's own settings file.
2
2
  #
3
- # SUPPLY-CHAIN COOLDOWN: a version must have been published for ${MIN_RELEASE_AGE}
4
- # minutes before pnpm will resolve it, so a compromised release has time to be caught
5
- # and yanked before the fleet installs it. Requires pnpm >= 10.16 (baseline: 10.28.1);
6
- # it gates fresh resolution only, so `--frozen-lockfile` installs are unaffected.
7
- # `@sidebase/*` is exempt: the first-party scope (this payload + the streamctl CLI),
8
- # published by the org itself, so a payload release reaches the fleet the same day.
3
+ # Cooldown: a version must be ${MIN_RELEASE_AGE} minutes old before pnpm resolves it,
4
+ # giving a compromised release time to be yanked. Needs pnpm >= 10.16. Gates fresh
5
+ # resolution only, so `--frozen-lockfile` is unaffected. `@sidebase/*` is exempt.
9
6
  #
10
- # `packages: []` is REQUIRED, not decorative: pnpm defaults it to `**` whenever a
11
- # pnpm-workspace.yaml exists, which would silently promote every nested package.json
12
- # (test fixtures, examples) to a workspace project and break `--frozen-lockfile`.
13
- # A real monorepo owns this key itself, so opt the file out with
14
- # `files: { "pnpm-workspace.yaml": "off" }` in .streamctl/config.ts.
7
+ # `packages: []` is required: pnpm otherwise defaults it to `**`, promoting every nested
8
+ # package.json to a workspace project and breaking `--frozen-lockfile`. A monorepo should
9
+ # opt out with `files: { "pnpm-workspace.yaml": "off" }`.
15
10
  #
16
- # `onlyBuiltDependencies` is the postinstall-script allowlist: everything else installs
17
- # with its lifecycle scripts blocked. The baseline covers what the fleet universally
18
- # needs; add extras via `pnpm: { onlyBuiltDependencies: [...] }` rather than running
19
- # `pnpm approve-builds` (which writes here and would be reverted on the next sync).
11
+ # `onlyBuiltDependencies` allowlists postinstall scripts. Add extras through the `pnpm`
12
+ # knob, not `pnpm approve-builds`, which writes here and is reverted on the next sync.
20
13
  packages: []
21
14
  minimumReleaseAge: ${MIN_RELEASE_AGE}
22
15
  minimumReleaseAgeExclude:
@@ -10,13 +10,14 @@
10
10
  { "path": ".vscode/settings.json", "strategy": "merge", "source": "base/vscode/settings.json", "render": "editor", "projectFields": ["editor.fontSize", "editor.rulers", "editor.formatOnSave", "files.exclude", "search.exclude", "files.watcherExclude"] },
11
11
  { "path": ".vscode/extensions.json", "strategy": "merge", "source": "base/vscode/extensions.json", "projectFields": ["unwantedRecommendations"] },
12
12
  { "path": "AGENTS.md", "strategy": "full", "source": "base/AGENTS.md", "adoption": "expected" },
13
- { "path": "CLAUDE.md", "strategy": "scaffold", "source": "base/CLAUDE.md", "adoption": "expected" },
14
13
  { "path": ".github/workflows/streamctl-upgrade.yml", "strategy": "full", "source": "base/github/workflows/streamctl-upgrade.yml", "render": "automation-upgrade", "enabledBy": "automation.upgradePr", "adoption": "unexpected" }
15
14
  ],
16
15
  "renders": {
17
16
  "automation-upgrade": {
18
17
  "placeholders": {
19
- "NODE_VERSION": { "configPath": "versions.node", "default": "24.13.0", "pattern": "^[\\w.+-]+$" }
18
+ "ALLOW_WORKFLOW_UPDATES": { "configPath": "automation.allowWorkflowUpdates", "default": "false", "pattern": "^(?:true|false)$" },
19
+ "NODE_VERSION": { "configPath": "versions.node", "default": "24.13.0", "pattern": "^[\\w.+-]+$" },
20
+ "PR_TOKEN_SECRET": { "configPath": "automation.prTokenSecret", "default": "STREAMCTL_PR_TOKEN", "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" }
20
21
  }
21
22
  },
22
23
  "editor": {
@@ -34,6 +35,8 @@
34
35
  }
35
36
  },
36
37
  "configKeys": {
38
+ "automation.allowWorkflowUpdates": "boolean",
39
+ "automation.prTokenSecret": "string",
37
40
  "automation.upgradePr": "boolean",
38
41
  "editor.i18nSourceLanguage": "string",
39
42
  "security.minimumReleaseAge": "string",