diff --git a/.github/actions/actionlint/action.yml b/.github/actions/actionlint/action.yml new file mode 100644 index 0000000..08ed1ec --- /dev/null +++ b/.github/actions/actionlint/action.yml @@ -0,0 +1,60 @@ +name: actionlint +description: Lint GitHub Actions workflow files with actionlint. + +inputs: + paths: + description: >- + Workflow files/dirs to lint, space-separated. Empty (default) lets + actionlint auto-discover .github/workflows/*.{yml,yaml}. + default: '' + config-file: + description: >- + Path to an actionlint.yaml config in the caller repo. Empty (default) + auto-discovers .github/actionlint.yaml if one exists. + default: '' + color: + description: Force colored output (true/false). + default: 'true' + version: + description: Exact actionlint version to install. + default: 1.7.12 + sha256: + description: >- + SHA-256 of the linux_amd64 .tar.gz release asset for `version`. Change + together with `version`. + default: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 + +runs: + using: composite + steps: + - name: Install actionlint + shell: bash + env: + VERSION: ${{ inputs.version }} + SHA256: ${{ inputs.sha256 }} + run: | + set -euo pipefail + url="https://github.com/rhysd/actionlint/releases/download/v${VERSION}/actionlint_${VERSION}_linux_amd64.tar.gz" + curl -fsSL "$url" -o actionlint.tar.gz + echo "${SHA256} actionlint.tar.gz" | sha256sum -c - + tar -xzf actionlint.tar.gz actionlint + sudo install actionlint /usr/local/bin/actionlint + rm -f actionlint actionlint.tar.gz + actionlint --version + + - name: Lint workflows + shell: bash + env: + PATHS: ${{ inputs.paths }} + CONFIG_FILE: ${{ inputs.config-file }} + COLOR: ${{ inputs.color }} + run: | + set -euo pipefail + # shellcheck/pyflakes are auto-detected on PATH (ubuntu-latest ships + # shellcheck), so embedded run: scripts are linted too. + args=() + if [[ "$COLOR" == "true" ]]; then args+=(-color); fi + if [[ -n "${CONFIG_FILE// }" ]]; then args+=(-config-file "$CONFIG_FILE"); fi + # $PATHS unquoted so multiple targets word-split; empty appends nothing + # and actionlint auto-discovers .github/workflows. + actionlint "${args[@]}" $PATHS diff --git a/.github/actions/check-jsonschema/action.yml b/.github/actions/check-jsonschema/action.yml new file mode 100644 index 0000000..bddfa51 --- /dev/null +++ b/.github/actions/check-jsonschema/action.yml @@ -0,0 +1,69 @@ +name: check-jsonschema +description: >- + Validate JSON/YAML files against one JSON Schema with check-jsonschema. One + invocation = one schema source + one-or-more files; call once per schema group. + +inputs: + files: + description: Space-separated files to validate (relative to the caller repo). + required: true + builtin-schema: + description: >- + Name of a built-in vendored schema (e.g. vendor.dependabot, + vendor.github-workflows, vendor.github-issue-forms). Mutually exclusive + with schemafile; set exactly one. + default: '' + schemafile: + description: >- + Path or URL of a caller-supplied JSON Schema. Mutually exclusive with + builtin-schema; set exactly one. + default: '' + no-cache: + description: Disable schema/$ref caching (true/false). + default: 'false' + output-format: + description: 'Output format: TEXT or JSON. Empty uses the tool default (TEXT).' + default: '' + version: + description: Exact check-jsonschema version to run. + default: 0.37.3 + python-version: + description: Python version uv provisions for the ephemeral tool environment. + default: '3.14' + +runs: + using: composite + steps: + - name: Set up uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + python-version: ${{ inputs.python-version }} + + - name: Validate + shell: bash + env: + FILES: ${{ inputs.files }} + BUILTIN: ${{ inputs.builtin-schema }} + SCHEMAFILE: ${{ inputs.schemafile }} + NO_CACHE: ${{ inputs.no-cache }} + OUTPUT_FORMAT: ${{ inputs.output-format }} + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + if [[ -z "${FILES// }" ]]; then + echo '::error::files is required.'; exit 1 + fi + # Exactly one schema source (the tool treats them as mutually exclusive). + if [[ -n "${BUILTIN// }" && -n "${SCHEMAFILE// }" ]]; then + echo '::error::Set only one of builtin-schema or schemafile, not both.'; exit 1 + fi + if [[ -z "${BUILTIN// }" && -z "${SCHEMAFILE// }" ]]; then + echo '::error::Set one of builtin-schema or schemafile.'; exit 1 + fi + args=() + if [[ -n "${BUILTIN// }" ]]; then args+=(--builtin-schema "$BUILTIN"); fi + if [[ -n "${SCHEMAFILE// }" ]]; then args+=(--schemafile "$SCHEMAFILE"); fi + if [[ "$NO_CACHE" == "true" ]]; then args+=(--no-cache); fi + if [[ -n "${OUTPUT_FORMAT// }" ]]; then args+=(--output-format "$OUTPUT_FORMAT"); fi + # $FILES unquoted so multiple targets word-split. + uvx "check-jsonschema@${VERSION}" "${args[@]}" $FILES diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2cb373..9c07218 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,17 +139,48 @@ jobs: - name: Scan for secrets uses: ./.github/actions/gitleaks + actionlint: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Lint workflows + uses: ./.github/actions/actionlint + + jsonschema: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Validate dependabot.yml + uses: ./.github/actions/check-jsonschema + with: + builtin-schema: vendor.dependabot + files: .github/dependabot.yml + - name: Validate workflows + uses: ./.github/actions/check-jsonschema + with: + builtin-schema: vendor.github-workflows + files: .github/workflows/ci.yml .github/workflows/link-check.yml + ci-status: if: always() - needs: [markdown, powershell, links, ruff, pyright, biome, tsc, typos, editorconfig, gitleaks] + needs: [markdown, powershell, links, ruff, pyright, biome, tsc, typos, editorconfig, gitleaks, actionlint, jsonschema] runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Aggregate lane results env: - RESULTS: ${{ needs.markdown.result }} ${{ needs.powershell.result }} ${{ needs.links.result }} ${{ needs.ruff.result }} ${{ needs.pyright.result }} ${{ needs.biome.result }} ${{ needs.tsc.result }} ${{ needs.typos.result }} ${{ needs.editorconfig.result }} ${{ needs.gitleaks.result }} + RESULTS: ${{ needs.markdown.result }} ${{ needs.powershell.result }} ${{ needs.links.result }} ${{ needs.ruff.result }} ${{ needs.pyright.result }} ${{ needs.biome.result }} ${{ needs.tsc.result }} ${{ needs.typos.result }} ${{ needs.editorconfig.result }} ${{ needs.gitleaks.result }} ${{ needs.actionlint.result }} ${{ needs.jsonschema.result }} run: | - for r in $RESULTS; do + read -ra results <<< "$RESULTS" + for r in "${results[@]}"; do case "$r" in success|skipped) ;; *) echo "A lane did not pass (result: $r)."; exit 1 ;; diff --git a/docs/dedup-program/plan.md b/docs/dedup-program/plan.md index c1c283c..c5ea4f9 100644 --- a/docs/dedup-program/plan.md +++ b/docs/dedup-program/plan.md @@ -50,8 +50,8 @@ missing, so there is no config work and both standards and medley benefit. ## Phase 2 — Actions and security linting -- [ ] `actionlint` composite action -- [ ] `check-jsonschema` (YAML schema) composite action +- [x] `actionlint` composite action +- [x] `check-jsonschema` (YAML schema) composite action - [ ] `zizmor` reusable workflow (Actions security lint) - [ ] `osv-scanner` reusable workflow (dependency vuln scan) diff --git a/docs/dedup-program/research/actionlint.md b/docs/dedup-program/research/actionlint.md new file mode 100644 index 0000000..8df7583 --- /dev/null +++ b/docs/dedup-program/research/actionlint.md @@ -0,0 +1,67 @@ +# Research — actionlint + +GitHub Actions workflow linter. Upstream: . + +## Version + +- Latest stable: **v1.7.12**, published 2026-03-30 (GitHub releases API, + `repos/rhysd/actionlint/releases/latest`, verified 2026-06-23). +- `medley` already pins `1.7.12` in its inline lane, so the action default + matches and adoption is behavior-preserving. + +## Install asset + checksum + +- Linux x64 asset: `actionlint_1.7.12_linux_amd64.tar.gz`; the binary is + `actionlint` at the archive root (no subdir). +- SHA-256: `8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8`. +- Corroboration: matches the upstream-published `actionlint_1.7.12_checksums.txt` + release asset and a locally computed hash of the download (both agree). The + `taiki-e/install-action` manifest does not cover actionlint (it is not a + cargo/Rust tool), so that cross-check is N/A. + +## Invocation + +`medley` runs (lift-and-shift target): + +```sh +actionlint -color +``` + +- With **no path arguments, actionlint auto-discovers** all workflow files under + `.github/workflows/*.{yml,yaml}` of the current repo. Explicit paths are an + optional override. +- `-color` forces colored output (the documented CI idiom). +- Exit code is non-zero when problems are found (CI-failing). + +### shellcheck / pyflakes integration + +actionlint shells out to `shellcheck` (for `run:` bash/sh blocks) and `pyflakes` +(for `shell: python` blocks) **when those binaries are on `PATH`**, and silently +skips the integration otherwise. GitHub-hosted `ubuntu-latest` ships shellcheck, +so the workflow-embedded `run:` scripts in a caller repo are shellchecked by +default — a desirable extra lint, but it means a caller's workflow scripts must +be shellcheck-clean. Disable explicitly with empty overrides (`-shellcheck=`, +`-pyflakes=`) if ever needed. + +## Config (backfill) + +actionlint auto-loads an optional `.github/actionlint.yaml` (or `.yml`) for +self-hosted-runner labels, declared `config-variables`, and path-specific config; +`-config-file ` points elsewhere. **Config-light:** `medley` ships no such +file and the lifted lane needs none, so no `standards` module is required for +this tool. The action exposes an optional `config-file` input (default empty = +auto-discover) for callers that later add one. + +## Relevant flags (exposed as optional inputs / behind defaults) + +`-color`, `-config-file `, `-shellcheck ` / `-pyflakes ` +(empty disables), `-ignore ` (repeatable message filter), +`-format `. + +## Why curl + sha256 (not download-actionlint.bash or a third-party action) + +`medley` installs via the upstream `download-actionlint.bash` bootstrap. This +repo's established idiom for single-binary GitHub-release tools is curl + +checksum-pin (see `shellcheck`, `editorconfig`, `lychee-offline`): self-contained, +no nested action, the checksum an explicit input. actionlint is the same shape, so +the action follows that idiom. diff --git a/docs/dedup-program/research/check-jsonschema.md b/docs/dedup-program/research/check-jsonschema.md new file mode 100644 index 0000000..67e76f3 --- /dev/null +++ b/docs/dedup-program/research/check-jsonschema.md @@ -0,0 +1,81 @@ +# Research — check-jsonschema + +JSON/YAML instance validator against JSON Schemas. PyPI package +`check-jsonschema`; upstream: . + +## Version + +- Latest stable: **0.37.3**, published 2026-06-12 (agrees across the GitHub + releases API `repos/python-jsonschema/check-jsonschema/releases/latest` and the + PyPI JSON API `pypi.org/pypi/check-jsonschema/json`, verified 2026-06-23). +- `requires_python >=3.10`. + +## Install + invocation + +This repo runs Python tools through `uvx` (see the `ruff` and `pyright` actions), +not a checksum-pinned binary — check-jsonschema is a pure-Python wheel with no +single-binary release. + +- The wheel's console-script entry point is `check-jsonschema = check_jsonschema:main`, + so `uvx check-jsonschema@0.37.3 ...` runs it with no `--from`. +- Pin the exact version: the **built-in/vendored schemas are bundled per release** + and drift across versions, so reproducibility requires an exact pin. + +`medley` runs (lift-and-shift target) — four separate invocations in one job, one +schema per call: + +```sh +check-jsonschema --builtin-schema vendor.dependabot .github/dependabot.yml +check-jsonschema --schemafile tools/schemas/lefthook.schema.json lefthook.yml +check-jsonschema --builtin-schema vendor.github-issue-forms
+check-jsonschema --builtin-schema vendor.github-issue-config .github/ISSUE_TEMPLATE/config.yml +``` + +## Schema selection model — one schema + N files per call + +A single invocation takes **exactly one** schema source and one-or-more target +files: `check-jsonschema file1 [file2 …]`. The three schema +sources are mutually exclusive per call: + +- `--builtin-schema ` — a vendored schema bundled with check-jsonschema. +- `--schemafile ` — a caller-supplied schema file. +- `--check-metaschema` — validate the files as schemas themselves. + +Built-in schema names in 0.37.3 (verified against the wheel's +`builtin_schemas/vendor/` directory, not just docs) — 26 `vendor.*` schemas: +`azure-pipelines`, `bamboo-spec`, `bitbucket-pipelines`, `buildkite`, `changie`, +`circle-ci`, `citation-file-format`, `cloudbuild`, `codecov`, `compose-spec`, +`dependabot`, `drone-ci`, `github-actions`, `github-discussion`, +`github-issue-config`, `github-issue-forms`, `github-workflows`, `gitlab-ci`, +`meltano`, `mergify`, `readthedocs`, `renovate`, `snapcraft`, `taskfile`, +`travis`, `woodpecker-ci` (each prefixed `vendor.`), plus one custom schema +`github-workflows-require-timeout` (no `vendor.` prefix). + +## No config file — design implication + +**Definitively config-light.** check-jsonschema reads **no** project config of its +own — no dotfile, no `pyproject.toml [tool.check-jsonschema]` table, no central +schema↔files mapping (verified against the 0.37.3 wheel source: no config-reading +code in `check_jsonschema/cli/`). The only places a schema↔files mapping lives as +"config" are a pre-commit hook list or the caller's own workflow. The schema for a +custom `--schemafile` (e.g. `medley`'s vendored `lefthook.schema.json`) is a +caller-owned file, not a `standards` module. + +Consequence for the action: it models **one invocation** (one schema source + +target files via inputs); a caller that validates several schema groups (as +`medley` does) calls the action once per group. No `standards` module is created +for this tool. + +## Other relevant flags (optional inputs / future backfill) + +`--no-cache` (disable `$ref`/schema caching — sensible in CI), `-o/--output-format` +(`TEXT` default | `JSON`), `--traceback-mode` (`short` | `full`), +`--default-filetype` (`json` | `yaml` | `toml` | `json5`), `--force-filetype`, +`--color` (`always` | `never` | `auto`), `--data-transform`, `--fill-defaults`. + +## Why uvx (not pip-install or a third-party action) + +`medley` pip-installs a version pinned in `requirements.txt`. This repo's idiom for +ephemeral Python tooling is `uvx @` provisioned by +`astral-sh/setup-uv` (see `ruff`, `pyright`): no virtualenv to manage, exact +version pin, self-contained.