diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 5660f69..2c020b4 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -20,22 +20,25 @@ jobs:
- name: Checkout 🛎️
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- - name: Setup Python 🔧
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- with:
- python-version: 3.14.5
-
- name: Check workflow files
uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
with:
args: -color
- - name: Test 🔍
- run: |
- # nosemgrep: generic.ci.security.use-frozen-lockfile.use-frozen-lockfile-pip
- pip install semgrep yamllint
- semgrep --config=auto --error
- yamllint .
+ # yamllint and semgrep run from their published images (no host Python),
+ # matching the actionlint step above. Renovate manages the tags + digests.
+ - name: Lint YAML 🔍
+ uses: docker://cytopia/yamllint:1@sha256:596fb19eb71e55ba5b2fa56d8c18a615ec82adc8d3bf2d73918cb78c8f3240fb
+ with:
+ args: .
+
+ - name: Security scan 🔒
+ uses: docker://semgrep/semgrep:1.167.0@sha256:06938c1f365d3f67b8cedd8bc117607ae64253f88a0e768e9da9408548927dd6
+ with:
+ # Set the entrypoint explicitly: this image has no ENTRYPOINT, and
+ # pinning it keeps the step correct if a digest bump reintroduces one.
+ entrypoint: semgrep
+ args: --config=auto --error
autodoc:
timeout-minutes: 5
@@ -44,11 +47,6 @@ jobs:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- - name: Install uv
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
- with:
- enable-cache: false
-
- name: Regenerate documentation
run: ./generate-doc.sh
diff --git a/generate-doc.py b/generate-doc.py
deleted file mode 100644
index 2676568..0000000
--- a/generate-doc.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["pyyaml"]
-# ///
-"""Generate the README Inputs table from action.yml.
-
-Replaces tj-actions/auto-doc: parses the action's inputs and rewrites the
-GitHub-flavoured Markdown table between the AUTO-DOC-INPUT markers in README.md.
-"""
-
-import pathlib
-import re
-
-import yaml
-
-ROOT = pathlib.Path(__file__).parent
-ACTION = ROOT / "action.yml"
-README = ROOT / "README.md"
-START = ""
-END = ""
-
-
-def render_description(text: str) -> str:
- """Render an action.yml description as a single Markdown table cell.
-
- Wrapped prose lines are joined with spaces; `*`-prefixed lines (e.g. the
- list of actions) become `
`-separated bullets so they render as a list
- inside the cell rather than a run of literal asterisks.
- """
- parts: list[str] = []
- for raw in text.strip().splitlines():
- line = raw.strip()
- if not line:
- continue
- if line.startswith("* "):
- parts.append("
• " + line[2:].strip())
- elif parts:
- parts[-1] += " " + line
- else:
- parts.append(line)
- return "".join(parts)
-
-
-def render_table(inputs: dict) -> str:
- rows = [
- "| Input | Type | Required | Default | Description |",
- "| --- | --- | --- | --- | --- |",
- ]
- for name in sorted(inputs):
- spec = inputs[name] or {}
- required = "true" if spec.get("required") else "false"
- default = spec.get("default")
- default_cell = f"`{default}`" if default not in (None, "") else ""
- description = render_description(str(spec.get("description", "")))
- rows.append(f"| `{name}` | string | {required} | {default_cell} | {description} |")
- return "\n".join(rows)
-
-
-def main() -> None:
- action = yaml.safe_load(ACTION.read_text(encoding="utf-8"))
- table = render_table(action.get("inputs") or {})
- block = f"{START}\n\n{table}\n\n{END}"
-
- readme = README.read_text(encoding="utf-8")
- pattern = re.escape(START) + r".*?" + re.escape(END)
- if not re.search(pattern, readme, flags=re.DOTALL):
- raise SystemExit("AUTO-DOC-INPUT markers not found in README.md")
-
- new = re.sub(pattern, lambda _: block, readme, flags=re.DOTALL)
- README.write_text(new, encoding="utf-8", newline="\n")
-
-
-if __name__ == "__main__":
- main()
diff --git a/generate-doc.sh b/generate-doc.sh
index feb77bb..80a22af 100755
--- a/generate-doc.sh
+++ b/generate-doc.sh
@@ -1,7 +1,128 @@
#!/bin/bash
+# Generate the README Inputs table from action.yml.
+#
+# Replaces tj-actions/auto-doc: parses the action's inputs and rewrites the
+# GitHub-flavoured Markdown table between the AUTO-DOC-INPUT markers in
+# README.md. Pure bash + awk so the repo needs no Python/uv toolchain.
+
set -euo pipefail
-command -v uv >/dev/null 2>&1 || { echo "uv is not installed: https://docs.astral.sh/uv/" >&2; exit 1; }
+ROOT="$(cd "$(dirname "$0")" && pwd)"
+ACTION="${ROOT}/action.yml"
+README="${ROOT}/README.md"
+START=""
+END=""
+
+if ! grep -qF "${START}" "${README}" || ! grep -qF "${END}" "${README}"; then
+ echo "AUTO-DOC-INPUT markers not found in README.md" >&2
+ exit 1
+fi
+
+# Render the README inputs table from action.yml. awk emits one finished
+# Markdown row per input; only the constrained GitHub Action inputs schema is
+# handled (2-space input names, 4-space properties, optional `|`/`>`
+# block-scalar descriptions).
+rows="$(
+ awk '
+ function trim(s){ sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
+ # unwrap a scalar: strip matching surrounding quotes (inside which "#" is
+ # literal), otherwise strip an inline " # comment" from a plain scalar.
+ function scalar(s){
+ s = trim(s)
+ if (s ~ /^".*"$/ || s ~ /^'"'"'.*'"'"'$/) return substr(s, 2, length(s) - 2)
+ sub(/[ \t]+#.*$/, "", s)
+ return trim(s)
+ }
+ # render a (newline-joined) description into a single Markdown table cell:
+ # wrapped prose joins with spaces; "* " lines become
-separated bullets.
+ function render(raw, nl, i, line, arr, parts, np, out){
+ np = 0; nl = split(raw, arr, "\n")
+ for (i = 1; i <= nl; i++) {
+ line = trim(arr[i])
+ if (line == "") continue
+ if (substr(line, 1, 2) == "* ") parts[++np] = "
\342\200\242 " trim(substr(line, 3))
+ else if (np > 0) parts[np] = parts[np] " " line
+ else parts[++np] = line
+ }
+ out = ""
+ for (i = 1; i <= np; i++) out = out parts[i]
+ return out
+ }
+ # emit the finished Markdown row; the downstream sort orders rows by the
+ # input name that follows the identical "| `" prefix.
+ function flush( defcell){
+ if (cur == "") return
+ defcell = (def == "") ? "" : "`" def "`"
+ printf "| `%s` | string | %s | %s | %s |\n", cur, req, defcell, render(desc)
+ }
+
+ BEGIN { in_inputs = 0; collecting = 0; cur = "" }
+ {
+ line = $0; sub(/\r$/, "", line)
+ p = match(line, /[^ ]/); ind = (p ? p - 1 : length(line))
+ blank = (line ~ /^[ \t]*$/)
+
+ if (collecting) {
+ if (blank) { desc = desc "\n"; next }
+ if (ind > blockind) { desc = desc (desc == "" ? "" : "\n") line; next }
+ collecting = 0 # dedent: fall through and reprocess this line
+ }
+
+ if (blank) next
+ if (line ~ /^[ \t]*#/) next # full-line comment
+
+ if (ind == 0) { # top-level key
+ flush(); cur = ""
+ in_inputs = (line ~ /^inputs:[ \t]*$/) ? 1 : 0
+ next
+ }
+ if (!in_inputs) next
+
+ if (ind == 2) { # new input name
+ flush()
+ key = line; sub(/:.*$/, "", key); cur = trim(key)
+ req = "false"; def = ""; desc = ""
+ next
+ }
+ if (ind >= 4 && cur != "") { # input property
+ prop = trim(line)
+ if (prop ~ /^description:/) {
+ val = prop; sub(/^description:[ \t]*/, "", val)
+ if (val ~ /^[|>]/) { collecting = 1; blockind = ind; desc = "" }
+ else desc = scalar(val)
+ } else if (prop ~ /^default:/) {
+ val = prop; sub(/^default:[ \t]*/, "", val); def = scalar(val)
+ } else if (prop ~ /^required:/) {
+ val = prop; sub(/^required:[ \t]*/, "", val); val = tolower(scalar(val))
+ req = (val == "true" || val == "yes" || val == "on") ? "true" : "false"
+ }
+ next
+ }
+ }
+ END { flush() }
+ ' "${ACTION}" | LC_ALL=C sort
+)"
+
+# Assemble the table; the rows already carry their Markdown formatting.
+table="| Input | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |"
+if [ -n "${rows}" ]; then
+ table="${table}
+${rows}"
+fi
-exec uv run "$(dirname "$0")/generate-doc.py"
+# Splice the rendered block between the markers (inclusive). The block is read
+# from a file rather than passed via `awk -v`, which rejects embedded newlines.
+tmp="$(mktemp)"
+blockfile="$(mktemp)"
+trap 'rm -f "${tmp}" "${blockfile}"' EXIT
+printf '%s\n\n%s\n\n%s\n' "${START}" "${table}" "${END}" > "${blockfile}"
+awk -v start="${START}" -v end="${END}" -v blockfile="${blockfile}" '
+ index($0, start) { while ((getline l < blockfile) > 0) print l; close(blockfile); skip = 1; next }
+ skip && index($0, end) { skip = 0; next }
+ skip { next }
+ { print }
+' "${README}" > "${tmp}"
+mv "${tmp}" "${README}"
+# tmp + blockfile are cleaned by the EXIT trap above.