@sidebase/base-config 0.2.1 → 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/README.md CHANGED
@@ -590,15 +590,18 @@ sync. A repo that genuinely needs a different layout should opt the file out
590
590
 
591
591
  The base preset ships `.github/workflows/streamctl-upgrade.yml`, a scheduled workflow
592
592
  that opens a PR when a payload update is available. It is an `enabledBy`-gated managed
593
- file, off by default: it only lands once a repo opts in, and it is inert until the
594
- required token exists.
593
+ file, off by default. It only lands once a repo opts in.
595
594
 
596
595
  Enable it in `streamctl.config.ts`, then `streamctl sync`:
597
596
 
598
597
  ```ts
599
598
  export default {
600
599
  // ...
601
- automation: { upgradePr: true },
600
+ automation: {
601
+ upgradePr: true,
602
+ // allowWorkflowUpdates: true,
603
+ // prTokenSecret: "STREAMCTL_PR_TOKEN",
604
+ },
602
605
  // optional: version pins, default to the payload's baseline. `node` feeds
603
606
  // ci.yml + the Dockerfile; `pnpm` feeds the Dockerfile only (CI pins pnpm
604
607
  // via package.json#packageManager):
@@ -606,24 +609,50 @@ export default {
606
609
  };
607
610
  ```
608
611
 
609
- Required secrets:
610
-
611
- | Secret | Purpose |
612
- | ------ | ------- |
613
- | `STREAMCTL_PR_TOKEN` | A GitHub App installation token or machine-account PAT used to open the PR. **Not** the default `GITHUB_TOKEN`: a PR opened with `GITHUB_TOKEN` does not trigger `pull_request` workflows, so the repo's own `check` gate would never run on the bot PR. |
614
-
615
- That is the only one, because the payload is public npm, so the install step needs no registry
616
- token. A repo whose *other* dependencies are private must opt this full-owned workflow
617
- out and wire its own auth.
612
+ By default, the workflow uses `GITHUB_TOKEN` and omits changes under
613
+ `.github/workflows/**`. If an update contains workflow changes, it opens a draft PR
614
+ with every other safe change and lists the omitted files and completion commands.
618
615
 
619
- The workflow branches on the CLI exit codes: `check` exit 4 opens a PR; exit 0 stops;
620
- exit 3 (pre-existing drift) opens a drift issue instead. A clean `upgrade` (exit 0)
621
- produces a reconcile PR; a conflicted `upgrade` (exit 2, rolled back) produces a
622
- plan-only PR labelled `needs-interactive-upgrade` for a human to finish with
623
- `streamctl sync --interactive`.
616
+ Set `automation.allowWorkflowUpdates` to `true` to include workflow changes. Add a
617
+ `STREAMCTL_PR_TOKEN` repository secret with `Contents`, `Pull requests`, and
618
+ `Workflows` write access. Change `automation.prTokenSecret` when the secret has another
619
+ name. A fine-grained PAT or GitHub App token works. The built-in `GITHUB_TOKEN` cannot
620
+ receive `Workflows` permission.
624
621
 
625
- > The org bot identity is still being decided. Until the runbook lands, leave the
626
- > workflow disabled. Design only; nothing is enabled anywhere.
622
+ | Knob | Default | Effect |
623
+ | ---- | ------- | ------ |
624
+ | `automation.upgradePr` | `false` | Sync the scheduled upgrade workflow |
625
+ | `automation.allowWorkflowUpdates` | `false` | Include `.github/workflows/**` changes in upgrade PRs |
626
+ | `automation.prTokenSecret` | `"STREAMCTL_PR_TOKEN"` | Secret used when workflow updates are enabled |
627
+
628
+ The upgrade job runs `streamctl check` and every available lint, typecheck, test, and
629
+ build script before opening a ready PR. PRs created with `GITHUB_TOKEN` do not trigger
630
+ their own `pull_request` workflows, so these inline checks are required. The external
631
+ token allows the new PR to trigger them normally.
632
+
633
+ The workflow handles CLI results as follows:
634
+
635
+ - `check` exit 0: stop without a PR.
636
+ - `check` exit 3: open or update one drift issue.
637
+ - `check` exit 4: attempt the upgrade.
638
+ - `upgrade` exit 0: validate and open a ready PR, or a draft when workflow files were omitted.
639
+ - `upgrade` exit 2: inspect conflicts using the pre-upgrade status.
640
+ - Any other exit: fail the workflow.
641
+
642
+ A conflict is safe to apply when the same path was previously `full` managed and
643
+ `in-sync`. The workflow retries those generated replacements with `--force`. Any new,
644
+ modified, malformed, or structurally conflicting path produces a draft PR instead.
645
+ Workflow changes also produce a draft unless explicitly allowed. Drafts contain every
646
+ safe change, keep conflicting and omitted workflow files at their current content, list
647
+ what remains, and give the commands needed to finish the upgrade. When a malformed or
648
+ structurally invalid file blocks `--force`, the draft only updates the payload version
649
+ and lockfile.
650
+
651
+ Diagnostic JSON stays under the runner's temporary directory. Upgrade branches include
652
+ the target version, so a later release does not overwrite an older open PR.
653
+
654
+ The payload is public npm. A repo with other private dependencies must opt this workflow
655
+ out and provide its own install authentication.
627
656
 
628
657
  ### Action pinning policy
629
658
 
package/dist/config.d.mts CHANGED
@@ -99,6 +99,10 @@ interface NuxtBaseDockerConfig {
99
99
  interface NuxtBaseAutomationConfig {
100
100
  /** Sync the weekly streamctl upgrade-PR workflow (default: off, no workflow lands). */
101
101
  upgradePr?: boolean;
102
+ /** Let upgrade PRs change GitHub Actions workflows (default: false). */
103
+ allowWorkflowUpdates?: boolean;
104
+ /** Repository secret used when workflow updates are allowed. */
105
+ prTokenSecret?: string;
102
106
  }
103
107
  /**
104
108
  * Supply-chain knobs (`config.security.*`): the pnpm install cooldown rendered into
package/dist/config.d.ts CHANGED
@@ -99,6 +99,10 @@ interface NuxtBaseDockerConfig {
99
99
  interface NuxtBaseAutomationConfig {
100
100
  /** Sync the weekly streamctl upgrade-PR workflow (default: off, no workflow lands). */
101
101
  upgradePr?: boolean;
102
+ /** Let upgrade PRs change GitHub Actions workflows (default: false). */
103
+ allowWorkflowUpdates?: boolean;
104
+ /** Repository secret used when workflow updates are allowed. */
105
+ prTokenSecret?: string;
102
106
  }
103
107
  /**
104
108
  * Supply-chain knobs (`config.security.*`): the pnpm install cooldown rendered into
package/dist/config.mjs CHANGED
@@ -24,7 +24,9 @@ const NUXT_BASE_DOCKER_KEYS = knobKeys({
24
24
  startCommand: 0
25
25
  });
26
26
  const NUXT_BASE_AUTOMATION_KEYS = knobKeys({
27
- upgradePr: 0
27
+ upgradePr: 0,
28
+ allowWorkflowUpdates: 0,
29
+ prTokenSecret: 0
28
30
  });
29
31
  const NUXT_BASE_SECURITY_KEYS = knobKeys({
30
32
  minimumReleaseAge: 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sidebase/base-config",
3
- "version": "0.2.1",
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",
@@ -1,41 +1,30 @@
1
- # Managed by streamctl. Off by default; lands only when a repo sets
2
- # `automation: { upgradePr: true }`. See the README section "Upgrade-PR workflow".
3
- #
4
- # Needs `secrets.STREAMCTL_PR_TOKEN`: a PR opened with `GITHUB_TOKEN` does not trigger
5
- # `pull_request` workflows, so the repo's own `check` gate would never run on it.
6
- #
7
- # A repo with private dependencies must opt this file out and wire its own auth:
8
- # `files: { ".github/workflows/streamctl-upgrade.yml": "off" }`.
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.
9
4
  name: streamctl upgrade
10
5
 
11
6
  on:
12
7
  schedule:
13
- - cron: "0 6 * * 1" # Mondays 06:00 UTC
8
+ - cron: "0 6 * * 1"
14
9
  workflow_dispatch:
15
10
 
16
- # Every PR and issue write below uses STREAMCTL_PR_TOKEN, so the default token
17
- # only needs to read the repo for checkout.
18
11
  permissions:
19
- contents: read
12
+ contents: write
13
+ issues: write
14
+ pull-requests: write
20
15
 
21
16
  env:
17
+ ALLOW_WORKFLOW_UPDATES: "${ALLOW_WORKFLOW_UPDATES}"
22
18
  NODE_VERSION: "${NODE_VERSION}"
23
19
 
24
20
  jobs:
25
21
  upgrade:
26
22
  runs-on: ubuntu-latest
27
23
  steps:
28
- # To mint an App token in-workflow instead of storing a PAT, uncomment this
29
- # and use `${{ steps.bot.outputs.token }}` in place of STREAMCTL_PR_TOKEN:
30
- #
31
- # - id: bot
32
- # uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
33
- # with:
34
- # app-id: ${{ secrets.STREAMCTL_APP_ID }}
35
- # private-key: ${{ secrets.STREAMCTL_APP_PRIVATE_KEY }}
36
-
37
24
  - name: Checkout
38
25
  uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
26
+ with:
27
+ persist-credentials: false
39
28
 
40
29
  - name: Set up pnpm
41
30
  uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
@@ -51,70 +40,362 @@ jobs:
51
40
  - name: Install dependencies
52
41
  run: pnpm install --frozen-lockfile
53
42
 
54
- # Exit codes: 4 = update available, 0 = up to date, 3 = repo already drifted.
43
+ - name: Capture managed-file state
44
+ run: pnpm streamctl status --json > "$RUNNER_TEMP/streamctl-status.json"
45
+
55
46
  - id: check
56
47
  name: Check for a payload update
57
48
  run: |
58
49
  set +e
59
- pnpm streamctl check --fail-on outdated --json > streamctl-check.json
60
- echo "code=$?" >> "$GITHUB_OUTPUT"
50
+ pnpm streamctl check --fail-on any --json > "$RUNNER_TEMP/streamctl-check.json"
51
+ code=$?
61
52
  set -e
62
- 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
63
60
 
64
- - 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
65
82
  if: ${{ steps.check.outputs.code == '3' }}
66
83
  env:
67
- GH_TOKEN: ${{ secrets.STREAMCTL_PR_TOKEN }}
84
+ GH_TOKEN: ${{ github.token }}
68
85
  run: |
69
- gh issue create \
70
- --title "streamctl: managed files have drifted" \
71
- --label streamctl \
72
- --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
73
94
 
74
- # Only upgrade when an update is actually available (check exit 4).
75
95
  - id: upgrade
76
96
  name: Upgrade the payload
77
97
  if: ${{ steps.check.outputs.code == '4' }}
98
+ env:
99
+ CI: "false"
100
+ TARGET: ${{ steps.check.outputs.target }}
78
101
  run: |
79
102
  set +e
80
- pnpm streamctl upgrade --json > streamctl-upgrade.json
81
- echo "code=$?" >> "$GITHUB_OUTPUT"
103
+ pnpm streamctl upgrade --to "$TARGET" --yes --json > "$RUNNER_TEMP/streamctl-upgrade.raw"
104
+ code=$?
82
105
  set -e
83
- 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
84
113
 
85
- # Exit 2 means the sync hit conflicts and rolled back. Bump only the pin and
86
- # devDependency so the PR carries a diff a human reconciles interactively.
87
- - name: Prepare a plan-only bump (interactive sync required)
114
+ - id: conflicts
115
+ name: Classify upgrade conflicts
88
116
  if: ${{ steps.upgrade.outputs.code == '2' }}
89
- 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
90
137
 
91
- - name: Open the upgrade PR (clean reconcile)
92
- 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' }}
93
280
  uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
94
281
  with:
95
- token: ${{ secrets.STREAMCTL_PR_TOKEN }}
96
- branch: chore/streamctl-upgrade
97
- commit-message: "chore: upgrade the streamctl payload"
98
- title: "chore: upgrade the streamctl payload"
99
- body: |
100
- Automated payload upgrade: the managed files were reconciled cleanly by
101
- `streamctl upgrade`. Review the diff; `streamctl-upgrade.json` (in the run
102
- 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
103
287
  labels: streamctl
104
288
 
105
- - name: Open a plan-only PR (conflicts need interactive sync)
106
- 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' }}
107
391
  uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
108
392
  with:
109
- token: ${{ secrets.STREAMCTL_PR_TOKEN }}
110
- branch: chore/streamctl-upgrade
111
- commit-message: "chore: bump the streamctl payload pin (interactive sync required)"
112
- title: "chore: streamctl payload upgrade needs interactive sync"
113
- body: |
114
- The automated `streamctl upgrade` hit conflicts and rolled the sync back, so
115
- only the payload pin + devDependency were bumped here. Finish the upgrade by
116
- hand: check out this branch, run `pnpm streamctl sync --interactive`, resolve
117
- 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
118
399
  labels: |
119
400
  streamctl
120
401
  needs-interactive-upgrade
@@ -15,6 +15,3 @@ prisma/migrations/dev
15
15
  /local.*
16
16
  /local.*/
17
17
  prisma/local.*
18
- # CLAUDE.md is managed by streamctl. A user-global gitignore matching the name
19
- # everywhere would silently untrack it, so force-allow it.
20
- !CLAUDE.md
@@ -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",
@@ -1,3 +0,0 @@
1
- # CLAUDE.md
2
-
3
- @AGENTS.md