From 9e7f65737889dcdbffd966042239d90797bb1ac4 Mon Sep 17 00:00:00 2001 From: Josh Vaughen Date: Mon, 21 Sep 2026 22:27:21 -0700 Subject: [PATCH] docs: the App runs jshvn repos only; katoptra has its own scheduler The katoptra mirrors moved to katoptra/dispatch on 2026-09-21, so the setup no longer installs this App on that organization and the add-a-target notes no longer say the org is covered. The test fixtures use a fictional second owner for the two-installation case instead of katoptra repository names. --- CLAUDE.md | 9 ++++----- README.md | 12 +++++------- test/github.test.ts | 16 ++++++++-------- test/index.test.ts | 26 +++++++++++++------------- 4 files changed, 30 insertions(+), 33 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bdb06f4..575e83c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,9 +29,9 @@ Starts GitHub Actions workflows on a schedule, from a Cloudflare Workflow. GitHu - No secrets in the repo. Worker secrets `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY`, set with `task secrets`. Repo secrets `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`, for deploying. -- One App, installed per account: on the `katoptra` organization with access to all - repositories, and on `jshvn` for the repos there. The installation is resolved from each - target's repo name, so nothing in the Worker names an owner. +- One App, installed on `jshvn` for the repos it runs. The installation is resolved from + each target's repo name, so nothing in the Worker names an owner, and a target under + another account is one more installation, not a code change. - The only network endpoint is `api.github.com`. - No cron parser. Cloudflare parses the expressions; this repo looks up strings. - One expression per slot, never a multi-hour daily. Cloudflare hands the Worker the same @@ -73,8 +73,7 @@ Each of these fails silently, or only in production. subrequests per owner per firing, none per extra target. - **A 404 from the installation lookup means the App is not installed on that repo's owner.** The step fails for good with `App is not installed on /`. A repo - created under `katoptra` is covered by that org's all-repositories install; one under - `jshvn` has to be added to that installation by hand. + has to be added to the installation by hand. - **A dispatch names a ref, and an unnamed one is `main`, not the repo's default branch.** `dispatchWorkflow` sends `ref: target.ref ?? "main"`; GitHub requires the field, so there is no asking it for the default. Every target is on `main`, which is what makes the default diff --git a/README.md b/README.md index 33a0c8f..c4e5fbe 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,9 @@ Once, by hand. 1. Create an App under `jshvn` with one permission -- Repository permissions -> Actions -> Read and write -- and no webhook. -2. Install it on the `katoptra` organization with access to all repositories, so a repo - created or transferred there is covered without another step, and on `jshvn` for the - repos that stay there. The Worker finds each target's installation from its repo name at - dispatch time, so there is no installation ID to note. +2. Install it on `jshvn` with access to the repositories it runs. The Worker finds each + target's installation from its repo name at dispatch time, so there is no installation + ID to note, and a target under another account only needs the App installed there. 3. Note the App ID from the App's settings page. 4. Generate a private key and convert it. GitHub issues PKCS#1; WebCrypto imports PKCS#8 only. @@ -85,9 +84,8 @@ To add one: - Write `schedules/.ts` and add its import to `schedules/index.ts`. - `task crons`, if the slot had no target before. -- Under `katoptra` the App already covers it. Under `jshvn`, add the repo to the App's - installation there. A repo the App cannot see fails its step for good with - `App is not installed on /`, which `task inspect` shows. +- Add the repo to the App's installation. A repo the App cannot see fails its step for + good with `App is not installed on /`, which `task inspect` shows. - Check three things in the target's own workflow. Nothing here can, and a target failing any of them is dispatched into silence: - `workflow_dispatch:` in its `on:` block, or the dispatch 404s. diff --git a/test/github.test.ts b/test/github.test.ts index 5835868..b16bb29 100644 --- a/test/github.test.ts +++ b/test/github.test.ts @@ -72,10 +72,10 @@ const responding = (status: number, body: string | null = null) => { describe("repoInstallation", () => { it("asks for the repo's installation with the App JWT and returns its id", async () => { const { calls, fetch } = responding(200, JSON.stringify({ id: 12345678 })) - const id = await repoInstallation("jwt", "katoptra/tlnet", fetch) + const id = await repoInstallation("jwt", "acme/alpha", fetch) expect(id).toBe("12345678") - expect(calls[0]?.url).toBe("https://api.github.com/repos/katoptra/tlnet/installation") + expect(calls[0]?.url).toBe("https://api.github.com/repos/acme/alpha/installation") expect(calls[0]?.init.method).toBeUndefined() expect((calls[0]?.init.headers as Record).authorization).toBe("Bearer jwt") }) @@ -84,33 +84,33 @@ describe("repoInstallation", () => { // this repo's owner is not one of them. isFatal makes it a hard failure. it("names the repo and fails for good when the App is not installed there", async () => { const { fetch } = responding(404, "Not Found") - const err = await repoInstallation("jwt", "katoptra/new-mirror", fetch).catch((e) => e) - expect(err.message).toMatch(/App is not installed on katoptra\/new-mirror/) + const err = await repoInstallation("jwt", "acme/new-repo", fetch).catch((e) => e) + expect(err.message).toMatch(/App is not installed on acme\/new-repo/) expect(isFatal(err)).toBe(true) }) it("leaves a server error retryable", async () => { const { fetch } = responding(502, "bad gateway") - const err = await repoInstallation("jwt", "katoptra/tlnet", fetch).catch((e) => e) + const err = await repoInstallation("jwt", "acme/alpha", fetch).catch((e) => e) expect(err.message).toMatch(/502/) expect(isFatal(err)).toBe(false) }) it("throws when the answer carries no id", async () => { const { fetch } = responding(200, "{}") - await expect(repoInstallation("jwt", "katoptra/tlnet", fetch)).rejects.toThrow(/no id/) + await expect(repoInstallation("jwt", "acme/alpha", fetch)).rejects.toThrow(/no id/) }) }) describe("dispatchWorkflow", () => { - const target = { repo: "jshvn/ctan", workflow: "sync.yml" } + const target = { repo: "jshvn/terraform", workflow: "sync.yml" } it("posts to the dispatches endpoint, defaulting the ref to main", async () => { const { calls, fetch } = responding(204) await dispatchWorkflow("tok", target, fetch) expect(calls[0]?.url).toBe( - "https://api.github.com/repos/jshvn/ctan/actions/workflows/sync.yml/dispatches", + "https://api.github.com/repos/jshvn/terraform/actions/workflows/sync.yml/dispatches", ) expect(calls[0]?.init.method).toBe("POST") expect(JSON.parse(calls[0]?.init.body as string)).toEqual({ ref: "main", inputs: {} }) diff --git a/test/index.test.ts b/test/index.test.ts index 5a69966..1d43f7d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -243,27 +243,27 @@ describe("Dispatch.run", () => { }) it("looks up and mints once for a burst of targets under one owner", async () => { - await dispatchTo("katoptra/tlnet", "katoptra/ctan") + await dispatchTo("acme/alpha", "acme/beta") expect(seen()).toEqual({ - lookedUp: ["katoptra/tlnet"], - minted: ["inst-katoptra"], + lookedUp: ["acme/alpha"], + minted: ["inst-acme"], dispatched: [ - ["tok-inst-katoptra", "katoptra/tlnet"], - ["tok-inst-katoptra", "katoptra/ctan"], + ["tok-inst-acme", "acme/alpha"], + ["tok-inst-acme", "acme/beta"], ], }) }) it("keeps two owners on two installations and two tokens", async () => { - await dispatchTo("jshvn/ctan", "katoptra/tlnet") + await dispatchTo("jshvn/terraform", "acme/alpha") expect(seen()).toEqual({ - lookedUp: ["jshvn/ctan", "katoptra/tlnet"], - minted: ["inst-jshvn", "inst-katoptra"], + lookedUp: ["jshvn/terraform", "acme/alpha"], + minted: ["inst-jshvn", "inst-acme"], dispatched: [ - ["tok-inst-jshvn", "jshvn/ctan"], - ["tok-inst-katoptra", "katoptra/tlnet"], + ["tok-inst-jshvn", "jshvn/terraform"], + ["tok-inst-acme", "acme/alpha"], ], }) }) @@ -272,14 +272,14 @@ describe("Dispatch.run", () => { // to a hard failure. The message has to name the repo, since that is the whole diagnosis. it("fails the step for good when the App is not installed on the repo", async () => { vi.mocked(github.repoInstallation).mockRejectedValueOnce( - new Error("App is not installed on katoptra/new-mirror"), + new Error("App is not installed on acme/new-repo"), ) vi.mocked(github.isFatal).mockReturnValueOnce(true) - const err = await dispatchTo("katoptra/new-mirror").catch((e) => e) + const err = await dispatchTo("acme/new-repo").catch((e) => e) expect(err).toBeInstanceOf(NonRetryableError) - expect(err.message).toMatch(/not installed on katoptra\/new-mirror/) + expect(err.message).toMatch(/not installed on acme\/new-repo/) expect(seen().dispatched).toEqual([]) }) })