Skip to content

Commit b7d2c59

Browse files
pirateclaude
andcommitted
Worker: manifest-based deployed-list (2 subrequests, not N)
With ~80 entries in users.txt, the per-user probe approach blew the Cloudflare Workers 50-subrequests-per-request limit (CF error 1101). Switch to: CI writes /deployed.json (a JSON array of user logins whose dashboard HTML is in /public) before every deploy. Worker reads users.txt + deployed.json (2 subrequests total) and computes the diff. Scales to any users.txt size. regen_manifest() is now called inside the mining-loop's deploy_now() and from the final-deploy step, so the manifest tracks reality even as users complete mid-run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c2e0378 commit b7d2c59

2 files changed

Lines changed: 62 additions & 38 deletions

File tree

.github/workflows/mine-and-deploy.yml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ jobs:
140140
# (before the mining loop) means every interim deploy also has
141141
# a fresh users.txt available to the homepage.
142142
cp cloudflare/users.txt cloudflare/public/users.txt
143+
# Generate deployed.json — a JSON array of user logins whose
144+
# dashboard HTML is currently in /public. Updated on every
145+
# deploy_now() call below so the homepage stays accurate.
146+
ls cloudflare/public/*.html 2>/dev/null \
147+
| sed -E 's|cloudflare/public/||;s|\.html$||' \
148+
| grep -vE '^(index|404)$' \
149+
| python3 -c "import sys,json; print(json.dumps(sorted([l.strip() for l in sys.stdin if l.strip()])))" \
150+
> cloudflare/public/deployed.json
143151
echo "Targets:"
144152
cat /tmp/targets.txt
145153
@@ -156,7 +164,16 @@ jobs:
156164
run: |
157165
set -e
158166
167+
regen_manifest() {
168+
ls cloudflare/public/*.html 2>/dev/null \
169+
| sed -E 's|cloudflare/public/||;s|\.html$||' \
170+
| grep -vE '^(index|404)$' \
171+
| python3 -c "import sys,json; print(json.dumps(sorted([l.strip() for l in sys.stdin if l.strip()])))" \
172+
> cloudflare/public/deployed.json
173+
}
174+
159175
deploy_now() {
176+
regen_manifest
160177
(cd cloudflare && npx --yes wrangler@latest deploy --minify 2>&1 |
161178
tail -2) || echo "::warning::interim deploy failed"
162179
}
@@ -221,8 +238,15 @@ jobs:
221238
git push || echo "::warning::push failed (no commit permission?)"
222239
223240
- name: Final deploy
224-
working-directory: cloudflare
225-
run: npx --yes wrangler@latest deploy
241+
working-directory: .
242+
run: |
243+
# Regenerate deployed.json one last time before the final push.
244+
ls cloudflare/public/*.html 2>/dev/null \
245+
| sed -E 's|cloudflare/public/||;s|\.html$||' \
246+
| grep -vE '^(index|404)$' \
247+
| python3 -c "import sys,json; print(json.dumps(sorted([l.strip() for l in sys.stdin if l.strip()])))" \
248+
> cloudflare/public/deployed.json
249+
cd cloudflare && npx --yes wrangler@latest deploy
226250
env:
227251
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
228252
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

cloudflare/worker/index.ts

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -429,55 +429,55 @@ async function handleStatus(
429429
}
430430

431431

432-
// Dynamic homepage. Reads /users.txt (also deployed as a static asset
433-
// by CI) for the canonical list of users we want dashboards for, then
434-
// probes /{user}.html via the ASSETS binding to see which are ready vs
435-
// still queued/mining. Output is cached in Workers Cache for 30s so
436-
// repeated visits don't fan out to N internal asset probes each time.
432+
// Dynamic homepage. Reads /deployed.json (a CI-generated manifest of
433+
// users whose dashboard HTML actually exists) and /users.txt (the
434+
// canonical list of users we want), then computes deployed vs queued.
435+
// Only 2 subrequests instead of probing each /{user}.html individually
436+
// (which blew the Worker's 50/req subrequest limit at 80+ users).
437437
async function handleIndex(env: Env, url: URL): Promise<Response> {
438438
const cache = caches.default;
439-
const cacheKey = new Request("https://internal-index.invalid/v1");
439+
const cacheKey = new Request("https://internal-index.invalid/v2");
440440
const cached = await cache.match(cacheKey);
441441
if (cached) return cached;
442442

443-
// Read users.txt from the deployed assets.
444443
const probeBase = new URL(url.toString());
445444
probeBase.search = "";
446-
const usersTxtUrl = new URL(probeBase.toString());
447-
usersTxtUrl.pathname = "/users.txt";
448-
let users: string[] = [];
449-
try {
450-
const r = await env.ASSETS.fetch(new Request(usersTxtUrl.toString()));
451-
if (r.ok) {
452-
const txt = await r.text();
453-
users = txt.split("\n")
454-
.map((l) => l.split("#", 1)[0].trim())
455-
.filter((l) => l.length > 0);
456-
}
457-
} catch {}
458445

459-
// Add pirate (intentionally not in users.txt — built locally).
460-
if (!users.includes("pirate")) users.unshift("pirate");
461-
462-
// Probe each user's /<u>.html for deploy status in parallel.
463-
const states = await Promise.all(users.map(async (u) => {
464-
const probeUrl = new URL(probeBase.toString());
465-
probeUrl.pathname = `/${u}.html`;
446+
async function fetchAsset(path: string): Promise<string | null> {
466447
try {
467-
const r = await env.ASSETS.fetch(new Request(probeUrl.toString()));
468-
return { user: u, deployed: r.status === 200 };
448+
const u = new URL(probeBase.toString());
449+
u.pathname = path;
450+
const r = await env.ASSETS.fetch(new Request(u.toString()));
451+
if (!r.ok) return null;
452+
return await r.text();
469453
} catch {
470-
return { user: u, deployed: false };
454+
return null;
471455
}
472-
}));
456+
}
457+
458+
// Parse users.txt — comment-aware, one login per line.
459+
const usersTxt = await fetchAsset("/users.txt");
460+
const wanted: string[] = (usersTxt ?? "")
461+
.split("\n")
462+
.map((l) => l.split("#", 1)[0].trim())
463+
.filter((l) => l.length > 0);
464+
465+
// Parse deployed.json — JSON array of logins.
466+
let deployedSet = new Set<string>();
467+
const depJson = await fetchAsset("/deployed.json");
468+
if (depJson) {
469+
try {
470+
const arr = JSON.parse(depJson) as string[];
471+
if (Array.isArray(arr)) deployedSet = new Set(arr);
472+
} catch {}
473+
}
474+
// Pirate is always deployed (committed in /public).
475+
deployedSet.add("pirate");
473476

474-
const deployed = states
475-
.filter((s) => s.deployed)
476-
.map((s) => s.user)
477+
const allUsers = new Set<string>([...wanted, ...deployedSet]);
478+
const deployed = [...allUsers].filter((u) => deployedSet.has(u))
477479
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
478-
const queued = states
479-
.filter((s) => !s.deployed)
480-
.map((s) => s.user)
480+
const queued = [...allUsers].filter((u) => !deployedSet.has(u))
481481
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
482482

483483
const html = indexPage(deployed, queued);

0 commit comments

Comments
 (0)