@ibalzam/codejitsu-core 0.12.0 → 0.13.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.
@@ -0,0 +1,56 @@
1
+ # 0.13.0 — Daily deploy supports multi-site monorepos
2
+
3
+ ## Summary
4
+
5
+ The deploy module now drives **every** Cloudflare Pages project in a repo, not just one. Previously the `daily-deploy.yml` workflow pinged a single deploy hook (`CLOUDFLARE_DEPLOY_HOOK_URL`), so a monorepo that deploys several Pages projects (e.g. `sites/www`, `sites/kamloops`, `sites/kelowna`) only ever rebuilt one of them on the morning cron — the others never graduated their scheduled/future-dated posts.
6
+
7
+ A new secret, `CLOUDFLARE_DEPLOY_HOOK_URLS`, holds every project's deploy hook URL (comma-separated). The workflow prefers it and pings each URL. Single-site repos are unchanged: `CLOUDFLARE_DEPLOY_HOOK_URL` still works and is still what `deploy:setup` writes when you give it one URL.
8
+
9
+ This is backward compatible — **no action needed for single-site repos**. Action is only required if one git repo deploys to **more than one** Cloudflare Pages project.
10
+
11
+ ## Required actions
12
+
13
+ ### Single-site repo (one Pages project)
14
+
15
+ Nothing to do. If you re-copy the workflow template, the behaviour is identical.
16
+
17
+ ### Multi-site monorepo (one repo → several Pages projects)
18
+
19
+ This is the case the change exists for. Symptom: only one site publishes scheduled content each morning; the others look stale until something else triggers a build.
20
+
21
+ 1. **Update the workflow file.** Re-copy the template so it loops over multiple hooks:
22
+
23
+ ```
24
+ cp node_modules/@ibalzam/codejitsu-core/modules/deploy/templates/daily-deploy.yml \
25
+ .github/workflows/daily-deploy.yml
26
+ ```
27
+
28
+ (Or run `npx codejitsu deploy:setup`, which copies it for you if missing.)
29
+
30
+ 2. **Create one Cloudflare deploy hook per Pages project** — dashboard → each project → Settings → Builds & deployments → Deploy hooks, branch `main`.
31
+
32
+ 3. **Store all the hook URLs in `CLOUDFLARE_DEPLOY_HOOK_URLS`, comma-separated.** Easiest via the wizard:
33
+
34
+ ```
35
+ npx codejitsu deploy:setup
36
+ ```
37
+
38
+ When it prompts, paste every project's URL comma-separated. It writes the plural secret (and removes the now-redundant singular one). Or set it directly:
39
+
40
+ ```
41
+ gh secret set CLOUDFLARE_DEPLOY_HOOK_URLS \
42
+ --body "https://api.cloudflare.com/.../hook-a,https://api.cloudflare.com/.../hook-b,https://api.cloudflare.com/.../hook-c"
43
+ ```
44
+
45
+ 4. **Adding a site later:** create its deploy hook and append `,<new-url>` to `CLOUDFLARE_DEPLOY_HOOK_URLS`. No workflow edit needed.
46
+
47
+ ## Verify
48
+
49
+ ```bash
50
+ npm update @ibalzam/codejitsu-core
51
+ npx codejitsu deploy:run # dispatch the workflow once
52
+ ```
53
+
54
+ Then confirm a deployment kicks off in **every** Pages project, not just one. `npx codejitsu audit` now passes when either `CLOUDFLARE_DEPLOY_HOOK_URL` or `CLOUDFLARE_DEPLOY_HOOK_URLS` is set.
55
+
56
+ See `modules/deploy/CLAUDE.md` → "Multi-site monorepos" for the full reference.
package/SETUP.md CHANGED
@@ -393,7 +393,11 @@ npx codejitsu deploy:setup
393
393
  Interactive wizard:
394
394
  - Copies `.github/workflows/daily-deploy.yml` + `wrangler.toml` from
395
395
  package templates if missing.
396
- - Prompts for the Cloudflare deploy hook URL.
396
+ - Prompts for the Cloudflare deploy hook URL(s). For a multi-site monorepo
397
+ (one repo → several Pages projects), enter every project's hook URL
398
+ comma-separated; they're stored in `CLOUDFLARE_DEPLOY_HOOK_URLS` and the
399
+ workflow pings each. A single site uses `CLOUDFLARE_DEPLOY_HOOK_URL`.
400
+ See `modules/deploy/CLAUDE.md` → "Multi-site monorepos".
397
401
  - Stores it as a GH Actions secret via `gh secret set`.
398
402
  - Optionally triggers a test run.
399
403
 
package/bin/codejitsu.mjs CHANGED
@@ -73,7 +73,7 @@ function printHelp() {
73
73
  console.log(` blog:init Install /blog, /blog-batch, /blog-images slash commands`);
74
74
  console.log(` blog:selftest Cold-Claude write a throwaway post + grade it. Flags: --topic, --model`);
75
75
  console.log(``);
76
- console.log(` deploy:setup Wire up daily Cloudflare deploy (prompts for hook URL)`);
76
+ console.log(` deploy:setup Wire up daily Cloudflare deploy (prompts for hook URL(s); multi-site aware)`);
77
77
  console.log(` deploy:run Trigger the Daily Deploy workflow once now`);
78
78
  console.log(``);
79
79
  console.log(` doctor Check Node + dependency versions are current (run BEFORE upgrading or scaffolding)`);
package/checklist/core.md CHANGED
@@ -9,7 +9,7 @@ The runner covers the easy stuff (file presence, meta tags, canonical, schema sc
9
9
  - [ ] `npm run build` exits 0 with no warnings about missing pages, unresolved imports, or broken links.
10
10
  - [ ] `dist/` contains static HTML for every expected route. No `.html` route is missing.
11
11
  - [ ] `wrangler.toml` is present and points to `dist`.
12
- - [ ] `.github/workflows/daily-deploy.yml` exists; the `CLOUDFLARE_DEPLOY_HOOK_URL` secret is set in the repo (skip if site has no scheduled content).
12
+ - [ ] `.github/workflows/daily-deploy.yml` exists; the deploy-hook secret is set in the repo — `CLOUDFLARE_DEPLOY_HOOK_URL` (single site) or `CLOUDFLARE_DEPLOY_HOOK_URLS` (comma-separated, one Deploy Hook per Pages project, for a multi-site monorepo). Skip if the site has no scheduled content.
13
13
 
14
14
  ## URLs + routing
15
15
 
@@ -75,12 +75,15 @@ export async function runStructure(ctx) {
75
75
  const secrets = await listSecrets(repo);
76
76
  if (secrets === null) {
77
77
  results.push(info('Skipped GH secret check (gh not authenticated for this repo)'));
78
+ } else if (secrets.includes('CLOUDFLARE_DEPLOY_HOOK_URLS')) {
79
+ results.push(pass(`CLOUDFLARE_DEPLOY_HOOK_URLS set in ${repo} (multi-site)`));
78
80
  } else if (secrets.includes('CLOUDFLARE_DEPLOY_HOOK_URL')) {
79
81
  results.push(pass(`CLOUDFLARE_DEPLOY_HOOK_URL set in ${repo}`));
80
82
  } else {
81
83
  results.push(warn(
82
- 'CLOUDFLARE_DEPLOY_HOOK_URL secret missing',
83
- 'Daily deploy workflow will fail. Run `npx codejitsu deploy:setup` to configure.'
84
+ 'Deploy hook secret missing (CLOUDFLARE_DEPLOY_HOOK_URL or _URLS)',
85
+ 'Daily deploy workflow will fail. Run `npx codejitsu deploy:setup` to configure. ' +
86
+ 'For a multi-site monorepo, set CLOUDFLARE_DEPLOY_HOOK_URLS (one hook per Pages project, comma-separated).'
84
87
  ));
85
88
  }
86
89
  }
@@ -93,13 +93,19 @@ export async function runDeploySetup() {
93
93
  }
94
94
 
95
95
  // ─── GitHub secret ───────────────────────────────────────────────────
96
+ // Single site → CLOUDFLARE_DEPLOY_HOOK_URL. Multi-site monorepo (one repo,
97
+ // several Pages projects) → CLOUDFLARE_DEPLOY_HOOK_URLS holding every
98
+ // project's hook, comma-separated. The daily-deploy workflow prefers the
99
+ // plural secret and pings each URL.
96
100
  console.log('\nChecking GitHub Actions secrets…');
97
101
  const secrets = await listSecrets(repo);
98
- const hasSecret = secrets.includes('CLOUDFLARE_DEPLOY_HOOK_URL');
102
+ const hasSingular = secrets.includes('CLOUDFLARE_DEPLOY_HOOK_URL');
103
+ const hasPlural = secrets.includes('CLOUDFLARE_DEPLOY_HOOK_URLS');
99
104
 
100
- if (hasSecret) {
101
- console.log(c.green('') + ' CLOUDFLARE_DEPLOY_HOOK_URL is set');
102
- const rotate = await prompt('\nRotate the deploy hook URL? [y/N]: ');
105
+ if (hasSingular || hasPlural) {
106
+ const which = hasPlural ? 'CLOUDFLARE_DEPLOY_HOOK_URLS' : 'CLOUDFLARE_DEPLOY_HOOK_URL';
107
+ console.log(c.green('✓') + ` ${which} is set`);
108
+ const rotate = await prompt('\nRotate / re-enter the deploy hook URL(s)? [y/N]: ');
103
109
  if (!/^y(es)?$/i.test(rotate.trim())) {
104
110
  console.log(c.gray('\nNothing to do. The daily deploy is configured.\n'));
105
111
  await offerTestRun(repo);
@@ -107,28 +113,51 @@ export async function runDeploySetup() {
107
113
  }
108
114
  }
109
115
 
110
- // Prompt for URL.
116
+ // Prompt for URL(s).
111
117
  console.log('');
112
- console.log('Get a deploy hook URL from Cloudflare Pages:');
118
+ console.log('Get a deploy hook URL from Cloudflare Pages (one per Pages project):');
113
119
  console.log(c.gray(' 1. Open Cloudflare dashboard → Pages → ' + (pagesName ?? 'your project') + ' → Settings'));
114
120
  console.log(c.gray(' 2. Builds & deployments → Deploy hooks → "Add deploy hook"'));
115
121
  console.log(c.gray(' 3. Name: "daily-scheduled-content" — Branch: main'));
116
122
  console.log(c.gray(' 4. Copy the URL'));
123
+ console.log(c.gray(' Multi-site monorepo: repeat for every Pages project and paste all URLs comma-separated.'));
117
124
  console.log('');
118
125
 
119
- const url = (await prompt('Paste the deploy hook URL: ')).trim();
120
- if (!url.startsWith('https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/')) {
121
- console.error(c.red('\n✗ That doesn\'t look like a Cloudflare Pages deploy hook URL.'));
122
- console.error(' Expected prefix: https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/');
126
+ const raw = (await prompt('Paste the deploy hook URL(s), comma-separated: ')).trim();
127
+ const urls = raw.split(',').map((u) => u.trim()).filter(Boolean);
128
+ const prefix = 'https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/';
129
+ if (urls.length === 0) {
130
+ console.error(c.red('\n✗ No deploy hook URL provided.'));
131
+ process.exit(1);
132
+ }
133
+ const bad = urls.filter((u) => !u.startsWith(prefix));
134
+ if (bad.length > 0) {
135
+ console.error(c.red('\n✗ These don\'t look like Cloudflare Pages deploy hook URLs:'));
136
+ bad.forEach((u) => console.error(' ' + u));
137
+ console.error(' Expected prefix: ' + prefix);
123
138
  process.exit(1);
124
139
  }
125
140
 
126
- const setOk = await setSecret(repo, 'CLOUDFLARE_DEPLOY_HOOK_URL', url);
141
+ // One URL singular secret (back-compat). Multiple → plural secret.
142
+ const secretName = urls.length > 1 ? 'CLOUDFLARE_DEPLOY_HOOK_URLS' : 'CLOUDFLARE_DEPLOY_HOOK_URL';
143
+ const setOk = await setSecret(repo, secretName, urls.join(','));
127
144
  if (!setOk) {
128
145
  console.error(c.red('\n✗ Failed to set GitHub secret.'));
129
146
  process.exit(1);
130
147
  }
131
- console.log(c.green('✓') + ` Secret CLOUDFLARE_DEPLOY_HOOK_URL set in ${repo}`);
148
+ console.log(
149
+ c.green('✓') + ` Secret ${secretName} set in ${repo}` +
150
+ (urls.length > 1 ? ` (${urls.length} hooks)` : '')
151
+ );
152
+
153
+ // Switching singular → plural: the stale singular secret is ignored by the
154
+ // workflow (plural wins) but is confusing. Remove it.
155
+ if (urls.length > 1 && hasSingular) {
156
+ const cleaned = await deleteSecret(repo, 'CLOUDFLARE_DEPLOY_HOOK_URL');
157
+ if (cleaned) {
158
+ console.log(c.gray(' Removed now-redundant CLOUDFLARE_DEPLOY_HOOK_URL (plural secret takes precedence).'));
159
+ }
160
+ }
132
161
 
133
162
  await offerTestRun(repo);
134
163
  }
@@ -236,6 +265,11 @@ async function setSecret(repo, name, value) {
236
265
  return r.code === 0;
237
266
  }
238
267
 
268
+ async function deleteSecret(repo, name) {
269
+ const r = await runGh(['secret', 'delete', name, '--repo', repo]);
270
+ return r.code === 0;
271
+ }
272
+
239
273
  function runGh(args) {
240
274
  return new Promise((resolve) => {
241
275
  const proc = spawn('gh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -5,7 +5,7 @@ When the user asks to **set up codejitsu/core/deploy** (or "wire up the Cloudfla
5
5
  ## What this module provides
6
6
 
7
7
  - `templates/wrangler.toml` — minimal Cloudflare Pages config.
8
- - `templates/daily-deploy.yml` — GitHub Action that pings a Cloudflare deploy hook every morning so scheduled blog posts (or any time-gated content) graduate from hidden to public on their publish date.
8
+ - `templates/daily-deploy.yml` — GitHub Action that pings one or more Cloudflare deploy hooks every morning so scheduled blog posts (or any time-gated content) graduate from hidden to public on their publish date. Handles both single-site repos and multi-site monorepos (see below).
9
9
 
10
10
  ## Wiring it into a site
11
11
 
@@ -30,11 +30,34 @@ gh secret set CLOUDFLARE_DEPLOY_HOOK_URL --body "<paste URL>"
30
30
 
31
31
  Or via the GitHub UI: Settings → Secrets and variables → Actions → New repository secret, named `CLOUDFLARE_DEPLOY_HOOK_URL`.
32
32
 
33
+ > Multi-site monorepo? Skip this step and follow **Multi-site monorepos** below instead.
34
+
33
35
  ### 4. Verify
34
36
 
35
37
  - Trigger the workflow manually: `gh workflow run "Daily Deploy"` (or via the GH UI).
36
38
  - A Cloudflare Pages deployment should kick off within seconds.
37
39
 
40
+ ## Multi-site monorepos
41
+
42
+ A single repo can host several sites (e.g. `sites/www`, `sites/kamloops`, `sites/kelowna`), each deployed as its **own Cloudflare Pages project**. There is still only **one** `.github/workflows/daily-deploy.yml` for the repo, but it must rebuild **every** project — otherwise only one site graduates its scheduled content each morning and the rest stay stale.
43
+
44
+ The template already handles this. To wire it up:
45
+
46
+ 1. **Create one deploy hook per Pages project** (Cloudflare dashboard → each project → Settings → Builds & deployments → Deploy hooks, branch `main`). For garagedoorpros that's three: `garagedoorpros-ca`, `gdp-kamloops`, `gdp-kelowna`.
47
+ 2. **Store all the URLs in a single plural secret, comma-separated** (one line — easiest to set and paste; Cloudflare hook URLs never contain commas):
48
+
49
+ ```bash
50
+ gh secret set CLOUDFLARE_DEPLOY_HOOK_URLS \
51
+ --body "https://api.cloudflare.com/.../www-hook,https://api.cloudflare.com/.../kamloops-hook,https://api.cloudflare.com/.../kelowna-hook"
52
+ ```
53
+
54
+ (Newline- or space-separated values also work — the workflow splits on commas, newlines, and spaces — but commas keep it a single line, which avoids the multi-line `--body` / paste pitfalls.)
55
+ 3. The workflow prefers `CLOUDFLARE_DEPLOY_HOOK_URLS` when present, splits it, and pings each URL. It only falls back to the singular `CLOUDFLARE_DEPLOY_HOOK_URL` when the plural secret is absent — so single-site repos need no change.
56
+
57
+ **Adding a site later:** create its deploy hook, append `,<new-url>` to `CLOUDFLARE_DEPLOY_HOOK_URLS`, done. No workflow edit needed.
58
+
59
+ **Verify:** `gh workflow run "Daily Deploy"` then confirm a deployment kicks off in *every* Pages project, not just one.
60
+
38
61
  ## Build command (for Cloudflare Pages git integration)
39
62
 
40
63
  If using Cloudflare's git integration (preferred for push-driven deploys):
@@ -45,7 +68,8 @@ If using Cloudflare's git integration (preferred for push-driven deploys):
45
68
 
46
69
  ## What must NOT be done
47
70
 
48
- - **Don't commit the deploy hook URL.** It belongs in `CLOUDFLARE_DEPLOY_HOOK_URL` secret only.
71
+ - **Don't commit the deploy hook URL(s).** They belong in the `CLOUDFLARE_DEPLOY_HOOK_URL` / `CLOUDFLARE_DEPLOY_HOOK_URLS` secret only.
72
+ - **Don't add a second daily-deploy workflow for a monorepo's extra sites.** One workflow pings all hooks via `CLOUDFLARE_DEPLOY_HOOK_URLS` — see **Multi-site monorepos**. Duplicate workflows mean duplicate crons and drift.
49
73
  - **Don't change the cron without reason.** 13:00 UTC is intentional — early-morning Pacific so posts are live by the time users wake up. If a site is on a different timezone, change it explicitly and note why in a comment in the workflow file.
50
74
  - **Don't add a `wrangler deploy` step to the cron workflow.** The workflow only *pings* the deploy hook; Cloudflare does the actual build via git integration. Doing the build twice causes drift.
51
75
  - **Don't skip the daily-deploy workflow even if the site has no scheduled content yet.** It's the safety net for when the user adds a scheduled post six months later.
@@ -54,5 +78,5 @@ If using Cloudflare's git integration (preferred for push-driven deploys):
54
78
 
55
79
  - [ ] `wrangler.toml` present at site root with correct `name`.
56
80
  - [ ] `.github/workflows/daily-deploy.yml` present.
57
- - [ ] `CLOUDFLARE_DEPLOY_HOOK_URL` secret set in the repo (check with `gh secret list`).
58
- - [ ] Cloudflare Pages project exists and is connected to the git repo.
81
+ - [ ] Deploy-hook secret set (check with `gh secret list`): `CLOUDFLARE_DEPLOY_HOOK_URL` for a single site, or `CLOUDFLARE_DEPLOY_HOOK_URLS` (one URL per line) for a multi-site monorepo.
82
+ - [ ] A Cloudflare Pages project exists for **each** site and is connected to the git repo, and every project has a hook in the secret.
@@ -3,7 +3,7 @@
3
3
  - [ ] `wrangler.toml` exists at site root with the correct Cloudflare Pages project `name`.
4
4
  - [ ] `pages_build_output_dir = "dist"` in `wrangler.toml`.
5
5
  - [ ] `.github/workflows/daily-deploy.yml` exists and is unmodified from the template (or modifications are documented in a comment).
6
- - [ ] `CLOUDFLARE_DEPLOY_HOOK_URL` secret is set in the repo (`gh secret list`).
7
- - [ ] Cloudflare Pages project exists and is connected to the GitHub repo (git integration).
8
- - [ ] Pages build command is `npm run build`, output dir is `dist`, Node version is 20.
9
- - [ ] Manual run of `gh workflow run "Daily Deploy"` triggers a Cloudflare deployment within seconds.
6
+ - [ ] Deploy-hook secret is set in the repo (`gh secret list`): `CLOUDFLARE_DEPLOY_HOOK_URL` (single site) or `CLOUDFLARE_DEPLOY_HOOK_URLS` (multi-site monorepo, one URL per line).
7
+ - [ ] A Cloudflare Pages project exists for **each** site and is connected to the GitHub repo (git integration); every project's hook is in the secret.
8
+ - [ ] Each Pages project's build command is `npm run build` (for its own site/workspace), output dir is `dist`, Node version is 20.
9
+ - [ ] Manual run of `gh workflow run "Daily Deploy"` triggers a Cloudflare deployment in **every** Pages project within seconds.
@@ -3,9 +3,14 @@ name: Daily Deploy
3
3
  # Triggers a Cloudflare Pages rebuild each morning so any scheduled content
4
4
  # whose publish date has arrived graduates from hidden to public.
5
5
  #
6
- # Requires a repository secret named CLOUDFLARE_DEPLOY_HOOK_URL holding
7
- # the Deploy Hook URL from Cloudflare Pages (Project settings -> Builds &
6
+ # Single-site repo: set the secret CLOUDFLARE_DEPLOY_HOOK_URL to the one
7
+ # Deploy Hook URL from Cloudflare Pages (Project settings -> Builds &
8
8
  # deployments -> Deploy hooks).
9
+ #
10
+ # Multi-site monorepo (one repo -> several Pages projects): set the secret
11
+ # CLOUDFLARE_DEPLOY_HOOK_URLS to ALL the projects' Deploy Hook URLs, separated
12
+ # by commas (newlines also work). The job pings every URL so each site
13
+ # rebuilds. If both secrets are set, the plural one wins.
9
14
 
10
15
  on:
11
16
  schedule:
@@ -18,12 +23,23 @@ jobs:
18
23
  runs-on: ubuntu-latest
19
24
  timeout-minutes: 5
20
25
  steps:
21
- - name: Ping Cloudflare deploy hook
26
+ - name: Ping Cloudflare deploy hook(s)
22
27
  env:
28
+ HOOKS: ${{ secrets.CLOUDFLARE_DEPLOY_HOOK_URLS }}
23
29
  HOOK: ${{ secrets.CLOUDFLARE_DEPLOY_HOOK_URL }}
24
30
  run: |
25
- if [ -z "$HOOK" ]; then
26
- echo "CLOUDFLARE_DEPLOY_HOOK_URL secret is not set"
31
+ # Prefer the plural (multi-URL) secret; fall back to the singular one.
32
+ urls="$HOOKS"
33
+ [ -z "$urls" ] && urls="$HOOK"
34
+ if [ -z "$urls" ]; then
35
+ echo "Neither CLOUDFLARE_DEPLOY_HOOK_URLS nor CLOUDFLARE_DEPLOY_HOOK_URL is set"
27
36
  exit 1
28
37
  fi
29
- curl --fail-with-body -X POST "$HOOK"
38
+ # Accept commas, newlines, or spaces as separators between URLs.
39
+ failed=0
40
+ for url in $(echo "$urls" | tr ',\n' ' '); do
41
+ [ -z "$url" ] && continue
42
+ echo "Pinging deploy hook: ${url%%\?*}"
43
+ curl --fail-with-body -X POST "$url" || failed=1
44
+ done
45
+ exit $failed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibalzam/codejitsu-core",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "description": "Shared core for Codejitsu Astro sites — reusable code and Claude-facing instructions for blog, SEO, images, deploy, and llms.txt.",
6
6
  "keywords": [