@rexezuge/tooling 0.0.0-stage → 1.0.1

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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +297 -2
  3. package/dist/eslint-boundaries.d.ts +75 -0
  4. package/dist/eslint-boundaries.d.ts.map +1 -0
  5. package/dist/eslint-boundaries.js +138 -0
  6. package/dist/eslint-boundaries.js.map +1 -0
  7. package/dist/eslint-rules.d.ts +41 -0
  8. package/dist/eslint-rules.d.ts.map +1 -0
  9. package/dist/eslint-rules.js +273 -0
  10. package/dist/eslint-rules.js.map +1 -0
  11. package/dist/eslint.d.ts +96 -0
  12. package/dist/eslint.d.ts.map +1 -0
  13. package/dist/eslint.js +189 -0
  14. package/dist/eslint.js.map +1 -0
  15. package/dist/functions/pages-proxy.d.ts +113 -0
  16. package/dist/functions/pages-proxy.d.ts.map +1 -0
  17. package/dist/functions/pages-proxy.js +131 -0
  18. package/dist/functions/pages-proxy.js.map +1 -0
  19. package/dist/index.d.ts +31 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +28 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/scripts/backup/d1-target.d.ts +35 -0
  24. package/dist/scripts/backup/d1-target.d.ts.map +1 -0
  25. package/dist/scripts/backup/d1-target.js +31 -0
  26. package/dist/scripts/backup/d1-target.js.map +1 -0
  27. package/dist/scripts/backup/destination-config.d.ts +61 -0
  28. package/dist/scripts/backup/destination-config.d.ts.map +1 -0
  29. package/dist/scripts/backup/destination-config.js +57 -0
  30. package/dist/scripts/backup/destination-config.js.map +1 -0
  31. package/dist/scripts/backup/encrypt-backup.d.ts +35 -0
  32. package/dist/scripts/backup/encrypt-backup.d.ts.map +1 -0
  33. package/dist/scripts/backup/encrypt-backup.js +98 -0
  34. package/dist/scripts/backup/encrypt-backup.js.map +1 -0
  35. package/dist/scripts/backup/naming.d.ts +44 -0
  36. package/dist/scripts/backup/naming.d.ts.map +1 -0
  37. package/dist/scripts/backup/naming.js +58 -0
  38. package/dist/scripts/backup/naming.js.map +1 -0
  39. package/dist/scripts/check-god-files.d.ts +164 -0
  40. package/dist/scripts/check-god-files.d.ts.map +1 -0
  41. package/dist/scripts/check-god-files.js +271 -0
  42. package/dist/scripts/check-god-files.js.map +1 -0
  43. package/dist/scripts/ensure-spa-shell-stub.d.ts +3 -0
  44. package/dist/scripts/ensure-spa-shell-stub.d.ts.map +1 -0
  45. package/dist/scripts/ensure-spa-shell-stub.js +30 -0
  46. package/dist/scripts/ensure-spa-shell-stub.js.map +1 -0
  47. package/dist/scripts/init-secrets.d.ts +63 -0
  48. package/dist/scripts/init-secrets.d.ts.map +1 -0
  49. package/dist/scripts/init-secrets.js +240 -0
  50. package/dist/scripts/init-secrets.js.map +1 -0
  51. package/dist/scripts/lib/cli-args.d.ts +78 -0
  52. package/dist/scripts/lib/cli-args.d.ts.map +1 -0
  53. package/dist/scripts/lib/cli-args.js +116 -0
  54. package/dist/scripts/lib/cli-args.js.map +1 -0
  55. package/dist/scripts/lib/github-actions.d.ts +26 -0
  56. package/dist/scripts/lib/github-actions.d.ts.map +1 -0
  57. package/dist/scripts/lib/github-actions.js +38 -0
  58. package/dist/scripts/lib/github-actions.js.map +1 -0
  59. package/dist/scripts/lib/wrangler-table.d.ts +46 -0
  60. package/dist/scripts/lib/wrangler-table.d.ts.map +1 -0
  61. package/dist/scripts/lib/wrangler-table.js +99 -0
  62. package/dist/scripts/lib/wrangler-table.js.map +1 -0
  63. package/dist/scripts/migrations-lock.d.ts +3 -0
  64. package/dist/scripts/migrations-lock.d.ts.map +1 -0
  65. package/dist/scripts/migrations-lock.js +46 -0
  66. package/dist/scripts/migrations-lock.js.map +1 -0
  67. package/dist/scripts/prepare-wrangler-config.d.ts +3 -0
  68. package/dist/scripts/prepare-wrangler-config.d.ts.map +1 -0
  69. package/dist/scripts/prepare-wrangler-config.js +50 -0
  70. package/dist/scripts/prepare-wrangler-config.js.map +1 -0
  71. package/dist/scripts/spa-shell.d.ts +41 -0
  72. package/dist/scripts/spa-shell.d.ts.map +1 -0
  73. package/dist/scripts/spa-shell.js +155 -0
  74. package/dist/scripts/spa-shell.js.map +1 -0
  75. package/dist/scripts/validate-locales.d.ts +26 -0
  76. package/dist/scripts/validate-locales.d.ts.map +1 -0
  77. package/dist/scripts/validate-locales.js +350 -0
  78. package/dist/scripts/validate-locales.js.map +1 -0
  79. package/dist/scripts/verify-migrations.d.ts +62 -0
  80. package/dist/scripts/verify-migrations.d.ts.map +1 -0
  81. package/dist/scripts/verify-migrations.js +302 -0
  82. package/dist/scripts/verify-migrations.js.map +1 -0
  83. package/dist/scripts/verify-spa-shell.d.ts +3 -0
  84. package/dist/scripts/verify-spa-shell.d.ts.map +1 -0
  85. package/dist/scripts/verify-spa-shell.js +53 -0
  86. package/dist/scripts/verify-spa-shell.js.map +1 -0
  87. package/dist/scripts/wrangler-config/cli.d.ts +22 -0
  88. package/dist/scripts/wrangler-config/cli.d.ts.map +1 -0
  89. package/dist/scripts/wrangler-config/cli.js +51 -0
  90. package/dist/scripts/wrangler-config/cli.js.map +1 -0
  91. package/dist/scripts/wrangler-config/patches.d.ts +51 -0
  92. package/dist/scripts/wrangler-config/patches.d.ts.map +1 -0
  93. package/dist/scripts/wrangler-config/patches.js +140 -0
  94. package/dist/scripts/wrangler-config/patches.js.map +1 -0
  95. package/dist/scripts/wrangler-config/resources.d.ts +70 -0
  96. package/dist/scripts/wrangler-config/resources.d.ts.map +1 -0
  97. package/dist/scripts/wrangler-config/resources.js +290 -0
  98. package/dist/scripts/wrangler-config/resources.js.map +1 -0
  99. package/dist/scripts/wrangler-config/types.d.ts +103 -0
  100. package/dist/scripts/wrangler-config/types.d.ts.map +1 -0
  101. package/dist/scripts/wrangler-config/types.js +49 -0
  102. package/dist/scripts/wrangler-config/types.js.map +1 -0
  103. package/dist/test/integration-migrations.d.ts +167 -0
  104. package/dist/test/integration-migrations.d.ts.map +1 -0
  105. package/dist/test/integration-migrations.js +171 -0
  106. package/dist/test/integration-migrations.js.map +1 -0
  107. package/dist/test/mocks/cloudflare-workers.d.ts +106 -0
  108. package/dist/test/mocks/cloudflare-workers.d.ts.map +1 -0
  109. package/dist/test/mocks/cloudflare-workers.js +90 -0
  110. package/dist/test/mocks/cloudflare-workers.js.map +1 -0
  111. package/dist/vite.d.ts +117 -0
  112. package/dist/vite.d.ts.map +1 -0
  113. package/dist/vite.js +125 -0
  114. package/dist/vite.js.map +1 -0
  115. package/dist/vitest-web.d.ts +73 -0
  116. package/dist/vitest-web.d.ts.map +1 -0
  117. package/dist/vitest-web.js +72 -0
  118. package/dist/vitest-web.js.map +1 -0
  119. package/dist/vitest.d.ts +92 -0
  120. package/dist/vitest.d.ts.map +1 -0
  121. package/dist/vitest.js +128 -0
  122. package/dist/vitest.js.map +1 -0
  123. package/package.json +58 -3
  124. package/src/eslint-boundaries.ts +169 -0
  125. package/src/eslint-rules.ts +276 -0
  126. package/src/eslint.test.ts +175 -0
  127. package/src/eslint.ts +257 -0
  128. package/src/functions/pages-proxy.test.ts +72 -0
  129. package/src/functions/pages-proxy.ts +187 -0
  130. package/src/github/actions/retry-step/action.yml +39 -0
  131. package/src/github/actions/setup-env/action.yml +20 -0
  132. package/src/github/dependabot.yml +30 -0
  133. package/src/github/workflows/backup-main.yml +46 -0
  134. package/src/github/workflows/continuous-deployment.yml +188 -0
  135. package/src/github/workflows/continuous-integration.yml +259 -0
  136. package/src/github/workflows/scheduled-version-update.yml +38 -0
  137. package/src/github/workflows/upstream-sync.yml +56 -0
  138. package/src/index.ts +42 -0
  139. package/src/scripts/backup/backup-rules.test.ts +105 -0
  140. package/src/scripts/backup/d1-target.ts +54 -0
  141. package/src/scripts/backup/destination-config.ts +92 -0
  142. package/src/scripts/backup/encrypt-backup.ts +107 -0
  143. package/src/scripts/backup/naming.ts +62 -0
  144. package/src/scripts/check-god-files.test.ts +131 -0
  145. package/src/scripts/check-god-files.ts +327 -0
  146. package/src/scripts/ensure-spa-shell-stub.ts +34 -0
  147. package/src/scripts/init-secrets.ts +265 -0
  148. package/src/scripts/lib/cli-args.ts +154 -0
  149. package/src/scripts/lib/github-actions.ts +41 -0
  150. package/src/scripts/lib/wrangler-table.ts +105 -0
  151. package/src/scripts/migrations-lock.ts +52 -0
  152. package/src/scripts/prepare-wrangler-config.ts +51 -0
  153. package/src/scripts/spa-shell.test.ts +91 -0
  154. package/src/scripts/spa-shell.ts +179 -0
  155. package/src/scripts/validate-locales.test.ts +89 -0
  156. package/src/scripts/validate-locales.ts +380 -0
  157. package/src/scripts/verify-migrations.test.ts +71 -0
  158. package/src/scripts/verify-migrations.ts +364 -0
  159. package/src/scripts/verify-spa-shell.ts +56 -0
  160. package/src/scripts/wrangler-config/cli.ts +51 -0
  161. package/src/scripts/wrangler-config/patches.ts +157 -0
  162. package/src/scripts/wrangler-config/resources.ts +330 -0
  163. package/src/scripts/wrangler-config/types.ts +113 -0
  164. package/src/test/integration-migrations.test.ts +169 -0
  165. package/src/test/integration-migrations.ts +267 -0
  166. package/src/test/mocks/cloudflare-workers.ts +115 -0
  167. package/src/vite.test.ts +83 -0
  168. package/src/vite.ts +202 -0
  169. package/src/vitest-web.ts +109 -0
  170. package/src/vitest.ts +185 -0
@@ -0,0 +1,259 @@
1
+ # Continuous Integration — the converged gate for the family's worker repos.
2
+ #
3
+ # The most-common superset of the nine repos' CI workflows. Each job exists because
4
+ # its failure is invisible to every other job: a locale defect still typechecks, a
5
+ # god file still lints, an edited migration still deploys. A job per gate means the
6
+ # run log names the tool that broke rather than burying it in a `checks` job.
7
+ #
8
+ # PLACEHOLDERS — the things a consumer must change for this to run at all:
9
+ # * REPLACE_ME_SCOPE the npm scope, in the `pnpm --filter @REPLACE_ME_SCOPE/web build`
10
+ # invocations below.
11
+ # * REPLACE_ME_WEB_FILTER the workspace filter for the SPA (usually @REPLACE_ME_SCOPE/web).
12
+ # * REPLACE_ME_MIGRATIONS the D1 binding `wrangler d1 migrations apply` targets.
13
+ #
14
+ # The script names in the `run:` steps are the ones this package's scripts suggest;
15
+ # see README.md for the package.json wiring.
16
+
17
+ name: Continuous Integration
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ on:
23
+ repository_dispatch:
24
+ types:
25
+ - Upstream Sync Integration
26
+ pull_request:
27
+ push:
28
+ branches:
29
+ - main
30
+ workflow_dispatch:
31
+
32
+ concurrency:
33
+ group: continuous-integration-${{ github.ref }}
34
+ cancel-in-progress: true
35
+
36
+ jobs:
37
+ # Typecheck and lint are separate from the gates that follow, because they need
38
+ # nothing but a checkout while the gates are read-only consumers of the tree.
39
+ checks:
40
+ name: Checks
41
+ runs-on: ubuntu-latest
42
+
43
+ steps:
44
+ - name: Checkout Repository
45
+ uses: actions/checkout@v6
46
+
47
+ - name: Setup Environment
48
+ uses: ./.github/actions/setup-env
49
+
50
+ # `pnpm -r typecheck` reaches every workspace package, `test/` among them.
51
+ - name: Run Type Check and Lint
52
+ run: pnpm -r typecheck && pnpm run lint
53
+
54
+ # Its own step, and read-only, because the `lint` job cannot catch this:
55
+ # `pnpm run lint` is `eslint --fix --quiet`, and `--fix` repairs what it can
56
+ # while `--quiet` suppresses the rest — so CI exits 0 on a tree Prettier
57
+ # rejects, the repairs land in the runner's checkout, and they are thrown
58
+ # away. `prettier --check` is the one command that fails instead of
59
+ # repairing. Edge-Sonic had 242 files in exactly that state.
60
+ - name: Check Formatting
61
+ run: pnpm run format:check
62
+
63
+ - name: Check God-Files
64
+ uses: ./.github/actions/retry-step
65
+ with:
66
+ command: pnpm run check:god-files
67
+ max_attempts: '2'
68
+
69
+ # A locale defect is invisible to the unit tests: the catalogs are data, and
70
+ # a key missing from every bundle still typechecks and still builds.
71
+ - name: Validate Locale Bundles
72
+ uses: ./.github/actions/retry-step
73
+ with:
74
+ command: pnpm run validate:locales
75
+ max_attempts: '2'
76
+
77
+ # Editing an already-applied migration is invisible to every other check:
78
+ # the deploy succeeds, fresh databases pick up the edited statements, and
79
+ # production keeps the schema it had. The lock is what turns that into a
80
+ # failure, and this job is read-only — `migrations-lock` is the operator's
81
+ # command, deliberately not CI's.
82
+ - name: Verify Migration Lock
83
+ uses: ./.github/actions/retry-step
84
+ with:
85
+ command: pnpm run validate:migrations
86
+ max_attempts: '2'
87
+
88
+ unit-tests:
89
+ name: Unit Tests
90
+ runs-on: ubuntu-latest
91
+
92
+ steps:
93
+ - name: Checkout Repository
94
+ uses: actions/checkout@v6
95
+
96
+ - name: Setup Environment
97
+ uses: ./.github/actions/setup-env
98
+
99
+ - name: Run Tests with Coverage
100
+ uses: ./.github/actions/retry-step
101
+ with:
102
+ command: pnpm run test:coverage
103
+ max_attempts: '2'
104
+
105
+ - name: Upload Coverage Report
106
+ if: always()
107
+ uses: actions/upload-artifact@v4
108
+ with:
109
+ name: coverage-report
110
+ path: coverage/
111
+ retention-days: 14
112
+
113
+ # The SPA's own suite. Only repositories with a Vite SPA keep this job; the two
114
+ # configs partition the suite rather than each running all of it, so a jsdom
115
+ # suite under `node` cannot pass while failing.
116
+ web-tests:
117
+ name: Web Tests
118
+ runs-on: ubuntu-latest
119
+
120
+ steps:
121
+ - name: Checkout Repository
122
+ uses: actions/checkout@v6
123
+
124
+ - name: Setup Environment
125
+ uses: ./.github/actions/setup-env
126
+
127
+ - name: Run SPA Unit Tests
128
+ run: pnpm run test:web
129
+
130
+ integration-tests:
131
+ name: Integration Tests
132
+ runs-on: ubuntu-latest
133
+
134
+ steps:
135
+ - name: Checkout Repository
136
+ uses: actions/checkout@v6
137
+
138
+ - name: Setup Environment
139
+ uses: ./.github/actions/setup-env
140
+
141
+ - name: Run Integration Tests
142
+ run: pnpm run test:integration
143
+
144
+ # Only the SPA build. The API worker is typechecked by `pnpm -r typecheck` above,
145
+ # so repeating it here would run the same `tsc --noEmit` against the same
146
+ # tsconfig a second time.
147
+ build:
148
+ name: Build Web App
149
+ runs-on: ubuntu-latest
150
+
151
+ steps:
152
+ - name: Checkout Repository
153
+ uses: actions/checkout@v6
154
+
155
+ - name: Setup Environment
156
+ uses: ./.github/actions/setup-env
157
+
158
+ # PLACEHOLDER: the workspace filter for the SPA.
159
+ - name: Build Web App
160
+ run: pnpm --filter @REPLACE_ME_SCOPE/web build
161
+
162
+ # Verifies the shell the API worker *serves*, which is a different artifact from
163
+ # `apps/web/dist/` — it is embedded into `apps/api/src/generated/spa-shell.ts` by
164
+ # the Vite plugin, and both are gitignored. A stale-but-present shell is a deploy
165
+ # that serves yesterday's operator UI.
166
+ #
167
+ # It builds first rather than depending on `build`, so the failure says "the
168
+ # served shell is wrong" instead of arriving inside a build log. That costs a
169
+ # second Vite build per run; the alternative — verifying inside `build` — is free
170
+ # and is the trade being made here.
171
+ spa-shell:
172
+ name: Verify SPA Shell
173
+ runs-on: ubuntu-latest
174
+
175
+ steps:
176
+ - name: Checkout Repository
177
+ uses: actions/checkout@v6
178
+
179
+ - name: Setup Environment
180
+ uses: ./.github/actions/setup-env
181
+
182
+ # PLACEHOLDER: the workspace filter for the SPA.
183
+ - name: Build Web App
184
+ run: pnpm --filter @REPLACE_ME_SCOPE/web build
185
+
186
+ # The build is a **precondition**, not a convenience: a fresh checkout has
187
+ # neither `apps/web/dist/` nor `apps/api/src/generated/`, and postinstall
188
+ # writes a deliberately empty stub so `tsc` can resolve `SPA_HTML` before
189
+ # anyone has built. Verifying without building cannot pass, and it fails
190
+ # with precisely the message the check exists to give.
191
+ - name: Verify The SPA Shell
192
+ uses: ./.github/actions/retry-step
193
+ with:
194
+ command: pnpm run verify:spa-shell
195
+ max_attempts: '2'
196
+
197
+ dependabot-auto-merge:
198
+ if: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.state == 'open' && !github.event.repository.fork }}
199
+ name: Auto-Merge Dependabot PR
200
+ needs:
201
+ - checks
202
+ - unit-tests
203
+ - web-tests
204
+ - integration-tests
205
+ - build
206
+ - spa-shell
207
+ permissions:
208
+ contents: write
209
+ pull-requests: write
210
+ runs-on: ubuntu-latest
211
+
212
+ steps:
213
+ - name: Checkout Repository
214
+ uses: actions/checkout@v6
215
+
216
+ # Auto-merge only after every gate above is green, which is what makes it
217
+ # safe: a dependency bump that breaks the suite waits for a human instead of
218
+ # merging itself.
219
+ - name: Enable Auto-Merge
220
+ uses: ./.github/actions/retry-step
221
+ with:
222
+ command: gh pr merge --auto --merge "$PR_URL"
223
+ max_attempts: '5'
224
+ env:
225
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
226
+ PR_URL: ${{ github.event.pull_request.html_url }}
227
+
228
+ dispatch-upstream-sync-deployment:
229
+ if: ${{ github.event_name == 'repository_dispatch' && github.event.action == 'Upstream Sync Integration' }}
230
+ name: Dispatch Upstream Sync Deployment
231
+ needs:
232
+ - checks
233
+ - unit-tests
234
+ - web-tests
235
+ - integration-tests
236
+ - build
237
+ - spa-shell
238
+ permissions:
239
+ contents: write
240
+ runs-on: ubuntu-latest
241
+
242
+ steps:
243
+ - name: Checkout Repository
244
+ uses: actions/checkout@v6
245
+
246
+ # The second half of the upstream-sync contract: a fork's sync runs CI, and
247
+ # CI — not the sync workflow — is what authorises the deploy.
248
+ - name: Dispatch Deployment
249
+ uses: ./.github/actions/retry-step
250
+ with:
251
+ command: >
252
+ printf '{"event_type":"Upstream Sync Deployment","client_payload":{"branch":"%s","sha":"%s"}}\n'
253
+ "$PAYLOAD_BRANCH" "$PAYLOAD_SHA"
254
+ | gh api repos/${{ github.repository }}/dispatches --method POST --input -
255
+ max_attempts: '2'
256
+ env:
257
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
258
+ PAYLOAD_BRANCH: ${{ github.event.client_payload.branch || 'main' }}
259
+ PAYLOAD_SHA: ${{ github.event.client_payload.sha || github.sha }}
@@ -0,0 +1,38 @@
1
+ # Scheduled Version Update — stamp .github/.version twice a month.
2
+ #
3
+ # The stamp is the family's cache-buster: the CD workflow reads it, and the
4
+ # backup workflows use it in branch names. Forked repos skip the schedule
5
+ # (`if: !github.event.repository.fork`) because a fork must not race its
6
+ # upstream's push.
7
+
8
+ name: Scheduled Version Update
9
+
10
+ on:
11
+ schedule:
12
+ - cron: '0 0 1,15 * *'
13
+
14
+ permissions:
15
+ contents: write
16
+
17
+ jobs:
18
+ update-version:
19
+ if: ${{ !github.event.repository.fork }}
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - name: Checkout Repository
24
+ uses: actions/checkout@v6
25
+ with:
26
+ persist-credentials: true
27
+
28
+ - name: Write Current Unix Timestamp
29
+ run: date +%s > .github/.version
30
+
31
+ - name: Commit and Push
32
+ run: |
33
+ git config user.name "github-actions[bot]"
34
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
35
+
36
+ git add .github/.version
37
+ git diff --staged --quiet || git commit -m "chore: update .version stamp"
38
+ git push
@@ -0,0 +1,56 @@
1
+ # Upstream Sync — a fork's scheduled pull from the repository it forked.
2
+ #
3
+ # Converged from eight repos (all but Records-Provider and CalDAV-Bridge ship this
4
+ # file, and the two that differ do so only in the upstream repository name).
5
+ #
6
+ # The contract has two sides and they change in the same commit: this workflow
7
+ # publishes `Upstream Sync Integration`, and continuous-integration.yml listens for
8
+ # it. The event is named `Upstream Sync Integration` rather than `Upstream Sync`
9
+ # precisely so it cannot be confused with the `Upstream Sync Deployment` event the
10
+ # deploy workflow listens for — one sync, two dispatches, and the name is what keeps
11
+ # a reader from wiring the wrong one.
12
+
13
+ name: Upstream Sync
14
+
15
+ on:
16
+ schedule:
17
+ # Weekly, Monday 02:00 UTC. Off the hour on purpose: GitHub drops scheduled
18
+ # workflows that share a minute with high-traffic crons.
19
+ - cron: '0 2 * * 1'
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: write
24
+
25
+ jobs:
26
+ sync:
27
+ if: ${{ !github.event.repository.fork }}
28
+ name: Sync From Upstream
29
+ runs-on: ubuntu-latest
30
+
31
+ steps:
32
+ - name: Checkout Repository
33
+ uses: actions/checkout@v6
34
+
35
+ # Pinned by commit rather than a tag: this action writes a config file and
36
+ # fetches a binary onto the runner, so a moved tag is a supply-chain change
37
+ # arriving through a version bump nobody reviewed.
38
+ - name: Sync Changes
39
+ id: sync
40
+ uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
41
+ with:
42
+ # PLACEHOLDER: the upstream repository this one forks, as owner/name.
43
+ upstream_sync_repo: REPLACE_ME_ORG/REPLACE_ME_REPO
44
+ upstream_sync_branch: main
45
+ target_sync_branch: main
46
+ target_repo_token: ${{ secrets.GITHUB_TOKEN }}
47
+ test_mode: false
48
+
49
+ - name: Trigger Continuous Integration
50
+ if: steps.sync.outputs.has_new_commits == 'true'
51
+ uses: ./.github/actions/retry-step
52
+ with:
53
+ command: gh api repos/${{ github.repository }}/dispatches --method POST -f event_type='Upstream Sync Integration'
54
+ max_attempts: '2'
55
+ env:
56
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `@rexezuge/tooling` — the tooling layer of the kit.
3
+ *
4
+ * The config factories and the test helpers are re-exported here, so a consumer has
5
+ * one import for everything its `*.config.mts` files and its mocks need. The
6
+ * scripts under `scripts/` are **not** re-exported: they resolve paths relative to
7
+ * the repository they run in, so they are run from disk with `tsx`, not imported.
8
+ * See `README.md` for the copy-in recipe and the per-script usage.
9
+ *
10
+ * The zero-dependency rule: nothing here imports another `@rexezuge/*` package. The
11
+ * one exception in the tree is `scripts/backup/encrypt-backup.ts`, which uses
12
+ * `@rexezuge/d1`'s AES-GCM so the backup and the workers share one definition of
13
+ * "what an encryption key looks like"; that dependency is declared on the package
14
+ * and is only ever resolved in the consumer's repo, where the kit is installed
15
+ * alongside `@rexezuge/d1`.
16
+ */
17
+
18
+ export { defineRepoLintConfig } from './eslint';
19
+ export type { RepoLintConfigOptions } from './eslint';
20
+
21
+ export { defineUnitVitestConfig } from './vitest';
22
+ export type { CoverageThresholds, UnitVitestConfigOptions, VitestAlias } from './vitest';
23
+
24
+ export { defineWebVitestConfig } from './vitest-web';
25
+ export type { WebVitestConfigOptions } from './vitest-web';
26
+
27
+ export { defineWebViteConfig, spaShellEmbedPlugin } from './vite';
28
+ export type { ProxyEntry, SpaShellTarget, WebViteConfigOptions } from './vite';
29
+
30
+ export { createPagesProxy } from './functions/pages-proxy';
31
+ export type { Fetcher, PagesFunction, PagesFunctionContext, PagesProxyEnv, PagesProxyOptions } from './functions/pages-proxy';
32
+
33
+ // --- Test helpers ---------------------------------------------------------
34
+ //
35
+ // Aliased in a Vitest config, not imported by application code, so the mocks stay
36
+ // where the suites can reach them.
37
+
38
+ export { DurableObject, RpcTarget, WorkflowEntrypoint } from './test/mocks/cloudflare-workers';
39
+ export type { DurableObjectState, ExecutionContext } from './test/mocks/cloudflare-workers';
40
+
41
+ export { createMigrationHelper, readMigrationFiles, splitSql } from './test/integration-migrations';
42
+ export type { D1DatabaseLike, D1PreparedStatementLike, MigrationFile, MigrationHelper, MigrationHelperOptions, MigrationRange } from './test/integration-migrations';
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveD1Target } from './d1-target';
3
+ import { evaluateConfig } from './destination-config';
4
+ import { backupFileName, backupStamp, requireRetentionDays, retentionCutoff } from './naming';
5
+
6
+ describe('resolveD1Target', () => {
7
+ it('prefers database_name, falling back to the binding', () => {
8
+ // `wrangler d1 export` takes a database *name*, so the name wins when present.
9
+ expect(resolveD1Target({ d1_databases: [{ binding: 'DB', database_name: 'my-db', database_id: 'abc' }] })).toStrictEqual({
10
+ binding: 'DB',
11
+ databaseId: 'abc',
12
+ target: 'my-db',
13
+ });
14
+ expect(resolveD1Target({ d1_databases: [{ binding: 'DB', database_id: 'abc' }] }).target).toBe('DB');
15
+ });
16
+
17
+ it('refuses a placeholder id, because the database was never created', () => {
18
+ expect(() => resolveD1Target({ d1_databases: [{ binding: 'DB', database_id: '00000000-0000-0000-0000-000000000000' }] })).toThrow(
19
+ /template placeholder/,
20
+ );
21
+ });
22
+
23
+ it('refuses a config with no binding to export under', () => {
24
+ expect(() => resolveD1Target({ d1_databases: [{ database_id: 'abc' }] })).toThrow(/No d1_databases entry with a binding/);
25
+ expect(() => resolveD1Target({})).toThrow(/No d1_databases entry with a binding/);
26
+ });
27
+ });
28
+
29
+ describe('evaluateConfig', () => {
30
+ it('stays silent when nothing is configured', () => {
31
+ // An unconfigured repository is a legitimate "I do not want off-site backups"
32
+ // state and must not turn the schedule red.
33
+ expect(evaluateConfig({})).toMatchObject({ cloudflare: false, encryption: false, anyDestination: false, errors: [] });
34
+ });
35
+
36
+ it('fails closed on a destination without an encryption key', () => {
37
+ const config = evaluateConfig({
38
+ CLOUDFLARE_API_TOKEN: 'token',
39
+ CLOUDFLARE_ACCOUNT_ID: 'account',
40
+ S3_ACCESS_KEY_ID: 'key',
41
+ S3_SECRET_ACCESS_KEY: 'secret',
42
+ S3_BUCKET: 'bucket',
43
+ });
44
+ expect(config.s3).toBe(true);
45
+ expect(config.encryption).toBe(false);
46
+ expect(config.errors.join('\n')).toMatch(/BACKUP_ENCRYPTION_KEY is required/);
47
+ });
48
+
49
+ it('fails a destination with no Cloudflare credentials', () => {
50
+ expect(evaluateConfig({ BACKUP_ENCRYPTION_KEY: 'k', WEBDAV_URL: 'https://dav.example.com', WEBDAV_USER: 'u', WEBDAV_PASSWORD: 'p' }).errors).toStrictEqual(
51
+ ['A backup destination is configured but CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID are missing.'],
52
+ );
53
+ });
54
+
55
+ it('passes when both a destination and the key are present', () => {
56
+ const config = evaluateConfig({
57
+ CLOUDFLARE_API_TOKEN: 'token',
58
+ CLOUDFLARE_ACCOUNT_ID: 'account',
59
+ BACKUP_ENCRYPTION_KEY: 'key',
60
+ WEBDAV_URL: 'https://dav.example.com',
61
+ WEBDAV_USER: 'u',
62
+ WEBDAV_PASSWORD: 'p',
63
+ });
64
+ expect(config.errors).toStrictEqual([]);
65
+ expect(config.webdav).toBe(true);
66
+ expect(config.s3).toBe(false);
67
+ });
68
+
69
+ it('treats whitespace-only secrets as absent', () => {
70
+ expect(evaluateConfig({ S3_ACCESS_KEY_ID: ' ', S3_SECRET_ACCESS_KEY: ' ', S3_BUCKET: '\t' }).s3).toBe(false);
71
+ });
72
+ });
73
+
74
+ describe('naming', () => {
75
+ it('names the artifact sortably, in UTC', () => {
76
+ // UTC rather than local: a retention window computed across a DST boundary is
77
+ // one day wide in one direction, and a backup pruned a day early is a backup
78
+ // that was never needed.
79
+ const stamp = backupStamp(new Date('2026-10-01T04:15:00.000Z'));
80
+ expect(stamp).toBe('2026-10-01_04-15-00');
81
+ expect(backupFileName(new Date('2026-10-01T04:15:00.000Z'))).toBe('backup_prod_2026-10-01_04-15-00.sql.xz.enc');
82
+ });
83
+
84
+ it('computes a retention cutoff as a date, days back', () => {
85
+ expect(retentionCutoff(new Date('2026-10-31T12:00:00.000Z'), 30)).toBe('2026-10-01');
86
+ });
87
+
88
+ it('rejects a bad retention window rather than defaulting one', () => {
89
+ // Silently falling back would either prune everything or keep backups forever.
90
+ const before = process.env['BACKUP_RETENTION_DAYS'];
91
+ try {
92
+ process.env['BACKUP_RETENTION_DAYS'] = '30d';
93
+ expect(() => requireRetentionDays()).toThrow(/must be a positive number/);
94
+ process.env['BACKUP_RETENTION_DAYS'] = '-1';
95
+ expect(() => requireRetentionDays()).toThrow(/must be a positive number/);
96
+ delete process.env['BACKUP_RETENTION_DAYS'];
97
+ expect(() => requireRetentionDays()).toThrow(/must be a positive number/);
98
+ process.env['BACKUP_RETENTION_DAYS'] = '30';
99
+ expect(requireRetentionDays()).toBe(30);
100
+ } finally {
101
+ if (before === undefined) delete process.env['BACKUP_RETENTION_DAYS'];
102
+ else process.env['BACKUP_RETENTION_DAYS'] = before;
103
+ }
104
+ });
105
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Choosing which D1 database the backup workflow exports.
3
+ *
4
+ * Provenance: ported from Edge-Sonic's `scripts/backup/d1-target.ts`. Pure, so the
5
+ * empty-database guard is asserted without running Actions or touching a real
6
+ * account.
7
+ */
8
+
9
+ import { DEFAULT_UUID } from '../wrangler-config/types';
10
+
11
+ /** A D1 binding as a parsed wrangler config carries one. */
12
+ export interface D1DatabaseBinding {
13
+ binding?: string;
14
+ database_id?: string;
15
+ database_name?: string;
16
+ }
17
+
18
+ /** The resolved export target. */
19
+ export interface D1Target {
20
+ /** The binding name, used for log output and as the export fallback. */
21
+ binding: string;
22
+ /** The database id, verified against Cloudflare by the caller. */
23
+ databaseId: string;
24
+ /** What `wrangler d1 export` should be pointed at. */
25
+ target: string;
26
+ }
27
+
28
+ /**
29
+ * Pick the export target from a parsed wrangler config.
30
+ *
31
+ * `wrangler d1 export` takes a database *name*, so `database_name` wins when
32
+ * present. Hand-written configs that omit it fall back to the binding name.
33
+ *
34
+ * @throws when there is no binding, or when `database_id` is still the template
35
+ * placeholder — meaning the database was auto-provisioned and holds no data.
36
+ */
37
+ export function resolveD1Target(config: { d1_databases?: D1DatabaseBinding[] }): D1Target {
38
+ const database = config.d1_databases?.[0];
39
+
40
+ const binding = database?.binding?.trim();
41
+ if (binding === undefined || binding === '') {
42
+ throw new Error('No d1_databases entry with a binding found in wrangler.jsonc.');
43
+ }
44
+
45
+ const databaseId = database?.database_id?.trim() ?? '';
46
+ if (databaseId === DEFAULT_UUID) {
47
+ throw new Error(
48
+ `D1 database_id for ${binding} is still the template placeholder. The database does not exist yet, so there is nothing to back up.`,
49
+ );
50
+ }
51
+
52
+ const target = database?.database_name?.trim() || binding;
53
+ return { binding, databaseId, target };
54
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Destination and encryption policy for the D1 backup workflow.
3
+ *
4
+ * Provenance: ported from Edge-Sonic's `scripts/backup/destination-config.ts`.
5
+ * Pure and side-effect free, so the fail-closed rules are asserted without running
6
+ * Actions.
7
+ */
8
+
9
+ /** The environment the `check-secrets` job is given, as repository secrets. */
10
+ export interface BackupSecretEnvironment {
11
+ CLOUDFLARE_API_TOKEN?: string;
12
+ CLOUDFLARE_ACCOUNT_ID?: string;
13
+ BACKUP_ENCRYPTION_KEY?: string;
14
+ S3_ACCESS_KEY_ID?: string;
15
+ S3_SECRET_ACCESS_KEY?: string;
16
+ S3_BUCKET?: string;
17
+ WEBDAV_URL?: string;
18
+ WEBDAV_USER?: string;
19
+ WEBDAV_PASSWORD?: string;
20
+ }
21
+
22
+ /** What is configured, and why the run cannot proceed when it is not. */
23
+ export interface DestinationConfig {
24
+ /** Cloudflare credentials present, so the export job can run. */
25
+ cloudflare: boolean;
26
+ /** An encryption passphrase is available. */
27
+ encryption: boolean;
28
+ /** At least one upload destination is configured. */
29
+ anyDestination: boolean;
30
+ /** The S3 destination is fully configured. */
31
+ s3: boolean;
32
+ /** The WebDAV destination is fully configured. */
33
+ webdav: boolean;
34
+ /** Human-readable reasons the run cannot proceed, empty when it can. */
35
+ errors: string[];
36
+ }
37
+
38
+ /** A secret counts as set only when it has non-whitespace content. */
39
+ function isSet(value: string | undefined): boolean {
40
+ return typeof value === 'string' && value.trim().length > 0;
41
+ }
42
+
43
+ /**
44
+ * Decides which destinations are configured and enforces fail-closed encryption.
45
+ *
46
+ * ### Why the encryption requirement is not optional here
47
+ *
48
+ * Two independent reasons, and they have different consequences so both are stated.
49
+ *
50
+ * **The at-rest protection assumes the dump is not in hand.** Every repo in this
51
+ * family seals credentials with AES-256-GCM, and each one's migration notes say the
52
+ * same thing: *obfuscation against a D1 dump, not protection against a full worker
53
+ * compromise.* An unencrypted backup inverts that assumption — dump plus the
54
+ * Secrets Store key is a complete credential set, and the WebDAV key is read on
55
+ * every scan and every stream.
56
+ *
57
+ * **The dump is a listening history.** Stars, ratings, play counts, play queues and
58
+ * auth failures record what each user does and when, and the schema records the
59
+ * full topology and every file path. None of that is credential material, and all
60
+ * of it is nobody else's to read.
61
+ *
62
+ * `S3_REGION` is deliberately not part of the S3 gate. R2 and MinIO have no
63
+ * meaningful region — Cloudflare documents an R2 bucket's region as `auto`, with
64
+ * empty and `us-east-1` both aliasing to it — so requiring the secret would
65
+ * silently disable backups for anyone using such a provider. The upload script
66
+ * defaults `AWS_DEFAULT_REGION` to `auto` instead.
67
+ */
68
+ export function evaluateConfig(env: BackupSecretEnvironment): DestinationConfig {
69
+ const cloudflare = isSet(env.CLOUDFLARE_API_TOKEN) && isSet(env.CLOUDFLARE_ACCOUNT_ID);
70
+ const encryption = isSet(env.BACKUP_ENCRYPTION_KEY);
71
+ const s3 = isSet(env.S3_ACCESS_KEY_ID) && isSet(env.S3_SECRET_ACCESS_KEY) && isSet(env.S3_BUCKET);
72
+ const webdav = isSet(env.WEBDAV_URL) && isSet(env.WEBDAV_USER) && isSet(env.WEBDAV_PASSWORD);
73
+ const anyDestination = s3 || webdav;
74
+
75
+ const errors: string[] = [];
76
+
77
+ // Fail closed, but only once someone has opted into a destination. An
78
+ // unconfigured repository is a legitimate "I do not want off-site backups" state
79
+ // and must not turn the schedule red.
80
+ if (anyDestination) {
81
+ if (!cloudflare) {
82
+ errors.push('A backup destination is configured but CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID are missing.');
83
+ }
84
+ if (!encryption) {
85
+ errors.push(
86
+ 'BACKUP_ENCRYPTION_KEY is required. The D1 dump holds AES-GCM-obfuscated credentials — obfuscation that, by its own design, assumes the dump is not in hand — plus every user history and the full schema topology.',
87
+ );
88
+ }
89
+ }
90
+
91
+ return { cloudflare, encryption, anyDestination, s3, webdav, errors };
92
+ }