From e64417ac277871261be7eaa3287838e767304ee1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 07:09:22 +0300 Subject: [PATCH 1/2] docs: add an agent playbook and a Start here section Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HxpS9r6qDihN2RKxQCwU2R --- AGENTS.md | 21 ++- CHANGELOG.md | 5 + README.md | 1 + docs/agent-playbook.md | 325 +++++++++++++++++++++++++++++++++++++++++ llms.txt | 1 + 5 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 docs/agent-playbook.md diff --git a/AGENTS.md b/AGENTS.md index c6a9628..2c8fde5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,19 @@ Guidance for AI coding agents that work **in** this repository, or that **add th Current tag to use from other repos: `LLazyEmail/email-template-workflows@v1`. +## Start here + +- **Adding this Action to another repository?** Follow [`docs/agent-playbook.md`](docs/agent-playbook.md): inspect the repo, choose a mode, write the workflow, verify. It has copy-paste recipes and a table that maps log messages to fixes. +- **Changing this repository?** Skip to [Working on this repository](#working-on-this-repository). + +The five rules that matter most when adding the Action: + +1. Look at the repository first (lockfile, `package.json` scripts, template files, `.npmrc`, existing workflows). Never invent a script, flag, path or input. +2. Pick one render mode: `command` (one command renders everything), `build-command` (a CLI renders one file; use `$EMAIL_TEMPLATE_INPUT` and `$EMAIL_TEMPLATE_OUTPUT`), or `build-script` (an npm script takes input and output paths). +3. Set `working-directory` (the default `generated` is rarely right) and `artifact-directory` (where the HTML really is). Paths are relative to `working-directory`. +4. Pin `@v1`, put `actions/checkout` first, and give every call its own `artifact-name`. +5. Do not report success until a pull request run shows the artifact, the job summary and a single `### Rendered emails` comment, or say plainly that you could not check. + ## Adding the Action to another repository: decision rules Follow in order and stop at the first match. @@ -74,13 +87,7 @@ Outputs: `html-path` (first rendered file), `html-count`, `failed-count`, `artif ## Verifying a change -After adding or editing a caller workflow: - -1. Confirm the workflow YAML is valid and the `uses:` line is `LLazyEmail/email-template-workflows@v1`. -2. Confirm every package script named in `build-script` (and `validate-script`) exists in the consumer's `package.json`. -3. Confirm `artifact-directory` matches where the build actually writes. -4. Confirm permissions match the table above. -5. Push to a branch and open a pull request; the run should show an artifact named `artifact-name`, a job summary, and a single `### Rendered emails` comment. +Step 4 of the [playbook](docs/agent-playbook.md#step-4-verify) is the checklist: the YAML parses, the scripts and paths exist, `artifact-directory` matches the build, permissions fit, and a pull request run shows an artifact, a job summary and one `### Rendered emails` comment. ## Working on this repository diff --git a/CHANGELOG.md b/CHANGELOG.md index f56a3f0..f0f3058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to `LLazyEmail/email-template-workflows` are listed here. ## [Unreleased] +### Added (agent documentation) +- `docs/agent-playbook.md`: a step-by-step procedure for AI agents that add the Action to a repository (inspect, choose a mode, write the workflow, verify), a decision flowchart, eleven recipes, a table that maps log messages to fixes, and hard rules. +- `AGENTS.md` now opens with a "Start here" section and the five rules that matter most. +- `tests/check-action.py` checks every YAML snippet in the README and the playbook: it must parse, and any call to this Action may only use inputs that exist in `action.yml`. It also checks that relative links in the docs point at files that exist. + ### Added (universal support) - Package managers: `package-manager` (`auto`, `npm`, `yarn`, `pnpm`, `bun`) and `package-manager-version`. `auto` reads the `packageManager` field, then the lockfile in `working-directory` and its parents (monorepos), then falls back to npm. yarn and pnpm run through Corepack, bun through `oven-sh/setup-bun`. - `build-command` and `validate-command`: run any tool once per template, with `EMAIL_TEMPLATE_INPUT` and `EMAIL_TEMPLATE_OUTPUT` set. These variables are now set for `build-script` and `validate-script` too. diff --git a/README.md b/README.md index df4d21a..e95d33e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ steps: ## For AI agents and LLM tools +- [`docs/agent-playbook.md`](docs/agent-playbook.md): start here. Inspect the repository, choose a mode, write the workflow, verify. Copy-paste recipes and a table that maps log messages to fixes. - [`llms.txt`](llms.txt): short index of this repository for LLMs. - [`AGENTS.md`](AGENTS.md): decision rules, input precedence, gotchas and a verification checklist for agents that add this Action to a repository. - [`action.yml`](action.yml): source of truth for inputs, outputs and defaults. diff --git a/docs/agent-playbook.md b/docs/agent-playbook.md new file mode 100644 index 0000000..091b0f6 --- /dev/null +++ b/docs/agent-playbook.md @@ -0,0 +1,325 @@ +# Agent playbook + +How to add `LLazyEmail/email-template-workflows@v1` to a repository, step by step. Written for AI coding agents; humans will find it useful too. + +If you are changing **this** repository instead, read [`AGENTS.md`](../AGENTS.md). + +## What the Action does, in one paragraph + +It is a GitHub composite Action. In a workflow job it detects the package manager, installs the project's dependencies, runs the project's own build to turn email sources (Markdown, MJML, Lit, anything) into HTML files, writes an `index.html` that links them, uploads the folder as a workflow artifact, writes a job summary, and on a pull request posts or updates one comment. It does **not** build templates itself, send email, lint HTML for email-client compatibility, or comment outside pull requests. Your job is to point it at the build the project already has. + +``` +detect package manager → set up Node + Corepack/Bun → install → prebuild-command + → render (command | build-command | run build-script, once per template) + → index.html, rendered.txt, failed.txt, job summary → upload artifact → PR comment + → fail the job if any template failed (only with fail-fast: false) +``` + +## The procedure + +Do these four steps in order. Do not skip step 1: wrong workflows usually come from guessing instead of looking. + +### Step 1: Inspect the repository + +Look for facts. Each one maps to a decision below. + +| Look for | Command | Tells you | +| --- | --- | --- | +| Where `package.json` is | `git ls-files '**/package.json'` | `working-directory` (use `.` if it is at the root) | +| Lockfile | `ls pnpm-lock.yaml yarn.lock bun.lock bun.lockb package-lock.json 2>/dev/null` | package manager (auto-detected; set `package-manager` only to override) | +| `packageManager` field | `grep packageManager package.json` | package manager and version | +| Build scripts | `node -e 'console.log(require("./package.json").scripts)'` | `build-script`, `validate-script`, or a `command` | +| Node version file | `ls .nvmrc .node-version .tool-versions 2>/dev/null` | `node-version-file` | +| Source templates | `git ls-files '*.mjml' '*.md' '*.mdx' '*.hbs' '*.njk'` and the folder names | `templates` or `input` paths | +| Where HTML is written | read the build script or config for the output folder | `output`, `artifact-directory` | +| Private packages | `cat .npmrc`; look for `@scope:registry=` | `registry-url`, `registry-scope`, `packages: read` | +| Existing workflows | `ls .github/workflows` and read any that already render emails | do not create a duplicate; edit the existing one | +| Monorepo markers | `pnpm-workspace.yaml`, `workspaces` in `package.json`, `turbo.json`, `nx.json` | lockfile is at the root, `working-directory` is a package | + +Run the build locally if you can, with the exact command you are about to give the Action. If it does not work on your machine, it will not work in CI. + +### Step 2: Choose the render mode + +Follow the first row that matches. + +```mermaid +flowchart TD + A[Does one command render everything,
e.g. a catalog CLI or npm run sandbox?] -->|yes| C[command mode:
command + artifact-directory] + A -->|no| B[Does an npm script take an input path
and an output path?] + B -->|yes| S[script mode:
build-script + input/output or templates] + B -->|no| T[Is there a CLI that renders one file?
MJML, Maizzle, pandoc, a Python script] + T -->|yes| D[build-command mode:
build-command + templates] + T -->|no| E[Wrap the build in one command
and use command mode] + S --> F{Script uses other argument names?} + F -->|yes| G[input-flag / output-flag, or read
EMAIL_TEMPLATE_INPUT / EMAIL_TEMPLATE_OUTPUT] +``` + +| Mode | Set | The Action runs | Use when | +| --- | --- | --- | --- | +| Command | `command`, `artifact-directory` | the command once; indexes every `*.html` under `artifact-directory` at any depth | one command renders a whole folder | +| Script | `build-script` plus `input`+`output` or `templates` | ` run --input X --output Y` per template | an npm script takes input and output paths | +| Build-command | `build-command` plus `templates` or `input`+`output` | your shell command per template, with `$EMAIL_TEMPLATE_INPUT` and `$EMAIL_TEMPLATE_OUTPUT` | the tool is not an npm script | + +Precedence, when several are set: `command` beats `build-command` beats `build-script`; `templates` beats `input`+`output`; `validate-command` beats `validate-script`. Do not set conflicting inputs on purpose; the lower ones are silently ignored. + +### Step 3: Write the workflow + +Start from this skeleton and fill the marked parts. Everything not marked is deliberate. + +```yaml +name: Render emails + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + pull-requests: write # drop this line (and set pr-comment: false) if you do not want the comment + +jobs: + render: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . # FILL: folder with package.json + # FILL: the render mode inputs from step 2 + artifact-directory: generated # FILL: folder the HTML is written to + artifact-name: rendered-email +``` + +Rules that apply to every workflow: + +1. `actions/checkout` comes first. +2. Set `working-directory` explicitly. Its default, `generated`, is almost never right. +3. All path inputs (`input`, `output`, `templates`, `artifact-directory`) are relative to `working-directory`. `node-version-file` is the exception: it is relative to the repository root. +4. `artifact-directory` must be the folder the build really writes to. +5. In a matrix, or when calling the Action more than once in a job, give every call its own `artifact-name`, and either set `pr-comment: false` or give each call its own `comment-key`. +6. Keep secrets out of inputs. Pass them through the job or step `env:`. +7. Pin `@v1`, never `@main`. Add `# vX.Y.Z` and a commit SHA only if the repository already pins actions by SHA. + +### Step 4: Verify + +1. The YAML parses: `python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" .github/workflows/render-emails.yml` (or `actionlint` if installed). +2. Every script named in `build-script` or `validate-script` exists in the `package.json` that `working-directory` points at. +3. Every path in `templates` / `input` exists: `git ls-files `. +4. The `artifact-directory` matches where the build writes. +5. Permissions: `contents: read`, plus `pull-requests: write` only if the comment is wanted, plus `packages: read` for private GitHub Packages. +6. Open a pull request. The run should show an artifact named `artifact-name`, a job summary listing the files, and **one** `### Rendered emails` comment. + +Do not report success until step 6 has happened, or tell the user plainly that it has not. + +## Recipes + +Each recipe shows only the Action call; wrap it in the skeleton above. All of them are checked against `action.yml` in CI, so they use real inputs. + +### An npm script that takes `--input` and `--output` + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . + build-script: build-email + templates: | + src/welcome.md:generated/welcome.html + src/invoice.md:generated/invoice.html + validate-script: "" # no validate-email script in this project + artifact-directory: generated +``` + +### A script with different argument names + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . + build-script: render + input-flag: --src= # passes --src=; "--src" passes --src ; "" passes the path alone + output-flag: --dest= + input: emails/welcome.md + output: dist/welcome.html + validate-script: "" + artifact-directory: dist +``` + +A script that reads its paths from the environment needs no flags at all: `EMAIL_TEMPLATE_INPUT` and `EMAIL_TEMPLATE_OUTPUT` are always set. + +### MJML, Maizzle, pandoc or any other CLI + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . + templates: | + emails/welcome.mjml:dist/welcome.html + emails/invoice.mjml:dist/invoice.html + build-command: npx mjml "$EMAIL_TEMPLATE_INPUT" -o "$EMAIL_TEMPLATE_OUTPUT" + validate-script: "" + artifact-directory: dist +``` + +Always quote `$EMAIL_TEMPLATE_INPUT` and `$EMAIL_TEMPLATE_OUTPUT`. Do not paste the paths into the command yourself; the variables are what make it work for every template. + +### A tool that is not installed on the runner + +Install it in a step before the Action. `skip-install: true` skips the Node dependency install when the project has no `package.json`. + +```yaml +- run: sudo apt-get update && sudo apt-get install -y pandoc +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . + skip-install: true + templates: | + emails/welcome.md:dist/welcome.html + build-command: pandoc --standalone "$EMAIL_TEMPLATE_INPUT" -o "$EMAIL_TEMPLATE_OUTPUT" + validate-script: "" + artifact-directory: dist +``` + +Node.js must still be on `PATH`, because the Action uses it to index the output. Leave `setup-node` at its default, or install Node yourself and set `setup-node: false`. + +### One command renders everything (a catalog) + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . + command: npx generate-template --all --out=generated + artifact-directory: generated +``` + +### A pnpm, yarn or bun project + +Usually nothing to add: the package manager is detected from `packageManager`, then the lockfile. To enforce the lockfile or pin a version: + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . + package-manager: pnpm + package-manager-version: "10.28.0" + install-command: pnpm install --frozen-lockfile + cache: auto + build-script: build-email + input: src/welcome.md + output: generated/welcome.html + validate-script: "" +``` + +### A monorepo + +The lockfile lives at the repository root; the emails are in a package. Detection and `cache: auto` look in parent folders. + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: packages/emails + cache: auto + install-command: pnpm install --frozen-lockfile --filter "@acme/emails..." + prebuild-command: pnpm --filter @acme/components build + build-script: build-email + templates: | + src/welcome.md:generated/welcome.html + validate-script: "" + artifact-directory: generated +``` + +### One job per template (a matrix) + +See [`examples/matrix-script-mode.yml`](../examples/matrix-script-mode.yml). The two rules are `pr-comment: false` and a unique `artifact-name` per row. + +### Private packages + +```yaml +permissions: + contents: read + packages: read + pull-requests: write +``` + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + working-directory: . + registry-url: https://npm.pkg.github.com + registry-scope: "@my-org" + command: npm run render + artifact-directory: generated +``` + +### Keep going when one template is broken + +```yaml +- uses: LLazyEmail/email-template-workflows@v1 + with: + working-directory: . + templates: | + emails/a.md:dist/a.html + emails/b.md:dist/b.html + fail-fast: false + validate-script: "" + artifact-directory: dist +``` + +The good templates are rendered, uploaded and commented; `failed.txt` in the artifact lists the broken ones; the job fails at the very end. + +### Two sets of emails in one pull request + +Give each call its own `comment-key` and `artifact-name`. See [`examples/keep-going-two-comments.yml`](../examples/keep-going-two-comments.yml). + +### A scheduled or manual run, no pull request + +Set `pr-comment: false`. The job summary and the artifact still list everything. See [`examples/scheduled-summary-only.yml`](../examples/scheduled-summary-only.yml). + +## When something goes wrong + +Match the message in the log, then apply the fix. + +| You see | Cause | Fix | +| --- | --- | --- | +| `Process completed with exit code 1` with no `::error::` above it, during install | the install failed before the render step started | read the install output; common causes are an out-of-sync lockfile with `npm ci`, a private package without `registry-url` and `NODE_AUTH_TOKEN`, or a missing `packages: read` | +| `missing artifact-directory` | `command` is set without `artifact-directory` | set `artifact-directory` to the folder the command writes | +| `artifact-directory not found` | the command did not create that folder, or the path is relative to the wrong place | `artifact-directory` is relative to `working-directory`; fix the path or the command | +| `build-script not found` | the script is not in the `package.json` at `working-directory` | add the script, fix `build-script`, fix `working-directory`, or use `build-command` or `command` | +| `invalid template line` | a `templates` line is not `input:output`, the two are equal, or a path contains a colon | one `input:output` per line, different paths, no colons in paths | +| `render failed for ` | the build exited non-zero | the build's own output is just above the error; run the same command locally | +| `validation failed for ` | the validate script or command exited non-zero | fix the HTML, or set `validate-script: ""` if the project has no real validator | +| `empty or missing output` | the build exited 0 but wrote nothing at the output path | the build ignores the output argument or writes elsewhere; check `output-flag`, or use `build-command` with `$EMAIL_TEMPLATE_OUTPUT` | +| `No templates rendered` | nothing was selected to render | set `command`, `templates`, or `input` and `output` | +| warning `validate-script 'validate-email' is not defined` | the default validate script is missing | harmless; set `validate-script: ""` to silence it | +| warning `Email may be clipped` | a file is larger than `size-limit-kb` (default 102); Gmail clips larger messages | shrink the email, or raise or disable (`0`) the limit | +| warning `Could not post the pull request comment (HTTP 403)` | read-only token (Dependabot, some forks) or missing `pull-requests: write` | add the permission, or set `pr-comment: false`; the artifact and summary are unaffected | +| artifact upload fails because the name already exists | two calls or matrix rows share an `artifact-name` | give each its own `artifact-name` | +| `Dependencies lock file is not found` | `cache` is set but there is no lockfile | use `cache: auto`, or leave `cache` empty | +| `node must be on PATH` | `setup-node: false` and no Node.js installed | install Node.js first, or keep `setup-node: true` | +| the wrong package manager ran | several lockfiles, or none | set `package-manager` | +| the comment appears twice, or is overwritten | several calls in one pull request | `comment-key` per call, or `pr-comment: false` | + +## Hard rules + +- Look before you write (step 1). Never invent a script name, a flag, a path or an input. +- Only use inputs that exist in [`action.yml`](../action.yml). Do not guess names such as `build-cmd` or `output-dir`. +- Do not use `@main`. Use `@v1`. +- Do not hard-code a package manager or `npm ci` unless the user asked; detection is on by default. +- Do not put the `$EMAIL_TEMPLATE_*` values into a command by string-building; quote the variables. +- Do not put values with spaces in `extra-args`; it is split on whitespace and applies to `build-script` only. +- Do not commit generated HTML to make the Action pass. It is generated in CI and uploaded. +- Do not edit `action.yml`, `scripts/` or the comment markers when you are only adding the Action to another repository. + +## Reference + +| Thing | Where | +| --- | --- | +| Every input, default and output | [`action.yml`](../action.yml), [README](../README.md#inputs) | +| What each build receives | `EMAIL_TEMPLATE_INPUT` and `EMAIL_TEMPLATE_OUTPUT`, relative to `working-directory` | +| Files in the artifact | `index.html` (links and sizes), `rendered.txt` (one rendered path per line), `failed.txt` (only when something failed) | +| Outputs | `html-path`, `html-count`, `failed-count`, `artifact-dir`, `artifact-url`, `package-manager` | +| Comment markers (do not change) | hidden `` (keyed: ``), heading `### Rendered emails` | +| Working examples | [`examples/`](../examples/) | +| A real consumer | `LLazyEmail/_playing_with_lit`, explained in [`real-world-example.md`](real-world-example.md) | diff --git a/llms.txt b/llms.txt index 5572dde..4505423 100644 --- a/llms.txt +++ b/llms.txt @@ -25,6 +25,7 @@ The package manager is detected (`package-manager: auto`) from the `packageManag ## Docs +- [Agent playbook](https://raw.githubusercontent.com/LLazyEmail/email-template-workflows/main/docs/agent-playbook.md): start here when adding this Action to a repository. Inspect, choose a mode, write the workflow, verify; copy-paste recipes; a table from log messages to fixes; hard rules - [README](https://raw.githubusercontent.com/LLazyEmail/email-template-workflows/main/README.md): overview, usage, full inputs table, outputs, behavior notes - [AGENTS.md](https://raw.githubusercontent.com/LLazyEmail/email-template-workflows/main/AGENTS.md): decision rules, input precedence, gotchas, and a checklist for agents wiring this Action into a repository - [action.yml](https://raw.githubusercontent.com/LLazyEmail/email-template-workflows/main/action.yml): the source of truth for inputs, outputs, defaults, and implementation From 5274e1eca7ccd3befbfc22040806be2f2e27afb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 07:10:47 +0300 Subject: [PATCH 2/2] docs: check agent docs against action.yml, mention failure causes - tests/check-action.py now parses every yaml block in the docs, fails when a call to this Action uses an input that is not in action.yml or is not pinned to @v1, and checks relative links and #anchors in the docs - the metadata workflow runs when AGENTS.md, llms.txt or docs/ change - the playbook tells agents to read the failure annotation first, which now carries the first cause line (#26) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HxpS9r6qDihN2RKxQCwU2R --- .github/workflows/render-email-template.yml | 6 ++ AGENTS.md | 3 +- README.md | 2 +- docs/agent-playbook.md | 10 +- tests/check-action.py | 107 ++++++++++++++++++++ 5 files changed, 123 insertions(+), 5 deletions(-) diff --git a/.github/workflows/render-email-template.yml b/.github/workflows/render-email-template.yml index 468321b..b5825b6 100644 --- a/.github/workflows/render-email-template.yml +++ b/.github/workflows/render-email-template.yml @@ -5,6 +5,9 @@ on: paths: - action.yml - README.md + - AGENTS.md + - llms.txt + - docs/** - scripts/** - tests/check-action.py - .github/workflows/** @@ -13,6 +16,9 @@ on: paths: - action.yml - README.md + - AGENTS.md + - llms.txt + - docs/** - scripts/** - tests/check-action.py - .github/workflows/** diff --git a/AGENTS.md b/AGENTS.md index 2c8fde5..db2c168 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,4 +110,5 @@ Step 4 of the [playbook](docs/agent-playbook.md#step-4-verify) is the checklist: - Real consumer: `LLazyEmail/_playing_with_lit` → `.github/workflows/render-email-action.yml` (explained in `docs/real-world-example.md`) ## Failure annotations -A non-zero build, validation, or command exits with an `::error::` annotation. The annotation includes the first cause line from the command (for example `error TS2307: Cannot find module ...`), and the failure block reprints that cause plus the last 20 lines. + +A non-zero build, validation or command exits with an `::error::` annotation. The annotation includes the first cause line from the command (for example `error TS2307: Cannot find module ...`), and the failure block reprints that cause plus the last 20 lines. The playbook's [troubleshooting table](docs/agent-playbook.md#when-something-goes-wrong) maps each message to a fix. diff --git a/README.md b/README.md index e95d33e..b9a7031 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ Your own `env:` on the job is passed through as usual, so secrets such as `NODE_ bash tests/detect-package-manager.test.sh # package manager, lockfile and cache resolution bash tests/render.test.sh # every render mode, with each installed package manager node tests/pr-comment.test.js # the pull request comment, with a mocked GitHub client -python3 tests/check-action.py # action.yml, README and pinned actions stay in sync +python3 tests/check-action.py # action.yml, docs, snippets, links and pinned actions stay in sync ``` CI runs these on Linux, macOS (including the system bash 3.2) and Windows, lints the YAML, shell scripts and workflows (`yamllint`, `shellcheck`, `actionlint`), and renders `fixtures/consumer` through `uses: ./` with each package manager. `.github/workflows/fixture-render.yml` has one job per capability. diff --git a/docs/agent-playbook.md b/docs/agent-playbook.md index 091b0f6..e64a88b 100644 --- a/docs/agent-playbook.md +++ b/docs/agent-playbook.md @@ -279,7 +279,11 @@ Set `pr-comment: false`. The job summary and the artifact still list everything. ## When something goes wrong -Match the message in the log, then apply the fix. +**Read the failure annotation first.** On a failed render, validation or command, the run page shows an `::error::` annotation, for example `render failed for src/welcome.md: error TS2307: Cannot find module './footer'`. The text after the colon is the first cause line from your build's own output. Below it, in the log, the failure block repeats the cause and the last 20 lines of the command, then lists what to check next. Fix that cause before anything else. + +How to read a failed run without a browser: `gh run view --log-failed`, or the check-run annotations through `gh api repos/{owner}/{repo}/check-runs/{id}/annotations`. + +Then match the message, and apply the fix. | You see | Cause | Fix | | --- | --- | --- | @@ -288,8 +292,8 @@ Match the message in the log, then apply the fix. | `artifact-directory not found` | the command did not create that folder, or the path is relative to the wrong place | `artifact-directory` is relative to `working-directory`; fix the path or the command | | `build-script not found` | the script is not in the `package.json` at `working-directory` | add the script, fix `build-script`, fix `working-directory`, or use `build-command` or `command` | | `invalid template line` | a `templates` line is not `input:output`, the two are equal, or a path contains a colon | one `input:output` per line, different paths, no colons in paths | -| `render failed for ` | the build exited non-zero | the build's own output is just above the error; run the same command locally | -| `validation failed for ` | the validate script or command exited non-zero | fix the HTML, or set `validate-script: ""` if the project has no real validator | +| `render failed for : ` | the build exited non-zero; `` is its first error line (a TypeScript error, a missing module, a syntax error) | fix that cause; run the same command locally to reproduce it | +| `validation failed for : ` | the validate script or command exited non-zero | fix what the cause names, or set `validate-script: ""` if the project has no real validator | | `empty or missing output` | the build exited 0 but wrote nothing at the output path | the build ignores the output argument or writes elsewhere; check `output-flag`, or use `build-command` with `$EMAIL_TEMPLATE_OUTPUT` | | `No templates rendered` | nothing was selected to render | set `command`, `templates`, or `input` and `output` | | warning `validate-script 'validate-email' is not defined` | the default validate script is missing | harmless; set `validate-script: ""` to silence it | diff --git a/tests/check-action.py b/tests/check-action.py index 5c65cda..e2c914e 100644 --- a/tests/check-action.py +++ b/tests/check-action.py @@ -13,8 +13,12 @@ * every environment variable that scripts/render.sh and the other scripts read from the environment is provided by action.yml * every third-party `uses:` is pinned to a full commit SHA + * every ```yaml block in the docs parses, and every call to this Action in the + docs and in examples/ uses only inputs that exist in action.yml, pinned to @v1 + * relative links and #anchors in the docs point at files and headings that exist """ import glob +import os import re import sys @@ -129,6 +133,109 @@ def ok(message): error(f"{path}:{number} pins a SHA without a version comment") ok(f"{checked} workflow/action references pinned to a SHA") +# --------------------------------------------------------------------------- +# Documentation: snippets and links +# --------------------------------------------------------------------------- +DOCS = ["README.md", "AGENTS.md", "llms.txt", "CHANGELOG.md"] + sorted(glob.glob("docs/*.md")) +ACTION_REF = re.compile(r"^LLazyEmail/email-template-workflows@") + + +def yaml_blocks(path): + """Yield (line number, text) for every ```yaml / ```yml fenced block.""" + with open(path, encoding="utf-8") as handle: + text = handle.read() + for match in re.finditer(r"^```(?:yaml|yml)[ \t]*\n(.*?)^```[ \t]*$", text, re.M | re.S): + yield text.count("\n", 0, match.start()) + 2, match.group(1) + + +def action_calls(node): + """Yield every mapping in a parsed YAML tree that calls this Action.""" + if isinstance(node, dict): + uses = node.get("uses") + if isinstance(uses, str) and ACTION_REF.match(uses): + yield node + for value in node.values(): + yield from action_calls(value) + elif isinstance(node, list): + for item in node: + yield from action_calls(item) + + +def check_calls(label, tree): + count = 0 + for call in action_calls(tree): + count += 1 + if not call["uses"].endswith("@v1"): + error(f"{label}: '{call['uses']}' must be pinned to @v1") + for key in (call.get("with") or {}): + if key not in inputs: + error(f"{label}: '{key}' is not an input of this action") + return count + + +print("documentation snippets") +snippets = 0 +calls = 0 +for path in DOCS: + for line, block in yaml_blocks(path): + snippets += 1 + label = f"{path}:{line}" + try: + tree = yaml.safe_load(block) + except yaml.YAMLError as exc: + error(f"{label}: YAML snippet does not parse ({str(exc).splitlines()[0]})") + continue + calls += check_calls(label, tree) +for path in sorted(glob.glob("examples/*.yml")): + with open(path, encoding="utf-8") as handle: + calls += check_calls(path, yaml.safe_load(handle)) +ok(f"{snippets} YAML snippets parse; {calls} calls to this action use only real inputs") + +print("documentation links") + + +def slug(heading): + """GitHub's heading anchor: lowercase, drop punctuation, spaces to hyphens.""" + heading = re.sub(r"[`*_]", "", heading.strip().lower()) + heading = re.sub(r"[^\w\- ]", "", heading) + return heading.replace(" ", "-") + + +anchor_cache = {} + + +def anchors(path): + if path not in anchor_cache: + found = set() + in_fence = False + with open(path, encoding="utf-8") as handle: + for line in handle: + if line.startswith("```"): + in_fence = not in_fence + elif not in_fence: + match = re.match(r"#{1,6}\s+(.*)", line) + if match: + found.add(slug(match.group(1))) + anchor_cache[path] = found + return anchor_cache[path] + + +links = 0 +for path in DOCS: + with open(path, encoding="utf-8") as handle: + text = re.sub(r"```.*?```", "", handle.read(), flags=re.S) + for target in re.findall(r"\]\(([^)\s]+)\)", text): + if re.match(r"[a-z][a-z0-9+.-]*:", target): + continue + file_part, _, fragment = target.partition("#") + resolved = os.path.normpath(os.path.join(os.path.dirname(path), file_part)) if file_part else path + links += 1 + if not os.path.exists(resolved): + error(f"{path}: link '{target}' points at a file that does not exist") + elif fragment and resolved.endswith(".md") and fragment not in anchors(resolved): + error(f"{path}: link '{target}' points at a heading that does not exist") +ok(f"{links} relative links resolve") + print() if errors: print(f"{len(errors)} problem(s)")