diff --git a/.claude/scripts/format-and-lint-after-edit.sh b/.claude/scripts/format-and-lint-after-edit.sh new file mode 100644 index 00000000..308dceff --- /dev/null +++ b/.claude/scripts/format-and-lint-after-edit.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -uo pipefail + +# Claude Code PostToolUse hook entrypoint for Write/Edit/MultiEdit. +# +# Reads the hook payload (JSON) from stdin, extracts the edited file path, +# then formats (always) and lints it via the shared pre-commit-oxc.sh script. +# +# Exit code contract (Claude Code PostToolUse): +# 0 -> success, or skip (no file path / file gone): nothing fed back +# 2 -> lint/format errors: stderr is fed back to the agent to fix +# 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"): +# - .claude/settings.json (PostToolUse) +# - .claude/scripts/pre-commit-oxc.sh (shared formatter/linter; also the prek.toml hook) + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Claude Code passes hook data as JSON on stdin, not via environment variables. +file_path="$(jq -r '.tool_input.file_path // empty')" +if [[ -z "$file_path" || ! -f "$file_path" ]]; then + exit 0 +fi + +output="$(bash "$script_dir/pre-commit-oxc.sh" "$file_path" 2>&1)" +code=$? + +# Success, or a tool crash we don't want to surface as actionable feedback. +if [[ "$code" -eq 0 || "$code" -ge 128 ]]; then + exit 0 +fi + +# Lint/format errors: surface them to the agent (exit 2 feeds stderr back). +printf '%s\n' "$output" >&2 +exit 2 diff --git a/.github/hooks/scripts/pre-commit-oxc.sh b/.claude/scripts/pre-commit-oxc.sh similarity index 76% rename from .github/hooks/scripts/pre-commit-oxc.sh rename to .claude/scripts/pre-commit-oxc.sh index 3686143d..39ed7cae 100644 --- a/.github/hooks/scripts/pre-commit-oxc.sh +++ b/.claude/scripts/pre-commit-oxc.sh @@ -2,11 +2,12 @@ set -euo pipefail # Shared formatter/linter entrypoint used by: -# - .pre-commit-config.yaml (local hook: oxc-format-and-lint) -# - .github/hooks/format-and-lint-after-edit.json (PostToolUse) -# AI agents: keep both wiring points and this usage list in sync when behavior or paths change. +# - prek.toml (local hook: oxc-format-and-lint) +# - .claude/scripts/format-and-lint-after-edit.sh (PostToolUse wrapper, +# wired via .claude/settings.json) +# AI agents: keep all wiring points and this usage list in sync when behavior or paths change. -# pre-commit passes staged file paths as positional arguments. +# prek passes staged file paths as positional arguments. # Exit fast when nothing matched this hook's file filter. if [[ "$#" -eq 0 ]]; then exit 0 diff --git a/.claude/scripts/stop-review-agents.sh b/.claude/scripts/stop-review-agents.sh new file mode 100755 index 00000000..75f5da9b --- /dev/null +++ b/.claude/scripts/stop-review-agents.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +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 +# (MEMORY.md + memory files, for user preferences and project context). +# Only triggers when there are uncommitted changes — skips read-only sessions. +# +# Used by: +# - .claude/settings.json (Stop hook) + +if ! command -v git >/dev/null 2>&1; then + exit 0 +fi + +# Skip if no working-tree changes (read-only session or everything already committed). +if git diff --quiet 2>/dev/null && git diff --cached --quiet 2>/dev/null; then + exit 0 +fi + +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." + } +} +EOF + +exit 0 diff --git a/.claude/scripts/type-check-after-edit.sh b/.claude/scripts/type-check-after-edit.sh new file mode 100644 index 00000000..b859d012 --- /dev/null +++ b/.claude/scripts/type-check-after-edit.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -uo pipefail + +# Claude Code PostToolUse hook for TypeScript type-checking after Write/Edit/MultiEdit. +# +# Runs as an async "rewake" hook (asyncRewake): it does NOT block the edit. The +# whole-project type check (`pnpm run tsc:check`) runs in the background, and on +# type errors the hook exits 2 to wake the agent with the errors to fix. +# +# Exit code contract (asyncRewake PostToolUse): +# 0 -> no type errors, or skipped: the agent is not interrupted +# 2 -> type errors: hook output is fed back to wake the agent to fix them +# +# 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"): +# - .claude/settings.json (PostToolUse) + +if ! command -v pnpm >/dev/null 2>&1; then + exit 0 +fi + +# Claude Code passes hook data as JSON on stdin, not via environment variables. +file_path="$(jq -r '.tool_input.file_path // empty')" +case "$file_path" in + *.ts | *.tsx | *.cts | *.mts | *.js | *.jsx | *.cjs | *.mjs | *.json) ;; + *) exit 0 ;; +esac + +tsc_output="$(pnpm run tsc:check 2>&1)" || tsc_exit=$? + +if [[ "${tsc_exit:-0}" -eq 0 ]]; then + exit 0 +fi + +# Type errors: exit 2 so asyncRewake feeds these back to the agent. +printf 'TypeScript type errors must be fixed:\n\n%s\n' "$tsc_output" +exit 2 diff --git a/.claude/settings.json b/.claude/settings.json index c39c70c2..1c5e8594 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,27 +5,31 @@ "hooks": [ { "type": "command", - "command": "bash ./.github/hooks/scripts/stop-type-check.sh", - "timeout": 60 + "command": "bash ./.claude/scripts/stop-review-agents.sh", + "timeout": 10 } ] - }, + } + ], + "PostToolUse": [ { + "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", - "command": "bash ./.github/hooks/scripts/stop-review-agents.sh", - "timeout": 10 + "command": "bash ./.claude/scripts/format-and-lint-after-edit.sh", + "statusMessage": "Formatting and linting edited file" } ] - } - ], - "PostToolUse": [ + }, { + "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", - "command": "if [[ -n \"$TOOL_INPUT_FILE_PATH\" && -f \"$TOOL_INPUT_FILE_PATH\" ]]; then bash ./.github/hooks/scripts/pre-commit-oxc.sh \"$TOOL_INPUT_FILE_PATH\"; fi" + "command": "bash ./.claude/scripts/type-check-after-edit.sh", + "asyncRewake": true, + "statusMessage": "Type-checking project" } ] } diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..0a4f4d68 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,76 @@ +# Base image pinned by digest for reproducibility. Bumping the node version +# means updating both the tag and the digest (see `docker pull` output). +FROM node:24.16.0-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf + +# System packages. Individual versions are not pinned — the digest-pinned base +# image plus Docker's layer cache provide reproducibility in practice. +# - zsh + zsh-autosuggestions: interactive shell setup, configured via zshrc. +# - chsh: make zsh the login shell for the `node` user so VS Code terminals +# and any `docker exec` sessions land in zsh by default. +# - openssh-client: provides ssh-keygen, which git invokes to SSH-sign commits +# (gpg.format=ssh + commit.gpgsign=true); without it commits fail. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl wget gnupg ca-certificates apt-transport-https \ + git openssh-client bash zsh zsh-autosuggestions \ + && rm -rf /var/lib/apt/lists/* \ + && chsh -s /usr/bin/zsh node + +# Persisted zsh history. /commandhistory is mounted as a named volume in +# devcontainer.json so history survives container rebuilds. The directory must +# exist and be owned by `node` before the volume mounts, otherwise the first +# write fails with EACCES. SHARE_HISTORY (set in zshrc) writes each command +# immediately and makes it visible across concurrent shells. +RUN mkdir -p /commandhistory \ + && touch /commandhistory/.zsh_history \ + && chown -R node:node /commandhistory + +# Shell config lives in a sibling file so it can be edited without escaping +# shell quoting inside a Dockerfile RUN. See .devcontainer/zshrc. +COPY --chown=node:node zshrc /home/node/.zshrc + +# pnpm 11.1.1 — installed globally from the npm registry rather than piping the +# get.pnpm.io install script to bash. npm resolves the package against its +# registry integrity hashes, so there is no unpinnable remote shell script +# executing as root at build time. npm's global prefix on the node image is +# /usr/local, so the binary lands on PATH for the non-root `node` user. Not +# Corepack: pnpm docs flag an outdated-signatures issue that adds bootstrapping +# friction; a plain npm global install sidesteps that. PNPM_HOME is where pnpm +# 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 + +# 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. +# To update: change the version in `apt-get install gh=` and update the comment. +# Latest releases: https://github.com/cli/cli/releases +RUN mkdir -p -m 755 /etc/apt/keyrings \ + && wget -nv -O /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && gpg --show-keys /etc/apt/keyrings/githubcli-archive-keyring.gpg 2>/dev/null \ + | grep -qF "2C6106201985B60E6C7AC87323F3D4EA75716059" \ + && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends gh=2.93.0 \ + && rm -rf /var/lib/apt/lists/* + +# act v0.2.88 — run GitHub Actions locally, installed via the nektos/act install +# script. The installer is pulled from the matching version tag (not `master`) so +# the script that runs at build time is pinned alongside the binary it installs. +# The script verifies the downloaded release against its published checksums +# internally. -b sets the install directory; the version arg pins the release. +RUN curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/v0.2.88/install.sh \ + | bash -s -- -b /usr/local/bin v0.2.88 + +# prek v0.4.3 — pre-commit hook manager, copied from the official distroless image +COPY --from=ghcr.io/j178/prek:v0.4.3@sha256:953593c93d253d44bad97b36b428c13edd074dc35de003ccbf3ff9a40c0c79b9 /prek /usr/local/bin/prek + +# Drop to the non-root `node` user as the image default. devcontainer.json sets +# `remoteUser: node` for VS Code sessions, but this also covers plain +# `docker exec`/`docker run`, so nothing lands as root by default. All privileged +# install steps above run as root; devcontainer features re-elevate on their own. +USER node + +WORKDIR /workspace diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000..651be800 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,42 @@ +# Dev Container + +This dev container provides a pre-configured TypeScript/Node environment with Claude Code and the GitHub CLI ready to use. + +## Host requirements + +The container bind-mounts a few host paths so your local Claude Code and `gh` credentials are available inside the container. `initializeCommand` creates these paths automatically on first launch if they don't exist, but you should authenticate them on the host **before** opening the project in the container — otherwise the container starts with empty/unauthenticated credentials. + +### Required host setup + +1. **Claude Code** — install and sign in on the host so that `~/.claude.json` and `~/.claude/` are populated: + + ```sh + # https://docs.claude.com/en/docs/claude-code/setup + claude # follow the sign-in prompts + ``` + +2. **GitHub CLI** — install and authenticate on the host so that `~/.config/gh/` contains valid credentials: + ```sh + # https://cli.github.com/ + gh auth login + ``` + +If either step is skipped, the container will still build, but you'll need to re-authenticate inside the container (and for the read-only mounts below, that means editing the host files instead). + +## Mounts + +| Source (host) | Target (container) | Mode | Purpose | +| ------------------------------------- | ------------------------- | ---------- | -------------------------------------------------- | +| `~/.claude.json` | `/home/node/.claude.json` | read-only | Claude Code account/config (credentials) | +| `~/.claude/` | `/home/node/.claude/` | read-write | Claude Code working state: projects, todos, memory | +| `~/.config/gh/` | `/home/node/.config/gh/` | read-only | `gh` CLI credentials and config | +| `cloudflare-pages-action-zsh-history` | `/commandhistory` | volume | Persistent zsh history across container rebuilds | + +The credential mounts (`.claude.json`, `gh`) are **read-only** so a compromised dependency running inside the container cannot exfiltrate or tamper with host credentials. Re-authentication should be performed on the host. + +The `.claude/` directory is read-write because Claude Code writes session state (projects, todos, memory) during normal use. + +## Security notes + +- `initializeCommand` runs on the **host** with your user privileges before the container starts. Review changes to [devcontainer.json](devcontainer.json) in PRs the same way you'd review CI workflow changes. +- Host credentials are exposed to any process running inside the container, including `pnpm install` postinstall scripts. The read-only flag limits write access but not read access — treat the container as a trust boundary for the credentials it can see. diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index fa811a29..ec86fa72 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -5,30 +5,10 @@ "resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a", "integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a" }, - "ghcr.io/devcontainers-extra/features/act:1.0.15": { - "version": "1.0.15", - "resolved": "ghcr.io/devcontainers-extra/features/act@sha256:db4a2194930d1f7ec62822d4f600dd2fa4aff3c33b98cdb0b578b64ffb10924c", - "integrity": "sha256:db4a2194930d1f7ec62822d4f600dd2fa4aff3c33b98cdb0b578b64ffb10924c" - }, - "ghcr.io/devcontainers-extra/features/pre-commit:2.0.18": { - "version": "2.0.18", - "resolved": "ghcr.io/devcontainers-extra/features/pre-commit@sha256:6e0bb2ce80caca1d94f44dab5d0653d88a1c00984e590adb7c6bce012d0ade6e", - "integrity": "sha256:6e0bb2ce80caca1d94f44dab5d0653d88a1c00984e590adb7c6bce012d0ade6e" - }, "ghcr.io/devcontainers/features/docker-outside-of-docker:1.10.0": { "version": "1.10.0", "resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e", "integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e" - }, - "ghcr.io/devcontainers/features/github-cli:1.1.0": { - "version": "1.1.0", - "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", - "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" - }, - "ghcr.io/devcontainers/features/node:2.0.0": { - "version": "2.0.0", - "resolved": "ghcr.io/devcontainers/features/node@sha256:fedd4c11f7adfb64283b578dddc7da906728daa25fa293351c9d913231acf12f", - "integrity": "sha256:fedd4c11f7adfb64283b578dddc7da906728daa25fa293351c9d913231acf12f" } } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d2b4a28d..1d8a66d4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,9 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// For format and config option details, see https://aka.ms/devcontainer.json. { "name": "Container - Cloudflare Pages Action", - // mcr.microsoft.com/devcontainers/base:jammy - "image": "mcr.microsoft.com/devcontainers/base:jammy@sha256:f2d74267998cfe76acefa5cc8d19ccc86bb2ba4520a5ad2b218def9566dc04cd", + "build": { + "dockerfile": "Dockerfile" + }, "customizations": { "vscode": { "extensions": [ @@ -11,36 +12,48 @@ "GraphQL.vscode-graphql-syntax@1.3.10", "GraphQL.vscode-graphql@0.13.4", "oxc.oxc-vscode@1.56.0", - "redhat.vscode-yaml@1.24.2026052209", "vitest.explorer@1.50.4", - "webpro.vscode-knip@2.1.5", - "yoavbls.pretty-ts-errors@0.8.7" + "webpro.vscode-knip@2.1.5" ], "settings": { + "chat.disableAIFeatures": true, "extensions.autoUpdate": false, - "extensions.autoCheckUpdates": true + "extensions.autoCheckUpdates": true, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/usr/bin/zsh" + } + } } } }, - "postCreateCommand": "sed -i '/^ZSH_THEME/c\\ZSH_THEME=\"bira\"' ~/.zshrc && pnpm i && pre-commit install", - "updateContentCommand": "rm -rf .cache && pnpm i && pre-commit install", + "postCreateCommand": "sed -i '/^ZSH_THEME/c\\ZSH_THEME=\"bira\"' ~/.zshrc && pnpm i && prek install", + "updateContentCommand": "rm -rf .cache && pnpm i && prek install", "features": { - "ghcr.io/devcontainers-extra/features/act:1.0.15": {}, "ghcr.io/devcontainers/features/docker-outside-of-docker:1.10.0": {}, - "ghcr.io/devcontainers/features/node:2.0.0": { - "version": "24.7.0", - "pnpmVersion": "11.0.8" - }, - "ghcr.io/devcontainers/features/github-cli:1.1.0": {}, - "ghcr.io/devcontainers-extra/features/pre-commit:2.0.18": {}, "ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5": {} }, "containerEnv": { - "CLAUDE_CONFIG_DIR": "/home/vscode/.claude" + "CLAUDE_CONFIG_DIR": "/home/node/.claude", + "DISABLE_AUTOUPDATER": "1" }, + // Ensure mount sources exist on the host before the container starts. + // Without this, Docker would auto-create an empty *directory* at the .claude.json + // path (since the source is missing), which breaks Claude Code. + // See ./README.md for host setup requirements. + "initializeCommand": "touch ${localEnv:HOME}/.claude.json && mkdir -p ${localEnv:HOME}/.claude ${localEnv:HOME}/.config/gh", "mounts": [ - "source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind" - ] - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + // Claude Code credentials — read-only so container processes can't tamper with host auth. + "source=${localEnv:HOME}/.claude.json,target=/home/node/.claude.json,type=bind,readonly", + // Claude Code working state (projects, todos, memory) — must be writable. + "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind", + // GitHub CLI credentials — read-only; re-auth on the host with `gh auth login`. + "source=${localEnv:HOME}/.config/gh,target=/home/node/.config/gh,type=bind,readonly", + // Named volume for persistent zsh history across container rebuilds. + "source=cloudflare-pages-action-zsh-history,target=/commandhistory,type=volume" + ], + // Connect as the non-root `node` user (the Dockerfile's default USER). Set to + // "root" to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "node" } diff --git a/.devcontainer/zshrc b/.devcontainer/zshrc new file mode 100644 index 00000000..bde68e63 --- /dev/null +++ b/.devcontainer/zshrc @@ -0,0 +1,28 @@ +# Zsh config for the devcontainer's `node` user. +# Installed to /home/node/.zshrc by .devcontainer/Dockerfile. + +# History persistence. +# HISTFILE lives in /commandhistory, a named volume mounted by devcontainer.json +# so history survives container rebuilds. SHARE_HISTORY writes each command to +# the file immediately and makes it visible across concurrent shells (useful +# when VS Code opens multiple terminals against the same container). +# HIST_IGNORE_DUPS drops a command if it matches the previous one. +export HISTFILE=/commandhistory/.zsh_history +export HISTSIZE=10000 +export SAVEHIST=10000 +setopt SHARE_HISTORY +setopt HIST_IGNORE_DUPS + +# Inline grey suggestions from history as you type. Accept with the right arrow +# key (or End). Installed via the `zsh-autosuggestions` apt package. +source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh + +# Prompt: cwd + git branch on line 1, input on line 2. +# vcs_info is a built-in zsh module — no plugin manager or external deps. +# PROMPT_SUBST is required so ${vcs_info_msg_0_} is re-expanded on every prompt. +# The $'\n' between the two single-quoted segments inserts a real newline. +autoload -Uz vcs_info +precmd() { vcs_info } +zstyle ':vcs_info:git:*' formats ' (%b)' +setopt PROMPT_SUBST +PROMPT='%F{cyan}%~%f%F{yellow}${vcs_info_msg_0_}%f'$'\n''$ ' diff --git a/.github/hooks/format-and-lint-after-edit.json b/.github/hooks/format-and-lint-after-edit.json deleted file mode 100644 index fdb6e68d..00000000 --- a/.github/hooks/format-and-lint-after-edit.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "hooks": { - "PostToolUse": [ - { - "type": "command", - "command": "if [[ -n \"$TOOL_INPUT_FILE_PATH\" && -f \"$TOOL_INPUT_FILE_PATH\" ]]; then bash ./.github/hooks/scripts/pre-commit-oxc.sh \"$TOOL_INPUT_FILE_PATH\"; fi" - } - ] - } -} diff --git a/.github/hooks/review-agents-at-stop.json b/.github/hooks/review-agents-at-stop.json deleted file mode 100644 index 31c603dd..00000000 --- a/.github/hooks/review-agents-at-stop.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "hooks": { - "Stop": [ - { - "type": "command", - "command": "bash ./.github/hooks/scripts/stop-review-agents.sh", - "timeout": 10 - } - ] - } -} diff --git a/.github/hooks/scripts/stop-review-agents.sh b/.github/hooks/scripts/stop-review-agents.sh deleted file mode 100755 index 8b8256cf..00000000 --- a/.github/hooks/scripts/stop-review-agents.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Stop hook: prompts the agent to review AGENTS.md for any learnings from this session. -# Only triggers when there are uncommitted changes — skips read-only sessions. -# -# Used by: -# - .github/hooks/review-agents-at-stop.json (Stop) - -if ! command -v git >/dev/null 2>&1; then - exit 0 -fi - -# Skip if no working-tree changes (read-only session or everything already committed). -if git diff --quiet 2>/dev/null && git diff --cached --quiet 2>/dev/null; then - exit 0 -fi - -cat <<'EOF' -{ - "hookSpecificOutput": { - "hookEventName": "Stop", - "decision": "block", - "reason": "Before finishing: review AGENTS.md for any learnings from this session worth capturing.\n\nConsider:\n- New patterns, conventions, or gotchas discovered\n- Corrections to stale or inaccurate information\n- Non-obvious constraints or workflow steps that needed clarification\n- Anything that would have saved time if it had been documented at the start\n\nIf AGENTS.md is already up to date, output a brief confirmation and the session can end." - } -} -EOF - -exit 0 diff --git a/.github/hooks/scripts/stop-type-check.sh b/.github/hooks/scripts/stop-type-check.sh deleted file mode 100644 index 0cf911dd..00000000 --- a/.github/hooks/scripts/stop-type-check.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Stop hook for TypeScript type checking. -# Runs `pnpm run tsc:check` and blocks the session if type errors are found, -# forcing the agent to resolve them before finishing. - -if ! command -v pnpm >/dev/null 2>&1; then - exit 0 -fi - -# Capture tsc output and exit code. -tsc_output=$(pnpm run tsc:check 2>&1) || tsc_exit=$? - -# If tsc succeeded, allow the session to stop. -if [[ "${tsc_exit:-0}" -eq 0 ]]; then - exit 0 -fi - -# tsc failed: block the session and report errors. -cat < #vX.Y.Z` action refs in both are kept current by [bin/sync-readme-versions.ts](bin/sync-readme-versions.ts); don't hand-edit the SHA or version. +- **Markdown paths**: wrap `__dunder__` path tokens in backticks — e.g. ``[`__generated__/`](__generated__/)`` — because the formatter reads a bare `__x__` as bold emphasis and can mangle both the link text and its target. +- **Dockerfiles** ([.devcontainer/Dockerfile](.devcontainer/Dockerfile)): keep logically distinct steps in separate `RUN` blocks. Don't merge blocks when it adds complexity (e.g. extra bootstrap or a second `apt-get update` just to consolidate) — favor clarity over layer minimisation. -- **Line anchors**: A few links in this document use line numbers to point to non-obvious code locations. When you edit code at one of those locations, update the line number in this file to match. -- **Knip**: Dead code detection ([knip.json](knip.json)) - ignores [**generated**/](__generated__/), fragments, wrangler, act -- **Oxlint**: TypeScript linting with custom rules ([.oxlintrc.json](.oxlintrc.json)) -- **TypeScript**: Strict mode with `verbatimModuleSyntax`, `noEmit`, `checkJs` -- **No console.log**: Use `@actions/core` methods (`info`, `debug`, `warning`, `error`, `setFailed`) -- **Hook Sync Rule**: When changing formatter/linter hook behavior or script paths, update these together: [.pre-commit-config.yaml](.pre-commit-config.yaml), [.github/hooks/format-and-lint-after-edit.json](.github/hooks/format-and-lint-after-edit.json), and the usage header in [.github/hooks/scripts/pre-commit-oxc.sh](.github/hooks/scripts/pre-commit-oxc.sh). -- **Type Check on Session End**: [.github/hooks/type-check-at-stop.json](.github/hooks/type-check-at-stop.json) runs `pnpm run tsc:check` when the agent finishes and blocks the session if type errors are found, forcing resolution before the session ends. -- **AGENTS.md Review on Session End**: [.github/hooks/review-agents-at-stop.json](.github/hooks/review-agents-at-stop.json) prompts the agent to review AGENTS.md for any learnings worth capturing before the session ends (only fires when working-tree changes are present). +## Build & Tooling -### File Organization +- **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. +- **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). +- **Debugging**: `pnpm run start` runs the built action locally; add `debugger` statements and run vitest under the Node inspector. +- **Code quality**: Knip dead-code detection ([knip.json](knip.json), ignores `__generated__`, fragments, wrangler, act); Oxlint ([.oxlintrc.json](.oxlintrc.json)); TypeScript strict mode (`verbatimModuleSyntax`, `noEmit`, `checkJs`). +- **Line anchors**: a few links here use line numbers (e.g. `create.ts#L49-L54`, `esbuild.config.js#L22-L35`). When you edit code at one of those locations, update the line number in this file. -- [src/common/](src/common/): Shared between deploy/delete actions -- [src/common/github/](src/common/github/): GitHub API interactions (deployments, comments, environments) -- [src/common/cloudflare/](src/common/cloudflare/): Cloudflare Pages API and deployment logic -- [**generated**/](__generated__/): Never edit manually - regenerated by codegen scripts -- [**fixtures**/](__fixtures__/): Test data that's manually maintained -- [**tests**/](__tests__/): Mirrors src/ structure with `.test.ts` suffix +## Dev Environment Hooks -### ESBuild Specifics +Formatting, linting, and type-checking are automated via [prek](https://prek.j178.dev) (`prek.toml`) and Claude Code hooks ([.claude/settings.json](.claude/settings.json), scripts in [.claude/scripts/](.claude/scripts/)). -- Banner adds `createRequire` shim for dynamic require compatibility ([esbuild.config.js](esbuild.config.js#L22-L35)) -- External: `wrangler` (peer dependency expected in user's environment) -- Minification: Syntax and whitespace only (not identifiers) for debugging +- **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. ## GitHub Actions Integration -- Requires manual creation of GitHub Environments (action cannot create due to permission requirements) -- Uses `GITHUB_TOKEN` with permissions: `actions:read`, `contents:read`, `deployments:write`, `pull-requests:write` -- Supports `push`, `pull_request`, `workflow_dispatch`, `workflow_run` events only (validated in [src/deploy/main.ts](src/deploy/main.ts)) -- Deployment payload includes Cloudflare metadata for deletion workflow ([src/common/github/deployment/types.ts](src/common/github/deployment/types.ts)) - -## When Modifying Core Functionality - -1. **Adding GitHub API Operations**: Write `graphql(/* GraphQL */ `...`)` operation in the appropriate file → run `pnpm run codegen` (types won't exist until you do) → import generated types from [`__generated__/gql/graphql.ts`](__generated__/gql/graphql.ts) → use typed `request()` client. If a scalar appears as `any` in generated output, add a mapping in [graphql.config.ts](graphql.config.ts) and re-run codegen. -2. **New Action Inputs**: Update action.yml → add key to input-keys.ts → handle in inputs.ts → stub in test helpers -3. **Cloudflare API Changes**: Update types in [src/common/cloudflare/types.ts](src/common/cloudflare/types.ts) → add fixtures to [**generated**/responses/](__generated__/responses/) -4. **Breaking Changes**: Document in [CHANGELOG.md](CHANGELOG.md) using changesets: `pnpm changeset` +- GitHub Environments must be created manually (the action lacks permission to create them). +- `GITHUB_TOKEN` permissions: `actions:read`, `contents:read`, `deployments:write`, `pull-requests:write`. +- Supported events only: `push`, `pull_request`, `workflow_dispatch`, `workflow_run` (validated in [src/deploy/main.ts](src/deploy/main.ts)). +- The deployment payload embeds Cloudflare metadata so the delete workflow can find deployments ([src/common/github/deployment/types.ts](src/common/github/deployment/types.ts)). -## Additional Resources for AI Agents +## Resources -- **[README.md](README.md)** — User-facing documentation for the action. -- **[CHANGELOG.md](CHANGELOG.md)** — Record of changes and breaking changes for this project. +- [README.md](README.md) — user-facing docs for the deploy action. +- [delete/README.md](delete/README.md) — user-facing docs for the delete action. +- [CHANGELOG.md](CHANGELOG.md) — changes and breaking changes. diff --git a/README.md b/README.md index 71fb158c..48b91470 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,86 @@ [![test](https://github.com/andykenward/github-actions-cloudflare-pages/actions/workflows/test.yml/badge.svg)](https://github.com/andykenward/github-actions-cloudflare-pages/actions/workflows/test.yml) [![Check dist/](https://github.com/andykenward/github-actions-cloudflare-pages/actions/workflows/check-dist.yml/badge.svg)](https://github.com/andykenward/github-actions-cloudflare-pages/actions/workflows/check-dist.yml) [![release](https://github.com/andykenward/github-actions-cloudflare-pages/actions/workflows/release.yml/badge.svg)](https://github.com/andykenward/github-actions-cloudflare-pages/actions/workflows/release.yml) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/andykenward/github-actions-cloudflare-pages/main.svg)](https://results.pre-commit.ci/latest/github/andykenward/github-actions-cloudflare-pages/main) -# GitHub Action Cloudflare Pages +# GitHub Action — Cloudflare Pages -This action deploys your build output to [Cloudflare Pages] using [Wrangler]. [GitHub Environments] and [GitHub Deployment] are used to keep track of the [Cloudflare Pages] deployments. +Deploy your build output to [Cloudflare Pages] with [Wrangler], while tracking every release through [GitHub Environments] and [GitHub Deployment]. On a [pull request], it creates a preview deployment and comments the URL on the PR. -When used in context of a [pull request], the action will create a deployment for the pull request and add a comment with the URL of the deployment. +**Features** - Deploy to [Cloudflare Pages]. -- Use [GitHub Environments] & [GitHub Deployment]. -- Comment on pull requests with deployment URL. -- Delete deployments using [`andykenward/github-actions-cloudflare-pages/delete`](./delete/README.md) -- Define a `working-directory` input for the `wrangler` cli command to execute from. Useful for monorepos where the `functions` folder may not be in the root directory. +- Track releases with [GitHub Environments] & [GitHub Deployment]. +- Comment the deployment URL on pull requests. +- Delete old deployments with the companion [`/delete`](./delete/README.md) action. +- Run Wrangler from a subfolder via the `working-directory` input — handy for monorepos where `functions` isn't in the repo root. -## GitHub Environments - **(Required)** +## Quick start -> **This GitHub Action doesn't create the required [GitHub Environments], see below for more information.** +1. Create a [Cloudflare Pages] project and an API token that can edit it. +2. **Manually create your [GitHub Environments]** (for example `production` and `preview`) — the action can't create them for you. See [Setup](#setup). +3. Add the Cloudflare values as repository secrets/variables, then add a workflow like the one below (this mirrors the official template in [.github/workflow-templates/deploy.yml](.github/workflow-templates/deploy.yml)): -The GitHub Action uses [GitHub Environments] for the deployments. **This GitHub Action doesn't create [GitHub Environments]**, this is due to the required permission of `administration:write` by the GitHub API, you will have to do this manually, see [Creating an environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#creating-an-environment). +```yaml +name: Cloudflare Pages Deploy +on: + push: + branches: [main] + pull_request: + branches: [main] -For example manually create two GitHub Environments called "production" & "preview". Then you can define them in the workflow yaml step for `github-environment` as the below example. The check for `github.ref == 'refs/heads/main'` is used to switch between these two GitHub Environments, `main` Git branch for `"production"` and any other branch will use `"preview"`. +# Deny all permissions by default; grant only what each job needs. +permissions: {} -```yaml -github-environment: ${{ (github.ref == 'refs/heads/main' && 'production') || 'preview' }} +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + actions: read # Only required for a private repo. + contents: read + deployments: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Deploy to Cloudflare Pages + uses: andykenward/github-actions-cloudflare-pages@1f45924c4dd0c6d746a7edfaa4e1dea8958806a6 #v3.4.0 + with: + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + cloudflare-account-id: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + cloudflare-project-name: ${{ vars.CLOUDFLARE_PROJECT_NAME }} + directory: dist + github-token: ${{ secrets.GITHUB_TOKEN }} + github-environment: ${{ (github.ref == 'refs/heads/main' && 'production') || 'preview' }} ``` -## Upgrading +The `github-environment` expression deploys the `main` branch to `production` and every other branch to `preview`. + +## Setup + +### 1. Cloudflare + +Create a [Cloudflare Pages] project, then give the action three values (store the token as a repository **secret** and the rest as **variables** or secrets): + +- `cloudflare-api-token` — an API token with permission to edit Cloudflare Pages. +- `cloudflare-account-id` — your Cloudflare account ID. +- `cloudflare-project-name` — the Pages project to upload to. + +### 2. GitHub Environments (required) -If you have previous deployments using an older version of this GitHub Action please see the [CHANGELOG.md](./CHANGELOG.md) for breaking changes. +> [!IMPORTANT] +> This action does **not** create [GitHub Environments]. Creating them requires the GitHub API `administration:write` permission, which the action can't request — so you must create them manually. See [Creating an environment]. + +Create each environment you reference (for example `production` and `preview`), then select one per run with the `github-environment` input. A common pattern switches on the branch: + +```yaml +github-environment: ${{ (github.ref == 'refs/heads/main' && 'production') || 'preview' }} +``` -## Permissions +### 3. Permissions -The [permissions] required for this GitHub Action when using the created [`GITHUB_TOKEN`] by the workflow for the `github-token` field. +When using the workflow's built-in [`GITHUB_TOKEN`] for the `github-token` input, grant these [permissions]: ```yaml permissions: - actions: read # Only required for a private GitHub Repo. + actions: read # Only required for a private GitHub repo. contents: read deployments: write pull-requests: write @@ -42,66 +88,38 @@ permissions: ## Inputs -```yaml -cloudflare-api-token: - description: 'Cloudflare API Token' - required: true -cloudflare-account-id: - description: 'Cloudflare Account ID' - required: true -cloudflare-project-name: - description: 'Cloudflare Pages project to upload to' - required: true -directory: - description: 'Directory of static files to upload' - required: true -github-token: - description: 'Github API key, make sure to add the required permissions for this action.' - required: true -github-environment: - description: 'GitHub environment to deploy to. You need to manually create this for the github repo' - required: true -pr-number: - description: 'GitHub pull request number to comment on. If not set, the action auto-detects from the event payload.' - required: false -working-directory: - description: 'Directory to run wrangler cli from' - required: false -wrangler-version: - description: 'Wrangler version to use. Otherwise a default version from the action will be used.' - required: false -``` +| Input | Required | Description | +| ------------------------- | -------- | ----------------------------------------------------------------------------------------------------- | +| `cloudflare-api-token` | yes | Cloudflare API Token | +| `cloudflare-account-id` | yes | Cloudflare Account ID | +| `cloudflare-project-name` | yes | Cloudflare Pages project to upload to | +| `directory` | yes | Directory of static files to upload | +| `github-token` | yes | Github API key, make sure to add the required permissions for this action. | +| `github-environment` | yes | GitHub environment to deploy to. You need to manually create this for the github repo | +| `pr-number` | no | GitHub pull request number to comment on. If not set, the action auto-detects from the event payload. | +| `working-directory` | no | Directory to run wrangler cli from | +| `wrangler-version` | no | Wrangler version to use. Otherwise a default version from the action will be used. | ## Outputs -```yaml -id: - description: 'Cloudflare Pages deployed id' - value: ${{ steps.action.outputs.id }} -url: - description: 'Cloudflare Pages deployed url' - value: ${{ steps.action.outputs.url }} -environment: - description: 'Cloudflare Pages deployed environment "production" or "preview"' - value: ${{ steps.action.outputs.environment }} -alias: - description: 'Cloudflare Pages deployed alias. Fallsback to deployed url if deployed alias is null' - value: ${{ steps.action.outputs.alias }} -wrangler: - description: 'Wrangler cli output' - values: ${{ steps.action.outputs.wrangler }} -``` +| Output | Description | +| ------------- | ------------------------------------------------------------------------------------- | +| `id` | Cloudflare Pages deployed id | +| `url` | Cloudflare Pages deployed url | +| `environment` | Cloudflare Pages deployed environment `production` or `preview` | +| `alias` | Cloudflare Pages deployed alias. Falls back to deployed url if deployed alias is null | +| `wrangler` | Wrangler cli output | ## Examples -See the GitHub Workflow Templates [.github/workflow-templates/](.github/workflow-templates/) +Ready-to-use GitHub Workflow Templates live in [.github/workflow-templates/](.github/workflow-templates/): -- [.github/workflow-templates/delete.yml](.github/workflow-templates/delete.yml) -- [.github/workflow-templates/deploy.yml](.github/workflow-templates/deploy.yml) +- [deploy.yml](.github/workflow-templates/deploy.yml) +- [delete.yml](.github/workflow-templates/delete.yml) ### Fork pull requests with `workflow_run` -When pull requests come from forks, the initial `pull_request` workflow may not have access to secrets. Use a second workflow triggered by `workflow_run` to deploy from the original repository context after approval, and set the `pr-number` input so the action can resolve the correct pull request to comment on. +Pull requests from forks don't have access to secrets in the initial `pull_request` workflow. Use a second workflow triggered by `workflow_run` to deploy from the original repository context after the first workflow succeeds, and set the `pr-number` input so the action can find the right pull request to comment on. ```yaml name: Deploy PR Preview (Fork Safe) @@ -136,17 +154,17 @@ jobs: pr-number: # The PR number ``` -This action supports the `workflow_run` event and will use the `workflow_run` head commit SHA and branch for deployment metadata. +The action supports the `workflow_run` event and uses its head commit SHA and branch for the deployment metadata. -## Comment Example +## Pull request comment ![pull request comment example](./docs/comment.png) -## Deleting Deployments +## Deleting deployments -See the sub-action [`andykenward/github-actions-cloudflare-pages/delete`](./delete/README.md) about deleting deployments. +Use the companion sub-action [`andykenward/github-actions-cloudflare-pages/delete`](./delete/README.md) to remove old deployments. -### GitHub Deployment payload example response +The GitHub Deployment payload this action creates includes the Cloudflare metadata the delete action needs: ```json { @@ -164,27 +182,18 @@ See the sub-action [`andykenward/github-actions-cloudflare-pages/delete`](./dele ## Debugging -[Action Debugging](https://github.com/actions/toolkit/blob/main/docs/action-debugging.md#step-debug-logs) - -### How to Access Step Debug Logs +GitHub provides two debug log levels — see [Action Debugging]. Enable them by [setting a repository secret]: -This flag can be enabled by [setting the secret](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#creating-encrypted-secrets) `ACTIONS_STEP_DEBUG` to `true`. +- **Step debug logs**: set `ACTIONS_STEP_DEBUG` to `true`. Debug events then appear in the [downloaded logs] and [web logs]. +- **Runner diagnostic logs**: set `ACTIONS_RUNNER_DEBUG` to `true`. Extra diagnostic files then appear in the `runner-diagnostic-logs` folder of the [log archive][downloaded logs]. -All actions ran while this secret is enabled will show debug events in the [Downloaded Logs](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#downloading-logs) and [Web Logs](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#viewing-logs-to-diagnose-failures). - -### How to Access Runner Diagnostic Logs - -These log files are enabled by [setting the secret](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#creating-encrypted-secrets) `ACTIONS_RUNNER_DEBUG` to `true`. - -All actions ran while this secret is enabled contain additional diagnostic log files in the `runner-diagnostic-logs` folder of the [log archive](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#downloading-logs). - -## Docs +## Upgrading -- [GitHub Action Variables](https://docs.github.com/en/actions/learn-github-actions/variables) -- [GitHub Action Default Environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables) +Upgrading from an older version? Check [CHANGELOG.md](./CHANGELOG.md) for breaking changes. -## ESM +## Related docs +- [GitHub Actions variables](https://docs.github.com/en/actions/learn-github-actions/variables) and [default environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables) - [TypeScript ESM Node](https://www.typescriptlang.org/docs/handbook/esm-node.html) [Cloudflare Pages]: https://pages.cloudflare.com/ @@ -192,5 +201,10 @@ All actions ran while this secret is enabled contain additional diagnostic log f [pull request]: https://docs.github.com/en/pull-requests [GitHub Environments]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment [GitHub Deployment]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment +[Creating an environment]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#creating-an-environment [permissions]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions [`GITHUB_TOKEN`]: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +[Action Debugging]: https://github.com/actions/toolkit/blob/main/docs/action-debugging.md#step-debug-logs +[setting a repository secret]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#creating-encrypted-secrets +[downloaded logs]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#downloading-logs +[web logs]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#viewing-logs-to-diagnose-failures diff --git a/delete/README.md b/delete/README.md index ce7aac2d..a33032c7 100644 --- a/delete/README.md +++ b/delete/README.md @@ -1,27 +1,55 @@ # andykenward/github-actions-cloudflare-pages/delete -Delete deployments made using [`andykenward/github-actions-cloudflare-pages`](../README.md) for the current branch or pull request. +Delete deployments created by [`andykenward/github-actions-cloudflare-pages`](../README.md) for the current branch or pull request. When a [pull request] is closed, it removes that PR's deployments from [Cloudflare Pages] and [GitHub Deployment], along with the related comments. -**The action is only able to delete deployments & comments that are created by `andykenward/github-actions-cloudflare-pages`, as it requires a certain payload in a GitHub deployment.** +> [!IMPORTANT] +> This action can only delete deployments and comments created by [`andykenward/github-actions-cloudflare-pages`](../README.md) — it relies on a specific payload stored in the GitHub deployment. -On closing the [pull request], all the deployments for that pull request will be deleted from [Cloudflare Pages], [GitHub Deployment] and related comments. +**Features** -- Delete Cloudflare Pages deployment. -- Update GitHub deployment status to `INACTIVE` on successfully deleting the Cloudflare Pages deployment. -- Delete GitHub deployment and related comment. -- Output [job summary] of deletion. +- Delete the Cloudflare Pages deployment. +- Mark the GitHub deployment status `INACTIVE` once the Cloudflare Pages deployment is deleted. +- Delete the GitHub deployment and its related comment. +- Write a [job summary] of what was deleted. -## Upgrading +## Quick start + +Run this on `pull_request: closed` to clean up a PR's preview deployments when it's closed or merged (this mirrors the official template in [.github/workflow-templates/delete.yml](../.github/workflow-templates/delete.yml)): + +```yaml +name: Cloudflare Pages Delete +on: + pull_request: + types: [closed] + branches: [main] -If you have previous deployments using an older version of this GitHub Action please see the [CHANGELOG.md](../CHANGELOG.md) for breaking changes. +# Deny all permissions by default; grant only what each job needs. +permissions: {} + +jobs: + delete: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + actions: read # Only required for a private repo. + contents: read + deployments: write + pull-requests: write + steps: + - name: Delete Cloudflare Pages deployment + uses: andykenward/github-actions-cloudflare-pages/delete@1f45924c4dd0c6d746a7edfaa4e1dea8958806a6 #v3.4.0 + with: + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} +``` ## Permissions -The [permissions] required for this GitHub Action when using the created [`GITHUB_TOKEN`] by the workflow for the `github-token` field. +When using the workflow's built-in [`GITHUB_TOKEN`] for the `github-token` input, grant these [permissions]: ```yaml permissions: - actions: read # Only required for a private GitHub Repo. + actions: read # Only required for a private GitHub repo. contents: read deployments: write pull-requests: write @@ -29,55 +57,20 @@ permissions: ## Inputs -```yaml -cloudflare-api-token: - description: 'Cloudflare API Token.' - required: true -github-token: - description: 'Github API key, make sure to add the required permissions for this action.' - required: true -github-environment: - description: 'GitHub environment to delete deployments from. Leave undefined to delete all deployments referencing the current branch or pull_request.' - required: false -keep-latest: - description: 'How many deployments to keep. Default is 0.' - default: '0' - required: false -``` +| Input | Required | Default | Description | +| ---------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `cloudflare-api-token` | yes | — | Cloudflare API Token. | +| `github-token` | yes | — | Github API key, make sure to add the required permissions for this action. | +| `github-environment` | no | — | GitHub environment to delete deployments from. Leave undefined to delete all deployments referencing the current branch or pull_request. | +| `keep-latest` | no | `0` | How many deployments to keep. Default is 0. | ## Examples -See GitHub Workflow example below or [.github/workflow-templates/delete.yml](../.github/workflow-templates/delete.yml) - -### `pull_request` `closed` +A ready-to-use template lives at [.github/workflow-templates/delete.yml](../.github/workflow-templates/delete.yml); the [Quick start](#quick-start) above is the same workflow. -```yaml -# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json - -name: 'Deployment Deletion' -on: - pull_request: - types: - - closed - branches: - - main +## Upgrading -jobs: - deploy-delete: - permissions: - actions: read # Only required for private GitHub Repo - contents: read - deployments: write - pull-requests: write - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Deploy deletion Cloudflare Pages - uses: andykenward/github-actions-cloudflare-pages/delete@1f45924c4dd0c6d746a7edfaa4e1dea8958806a6 #v3.4.0 - with: - cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - github-token: ${{ secrets.GITHUB_TOKEN }} -``` +Upgrading from an older version? Check [CHANGELOG.md](../CHANGELOG.md) for breaking changes. [pull request]: https://docs.github.com/en/pull-requests [Cloudflare Pages]: https://pages.cloudflare.com/ diff --git a/package.json b/package.json index a8f672e9..c1ef6576 100644 --- a/package.json +++ b/package.json @@ -83,10 +83,10 @@ "wrangler": "4.72.0" }, "engines": { - "node": "^24.7.0", - "pnpm": "^11.0.8" + "node": "^24.16.0", + "pnpm": "^11.1.1" }, - "packageManager": "pnpm@11.0.8", + "packageManager": "pnpm@11.1.1", "pnpm": { "peerDependencyRules": { "allowedVersions": { diff --git a/prek.toml b/prek.toml new file mode 100644 index 00000000..f0f16c62 --- /dev/null +++ b/prek.toml @@ -0,0 +1,27 @@ +# Configuration file for `prek`, a git hook framework written in Rust. +# See https://prek.j178.dev for more information. +#:schema https://www.schemastore.org/prek.json + +exclude = "__generated__|.changeset|pnpm-lock.yaml|dist|.devcontainer/devcontainer-lock.json|schema|.agents|.claude" + +[[repos]] +repo = "https://github.com/pre-commit/pre-commit-hooks" +rev = "v6.0.0" +hooks = [ + { id = "trailing-whitespace" }, + { id = "end-of-file-fixer" }, + { id = "check-yaml" }, + { id = "check-added-large-files" }, + { id = "check-merge-conflict" } +] + +[[repos]] +repo = "https://gitlab.com/bmares/check-json5" +rev = "v1.0.1" +hooks = [{ id = "check-json5" }] + +[[repos]] +repo = "local" +hooks = [ + { id = "oxc-format-and-lint", name = "oxfmt + oxlint", entry = "bash ./.claude/scripts/pre-commit-oxc.sh", language = "system", pass_filenames = true, files = '\.(js|jsx|cjs|mjs|ts|tsx|cts|mts|json|md|markdown|graphql|gql|yml|yaml)$' } +]