Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silly-moments-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"github-actions-cloudflare-pages": minor
---

feat: use npm package openapi-fetch and generate TypeScript types from Cloudflare OpenAPI schema
37 changes: 20 additions & 17 deletions AGENTS.md → .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Non-negotiable. Violating these breaks the build or the type system.
5. **No `console.log`** — use `@actions/core` methods (`info`, `debug`, `warning`, `error`, `setFailed`).
6. **Touch an exported function → update its tests.** Tests for `bin/` scripts live in `__tests__/scripts/` (NOT `__tests__/bin/`, which vitest excludes).
7. **Change a GraphQL selection set → update every test mock** for that operation (`grep` the operation name across `__tests__/`; multiple files may mock it).
8. **Run scripts with the right runner**: `node` for scripts importing only `node:*` / relative / `package.json`; `tsx` for scripts importing via `@/` aliases (see [Build & Tooling](#build--tooling)).
8. **Run scripts with the right runner**: `node` runs scripts whose only **runtime** imports are `node:*` / relative / `package.json` / npm packages / type-only `@/` aliases (type-only imports are erased, so they're fine). `tsx` is required for any script that **transitively imports the generated GraphQL client** (`__generated__/gql/`) — see [Build & Tooling](#build--tooling).

## Architecture

Expand All @@ -30,22 +30,23 @@ Non-negotiable. Violating these breaks the build or the type system.

**GraphQL type safety**: Inline ``graphql(/* GraphQL */ `...`)`` operations in `src/**` and `bin/**` are typed via [@graphql-codegen/client-preset](graphql.config.ts). The custom client [src/common/github/api/client.ts](src/common/github/api/client.ts) wraps fetch with `TypedDocumentString` for compile-time validation. Preview features come from [schema/github/schema.graphql](schema/github/schema.graphql).

**Cloudflare**: Runs `wrangler pages deploy` via `execAsync()` ([src/common/cloudflare/deployment/create.ts](src/common/cloudflare/deployment/create.ts#L49-L54)). Wrangler is external to the bundle (esbuild `external`, [esbuild.config.js](esbuild.config.js)) and installed at runtime via `npx wrangler@<version>` — the version comes from the `wrangler-version` input or, failing that, the default in [src/common/inputs.ts](src/common/inputs.ts), which [bin/sync-versions.ts](bin/sync-versions.ts) keeps in lockstep with `devDependencies.wrangler` (the single source of truth; tests read it too). Deployment status polling and deletion use Cloudflare's REST API.
**Cloudflare**: Runs `wrangler pages deploy` via `execAsync()` ([src/common/cloudflare/deployment/create.ts](src/common/cloudflare/deployment/create.ts#L49-L54)). Wrangler is external to the bundle (esbuild `external`, [esbuild.config.js](esbuild.config.js)) and installed at runtime via `npx wrangler@<version>` — the version comes from the `wrangler-version` input or, failing that, the default in [src/common/inputs.ts](src/common/inputs.ts), which [bin/sync-versions.ts](bin/sync-versions.ts) keeps in lockstep with `devDependencies.wrangler` (the single source of truth; tests read it too). Deployment status polling and deletion use Cloudflare's REST API via the typed [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) client [src/common/cloudflare/api/client.ts](src/common/cloudflare/api/client.ts) (paths/params/responses inferred from the generated `paths`); the Cloudflare `{success, result, errors}` envelope is unwrapped by `unwrap`/`unwrapSuccess` in [src/common/cloudflare/api/fetch-result.ts](src/common/cloudflare/api/fetch-result.ts), which is where the envelope error handling lives. [src/common/cloudflare/api/endpoints.ts](src/common/cloudflare/api/endpoints.ts) now only builds the `dash.cloudflare.com` log URL.

## Commands

| Command | Purpose |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `pnpm run all` | Full validation: sync-versions → knip → codegen → codegen:events → tsc → format → lint → test → build |
| `pnpm run build` | ESBuild bundle to `dist/deploy` & `dist/delete` |
| `pnpm run codegen` | Regenerate GraphQL types in [`__generated__/gql/`](__generated__/gql/) from inline `graphql()` calls |
| `pnpm run codegen:events` | Generate GitHub event types via [bin/codegen/index.ts](bin/codegen/index.ts) from `@octokit/webhooks-schemas` |
| `pnpm run codegen:watch` | Auto-regenerate types on GraphQL changes |
| `pnpm run tsc:check` | Type-check (`tsc --noEmit --checkJs`) |
| `pnpm run test` / `test:watch` | Vitest run / interactive |
| `pnpm run start` | Run the built action locally (needs local env vars) |
| `pnpm run act:d` | Test the delete action locally with `act` |
| `pnpm changeset` | Record a changeset for notable/breaking changes |
| Command | Purpose |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm run all` | Full validation: sync-versions → knip → codegen → codegen:events → codegen:cloudflare → tsc → format → lint → test → build |
| `pnpm run build` | ESBuild bundle to `dist/deploy` & `dist/delete` |
| `pnpm run codegen` | Regenerate GraphQL types in [`__generated__/gql/`](__generated__/gql/) from inline `graphql()` calls |
| `pnpm run codegen:cloudflare` | Generate Cloudflare Pages types via [bin/codegen/cloudflare-pages.ts](bin/codegen/cloudflare-pages.ts) |
| `pnpm run codegen:events` | Generate GitHub event types via [bin/codegen/github-workflow-events.ts](bin/codegen/github-workflow-events.ts) from `@octokit/webhooks-schemas` |
| `pnpm run codegen:watch` | Auto-regenerate types on GraphQL changes |
| `pnpm run tsc:check` | Type-check (`tsc --noEmit --checkJs`) |
| `pnpm run test` / `test:watch` | Vitest run / interactive |
| `pnpm run start` | Run the built action locally (needs local env vars) |
| `pnpm run act:d` | Test the delete action locally with `act` |
| `pnpm changeset` | Record a changeset for notable/breaking changes |

## Task Playbooks

Expand All @@ -69,7 +70,9 @@ Non-negotiable. Violating these breaks the build or the type system.
4. Tests: `stubRequiredInputEnv()` only stubs `INPUT_KEYS_REQUIRED`, so a **required** input is covered automatically. An **optional** input is not — stub it per-test with `stubInputEnv(INPUT_KEY_X, value)` and assert the `undefined` default case too (see [`__tests__/deploy/inputs.test.ts`](__tests__/deploy/inputs.test.ts)).
5. Document it in the Inputs table of [README.md](README.md) (or [delete/README.md](delete/README.md) for the delete action).

**Cloudflare API change**: update types in [src/common/cloudflare/types.ts](src/common/cloudflare/types.ts) → add fixtures to [`__generated__/responses/`](__generated__/responses/).
**Cloudflare API change**: Pages request/response types are generated from Cloudflare's canonical OpenAPI schema — edit the operation whitelist in [bin/codegen/cloudflare-pages.ts](bin/codegen/cloudflare-pages.ts) → `pnpm run codegen:cloudflare` → consume via `components['schemas'][...]` re-exported from [src/common/cloudflare/types.ts](src/common/cloudflare/types.ts) (e.g. `PagesDeployment`). Never hand-edit [`__generated__/types/cloudflare/`](__generated__/types/cloudflare/). Add fixtures to [`__generated__/responses/`](__generated__/responses/).

**Call a new Pages REST endpoint**: whitelist + codegen the operation (above), then call it through the typed client — `cloudflareClient.GET/POST/DELETE('/accounts/{account_id}/...', {params: {path, query}})` ([client.ts](src/common/cloudflare/api/client.ts)) — and unwrap the Cloudflare envelope with `unwrap()` (returns the typed `result`, throws on failure) or `unwrapSuccess()` (returns the `success` boolean) from [fetch-result.ts](src/common/cloudflare/api/fetch-result.ts). Auth is injected once by the client middleware; don't set headers per-call. Mock in tests with `interceptCloudflare(path, response, status, method)` ([`__tests__/helpers/api.ts`](__tests__/helpers/api.ts)) — the client hits the same `https://api.cloudflare.com/client/v4/...` URLs, so undici `MockAgent` interception is unchanged.

**Breaking change**: `pnpm changeset` to record it for [CHANGELOG.md](CHANGELOG.md).

Expand Down Expand Up @@ -102,7 +105,7 @@ Non-negotiable. Violating these breaks the build or the type system.
## Build & Tooling

- **Versions**: Node via `engines`, pnpm via `packageManager` — both in [package.json](package.json).
- **`node` vs `tsx`**: use `node path/to/script.ts` for scripts importing only `node:*`, relative paths, or `package.json` (e.g. `node bin/sync-versions.ts`). Use `tsx path/to/script.ts` for scripts importing via `@/` aliases — `tsx` reads `tsconfig.json` paths and resolves `.js`→`.ts` through the entire import chain (including generated files); plain `node` cannot, and `#`-prefixed subpath imports only redirect the entry import, not relative `.js` imports inside loaded files.
- **`node` vs `tsx`**: prefer `node path/to/script.ts` (native type-stripping, no extra dep). What matters is a script's **runtime** imports, not every import: `node` is fine when those are only `node:*`, relative paths, `package.json`, npm packages, or **type-only** `@/` aliases — `import type` is stripped, so the alias never resolves at runtime (e.g. `node bin/deployments/index.ts`, whose sole `@/` import is `import type`). Use `tsx path/to/script.ts` for any script that **transitively imports the generated GraphQL client** (`__generated__/gql/`), e.g. `tsx bin/sync-readme-versions.ts`. Two things in `__generated__/gql/` break native `node`, and neither is hand-fixable (never edit generated files): (1) `graphql.ts` emits `export enum` — node's `--experimental-strip-types` rejects enums (`--experimental-transform-types` would handle them but not #2); (2) `gql.ts` has a runtime `import * as types from './graphql.js'` (the `types.*Document` values populate its `documents` map), and node won't remap `.js`→`.ts`. `tsx` handles both: it reads `tsconfig.json` paths, transforms enums, and resolves `.js`→`.ts` through the whole chain. `verbatimModuleSyntax` does not change this — node doesn't read tsconfig, and the `gql.ts` import is a real value import. Note also that `#`-prefixed subpath imports only redirect the entry import, not relative `.js` imports inside loaded files.
- **Bin script pattern**: a `bin/` script that must be both importable (for tests) and directly executable wraps side-effectful code in `if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href)` and exports the pure functions. Reference: [bin/sync-readme-versions.ts](bin/sync-readme-versions.ts).
- **ESBuild** ([esbuild.config.js](esbuild.config.js)): banner adds a `createRequire` shim for dynamic-require compatibility ([esbuild.config.js](esbuild.config.js#L22-L35)); `wrangler` is external; minification is syntax + whitespace only (identifiers preserved for debugging); sourcemaps enabled.
- **Sequencing**: run `pnpm run codegen` after GraphQL changes before building; update [input-keys.ts](input-keys.ts) after changing input keys in [action.yml](action.yml).
Expand All @@ -117,7 +120,7 @@ Formatting, linting, and type-checking are automated via [prek](https://prek.j17
- **Hook Sync Rule**: when changing formatter/linter behavior or script paths, update together — the `oxc-format-and-lint` local hook in [prek.toml](prek.toml), the PostToolUse hook in [.claude/settings.json](.claude/settings.json), and the usage header in [.claude/scripts/pre-commit-oxc.sh](.claude/scripts/pre-commit-oxc.sh).
- **Format + lint after edits**: [.claude/scripts/format-and-lint-after-edit.sh](.claude/scripts/format-and-lint-after-edit.sh) runs oxfmt + oxlint on each edited file and feeds lint errors back to the agent.
- **Type-check after edits**: [.claude/scripts/type-check-after-edit.sh](.claude/scripts/type-check-after-edit.sh) runs `pnpm run tsc:check` asynchronously (the `asyncRewake` PostToolUse hook); type errors wake the agent without blocking the edit.
- **Session-end review on stop**: [.claude/scripts/stop-review-agents.sh](.claude/scripts/stop-review-agents.sh) (Stop hook) prompts capturing session learnings in AGENTS.md (shared repo conventions) and auto-memory (user preferences + project context) when the working tree has changes.
- **Session-end review on stop**: [.claude/scripts/stop-review-agents.sh](.claude/scripts/stop-review-agents.sh) (Stop hook) prompts capturing session learnings in .claude/CLAUDE.md (shared repo conventions) and auto-memory (user preferences + project context) when the working tree has changes.

## GitHub Actions Integration

Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/format-and-lint-after-edit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ set -uo pipefail
# Tool crashes (killed by a signal, exit >= 128) are swallowed so a transient
# tooling bug never repeatedly blocks the agent.
#
# AI agents: keep this in sync with the other wiring points (AGENTS.md "Hook Sync Rule"):
# AI agents: keep this in sync with the other wiring points (.claude/CLAUDE.md "Hook Sync Rule"):
# - .claude/settings.json (PostToolUse)
# - .claude/scripts/pre-commit-oxc.sh (shared formatter/linter; also the prek.toml hook)

Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/stop-review-agents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail

# Stop hook: prompts the agent to capture any learnings from this session in the
# right place — AGENTS.md (team-shared repo conventions) and auto-memory
# right place — .claude/CLAUDE.md (team-shared repo conventions) and auto-memory
# (MEMORY.md + memory files, for user preferences and project context).
# Only triggers when there are uncommitted changes — skips read-only sessions.
#
Expand All @@ -23,7 +23,7 @@ cat <<'EOF'
"hookSpecificOutput": {
"hookEventName": "Stop",
"decision": "block",
"reason": "Before finishing: capture any learnings from this session in the right place.\n\n1. AGENTS.md — team-shared, repo-specific conventions, patterns, gotchas, or corrections to stale info.\n2. Auto-memory (MEMORY.md + memory files) — user preferences, feedback on how to work, and project context not derivable from the code.\n\nFor each: add what's new, fix what's stale, and skip what the repo already records. If both are already up to date, output a brief confirmation and the session can end."
"reason": "Before finishing: capture any learnings from this session in the right place.\n\n1. .claude/CLAUDE.md — team-shared, repo-specific conventions, patterns, gotchas, or corrections to stale info.\n2. Auto-memory (MEMORY.md + memory files) — user preferences, feedback on how to work, and project context not derivable from the code.\n\nFor each: add what's new, fix what's stale, and skip what the repo already records. If both are already up to date, output a brief confirmation and the session can end."
}
}
EOF
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/type-check-after-edit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ set -uo pipefail
# tsc checks the whole project, so this only runs for edits that can change type
# results; doc/config-only edits are skipped to avoid pointless background runs.
#
# AI agents: keep wiring points in sync (AGENTS.md "Hook Sync Rule"):
# AI agents: keep wiring points in sync (.claude/CLAUDE.md "Hook Sync Rule"):
# - .claude/settings.json (PostToolUse)

if ! command -v pnpm >/dev/null 2>&1; then
Expand Down
14 changes: 6 additions & 8 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"Stop": [
{
Expand All @@ -18,18 +19,15 @@
{
"type": "command",
"command": "bash ./.claude/scripts/format-and-lint-after-edit.sh",
"statusMessage": "Formatting and linting edited file"
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
"statusMessage": "Formatting and linting edited file",
"timeout": 30
},
{
"type": "command",
"command": "bash ./.claude/scripts/type-check-after-edit.sh",
"asyncRewake": true,
"statusMessage": "Type-checking project"
"statusMessage": "Type-checking project",
"timeout": 120
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ COPY --chown=node:node zshrc /home/node/.zshrc
# places its own globally-installed tools (`pnpm add -g`).
ENV PNPM_HOME="/usr/local/share/pnpm"
ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH"
RUN npm install -g pnpm@11.1.1
RUN npm install -g pnpm@11.9.0

# GitHub CLI 2.93.0 — installed from the official cli.github.com apt repository.
# The keyring fingerprint is verified against GitHub's published key before trusting it.
Expand Down
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"__generated__/responses/",
".agents",
".cache",
".claude",
".claude/skills",
".changeset/*.md",
"skills-lock.json"
],
Expand Down
6 changes: 1 addition & 5 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@
"constructor-super": "error",
"for-direction": "error",
"import/consistent-type-specifier-style": "error",
"import/extensions": [
"error",
"always",
{"ts": "never", "ignorePackages": true, "checkTypeImports": true}
],
"import/extensions": ["error"],
"import/no-absolute-path": "error",
"import/no-cycle": ["error", {"maxDepth": 3}],
// "import/no-relative-parent-imports": "error",
Expand Down
9 changes: 4 additions & 5 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{
"recommendations": [
"github.vscode-github-actions",
"eamodio.gitlens",
"github.vscode-github-actions",
"GraphQL.vscode-graphql-syntax",
"GraphQL.vscode-graphql",
"oxc.oxc-vscode",
"yoavbls.pretty-ts-errors",
"redhat.vscode-yaml",
"graphql.vscode-graphql-syntax",
"graphql.vscode-graphql",
"vitest.explorer",
"webpro.vscode-knip"
]
}
1 change: 0 additions & 1 deletion CLAUDE.md

This file was deleted.

Loading