diff --git a/.github/workflows/jvm-ci.yml b/.github/workflows/jvm-ci.yml index 26bf8e0..f4a3cc0 100644 --- a/.github/workflows/jvm-ci.yml +++ b/.github/workflows/jvm-ci.yml @@ -29,10 +29,10 @@ on: type: string default: --no-daemon lint-gradle-args: - description: Gradle tasks or arguments for lint checks. + description: Static-only Gradle lint tasks or arguments. Default assumes detekt and ktlint tasks; repos without them must pass their static-only lint command. required: false type: string - default: check + default: detektMain detektTest ktlintCheck test-gradle-args: description: Gradle tasks or arguments for tests. required: false @@ -73,7 +73,7 @@ jobs: - name: Run lint checks working-directory: ${{ inputs.working-directory }} - run: ${{ inputs.gradle-command }} ${{ inputs.lint-gradle-args }} ${{ inputs.gradle-args }} + run: ${{ inputs.gradle-command }} ${{ inputs.lint-gradle-args }} ${{ inputs.gradle-args }} --warning-mode=fail test: name: JVM Test @@ -96,4 +96,4 @@ jobs: - name: Run tests working-directory: ${{ inputs.working-directory }} - run: ${{ inputs.gradle-command }} ${{ inputs.test-gradle-args }} ${{ inputs.gradle-args }} + run: ${{ inputs.gradle-command }} ${{ inputs.test-gradle-args }} ${{ inputs.gradle-args }} --warning-mode=fail diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index 4767489..4cd7f16 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -13,6 +13,21 @@ on: required: false type: string default: nix flake check --print-build-logs + format-command: + description: Format check command. Empty auto-detects alejandra or nixpkgs-fmt and runs it with --check. + required: false + type: string + default: '' + statix-command: + description: Statix lint command. + required: false + type: string + default: nix run nixpkgs#statix -- check . + deadnix-command: + description: Deadnix lint command. The default fails when dead code is found. + required: false + type: string + default: nix run nixpkgs#deadnix -- --fail . validate-command: description: Optional extra validation command. required: false @@ -31,6 +46,38 @@ jobs: - uses: cachix/install-nix-action@v31 + - name: Check Nix formatting + working-directory: ${{ inputs.flake-path }} + env: + FORMAT_COMMAND: ${{ inputs.format-command }} + run: | + set -euo pipefail + + format_command="$FORMAT_COMMAND" + if [ -z "$format_command" ]; then + if grep -Rqs "nixpkgs-fmt" flake.nix treefmt.toml .treefmt.toml 2>/dev/null; then + format_command="nix run nixpkgs#nixpkgs-fmt -- --check ." + else + format_command="nix run nixpkgs#alejandra -- --check ." + fi + fi + + bash -euo pipefail -c "$format_command" + + - name: Statix + if: ${{ inputs.statix-command != '' }} + working-directory: ${{ inputs.flake-path }} + env: + STATIX_COMMAND: ${{ inputs.statix-command }} + run: bash -euo pipefail -c "$STATIX_COMMAND" + + - name: Deadnix + if: ${{ inputs.deadnix-command != '' }} + working-directory: ${{ inputs.flake-path }} + env: + DEADNIX_COMMAND: ${{ inputs.deadnix-command }} + run: bash -euo pipefail -c "$DEADNIX_COMMAND" + - name: Validate if: ${{ inputs.validate-command != '' }} working-directory: ${{ inputs.flake-path }} diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 55484ce..6a681c7 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -24,15 +24,25 @@ on: type: string default: '' lint-command: - description: Optional lint command. + description: Optional additional lint command run after the built-in zero-warning checks. required: false type: string default: '' + eslint-args: + description: ESLint arguments. The workflow always appends --max-warnings=0. + required: false + type: string + default: . typecheck-command: - description: Optional typecheck command. + description: TypeScript typecheck command. Empty uses tsc --noEmit through the selected package manager. required: false type: string default: '' + prettier-args: + description: Prettier check arguments. + required: false + type: string + default: . test-command: description: Optional test command. required: false @@ -102,19 +112,93 @@ jobs: PACKAGE_CHECK_COMMAND: ${{ inputs.package-check-command }} run: bash -euo pipefail -c "$PACKAGE_CHECK_COMMAND" - - name: Lint - if: ${{ inputs.lint-command != '' }} + - name: ESLint working-directory: ${{ inputs.working-directory }} env: - LINT_COMMAND: ${{ inputs.lint-command }} - run: bash -euo pipefail -c "$LINT_COMMAND" + ESLINT_ARGS: ${{ inputs.eslint-args }} + PACKAGE_MANAGER: ${{ inputs.package-manager }} + run: | + set -euo pipefail + + case "$PACKAGE_MANAGER" in + npm) + eslint_command="npm exec -- eslint ${ESLINT_ARGS} --max-warnings=0" + ;; + pnpm) + eslint_command="pnpm exec eslint ${ESLINT_ARGS} --max-warnings=0" + ;; + yarn) + eslint_command="yarn eslint ${ESLINT_ARGS} --max-warnings=0" + ;; + *) + echo "::error::Unsupported package-manager: $PACKAGE_MANAGER. Use npm, pnpm, or yarn." + exit 1 + ;; + esac + + bash -euo pipefail -c "$eslint_command" - name: Typecheck - if: ${{ inputs.typecheck-command != '' }} working-directory: ${{ inputs.working-directory }} env: + PACKAGE_MANAGER: ${{ inputs.package-manager }} TYPECHECK_COMMAND: ${{ inputs.typecheck-command }} - run: bash -euo pipefail -c "$TYPECHECK_COMMAND" + run: | + set -euo pipefail + + typecheck_command="$TYPECHECK_COMMAND" + if [ -z "$typecheck_command" ]; then + case "$PACKAGE_MANAGER" in + npm) + typecheck_command="npm exec -- tsc --noEmit" + ;; + pnpm) + typecheck_command="pnpm exec tsc --noEmit" + ;; + yarn) + typecheck_command="yarn tsc --noEmit" + ;; + *) + echo "::error::Unsupported package-manager: $PACKAGE_MANAGER. Use npm, pnpm, or yarn." + exit 1 + ;; + esac + fi + + bash -euo pipefail -c "$typecheck_command" + + - name: Prettier + working-directory: ${{ inputs.working-directory }} + env: + PACKAGE_MANAGER: ${{ inputs.package-manager }} + PRETTIER_ARGS: ${{ inputs.prettier-args }} + run: | + set -euo pipefail + + case "$PACKAGE_MANAGER" in + npm) + prettier_command="npm exec -- prettier --check ${PRETTIER_ARGS}" + ;; + pnpm) + prettier_command="pnpm exec prettier --check ${PRETTIER_ARGS}" + ;; + yarn) + prettier_command="yarn prettier --check ${PRETTIER_ARGS}" + ;; + *) + echo "::error::Unsupported package-manager: $PACKAGE_MANAGER. Use npm, pnpm, or yarn." + exit 1 + ;; + esac + + bash -euo pipefail -c "$prettier_command" + + - name: Additional lint + if: ${{ inputs.lint-command != '' }} + working-directory: ${{ inputs.working-directory }} + env: + LINT_COMMAND: ${{ inputs.lint-command }} + run: bash -euo pipefail -c "$LINT_COMMAND" - name: Test if: ${{ inputs.test-command != '' }} diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index f7ddcf6..76d476d 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -14,10 +14,15 @@ on: type: string default: python -m pip install -r requirements-dev.txt lint-command: - description: Lint command. + description: Ruff check command. The command must fail on diagnostics. required: false type: string default: ruff check . + typecheck-command: + description: Type check command. Use mypy or pyright, and keep it failing on errors. + required: false + type: string + default: python -m mypy . test-command: description: Test command. required: false @@ -57,6 +62,13 @@ jobs: LINT_COMMAND: ${{ inputs.lint-command }} run: bash -euo pipefail -c "$LINT_COMMAND" + - name: Typecheck + if: ${{ inputs.typecheck-command != '' }} + working-directory: ${{ inputs.working-directory }} + env: + TYPECHECK_COMMAND: ${{ inputs.typecheck-command }} + run: bash -euo pipefail -c "$TYPECHECK_COMMAND" + - name: Test if: ${{ inputs.test-command != '' }} working-directory: ${{ inputs.working-directory }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1425307..08d324a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Features + +* enforce zero-warning reusable CI gates for Node, Nix, Python, and JVM workflows +* fail Gradle warnings and deprecations while making JVM lint static-only by default + ## [0.9.0](https://github.com/JorisJonkers-dev/github-workflows/compare/v0.8.1...v0.9.0) (2026-06-29) diff --git a/README.md b/README.md index f190a14..4e7c6c8 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,10 @@ should call released tags instead of branches. | Workflow | Purpose | | --- | --- | -| `node-ci.yml` | Node install, lint, typecheck, test, and build jobs. | -| `python-ci.yml` | Python lint/test workflow with overrideable commands. | -| `nix-ci.yml` | Nix installer plus flake check and optional validation. | -| `jvm-ci.yml` | Gradle lint/test workflow for JVM repositories. | +| `node-ci.yml` | Node install, zero-warning ESLint, TypeScript, Prettier, test, and build jobs. | +| `python-ci.yml` | Python Ruff, typecheck, and test workflow with overrideable commands. | +| `nix-ci.yml` | Nix formatter, Statix, Deadnix, flake check, and optional validation. | +| `jvm-ci.yml` | Gradle static lint/test workflow that fails Gradle warnings and deprecations. | | `docker-image-ci.yml` | Build a Docker image without publishing. | | `container-publish.yml` | Publish a GHCR image with release and sha tags. | | `publish-api-clients.yml` | Generate and publish API clients from an OpenAPI spec. | @@ -55,12 +55,33 @@ jobs: uses: JorisJonkers-dev/github-workflows/.github/workflows/node-ci.yml@v0.7.3 with: package-manager: pnpm - lint-command: pnpm lint test-command: pnpm test + build-command: pnpm build secrets: packages-token: ${{ secrets.GITHUB_TOKEN }} ``` +## Zero-Warning Gates + +`node-ci.yml` always runs ESLint through the selected package manager with +`--max-warnings=0`, runs TypeScript with `tsc --noEmit` unless +`typecheck-command` is set, and runs `prettier --check` before optional +caller-owned lint, test, and build commands. + +`python-ci.yml` defaults to `ruff check .`, `python -m mypy .`, and +`python -m unittest discover`. Pyright consumers can set `typecheck-command` to +their Pyright invocation. + +`nix-ci.yml` checks formatting with Alejandra by default, auto-switches to +`nixpkgs-fmt --check` when the flake or treefmt config references it, then runs +`statix check`, `deadnix --fail`, and `nix flake check`. + +`jvm-ci.yml` appends `--warning-mode=fail` to both lint and test Gradle +invocations so Gradle warnings and deprecations fail CI. The default +`lint-gradle-args` is now `detektMain detektTest ktlintCheck`; consumers without +those tasks must pass their own static-only lint command. The default +`test-gradle-args` remains `test`. + ```yaml jobs: deploy-bundle: diff --git a/tests/test_check_migrations.py b/tests/test_check_migrations.py index ff2505d..9ec35af 100644 --- a/tests/test_check_migrations.py +++ b/tests/test_check_migrations.py @@ -1,11 +1,12 @@ from __future__ import annotations +import io import os import shutil import subprocess import tempfile import unittest -from contextlib import contextmanager +from contextlib import contextmanager, redirect_stderr, redirect_stdout from pathlib import Path from scripts.check_migrations import main @@ -23,6 +24,13 @@ def write(path: Path, body: str) -> None: path.write_text(body, encoding="utf-8") +def run_main(args: list[str]) -> int: + stdout = io.StringIO() + stderr = io.StringIO() + with redirect_stdout(stdout), redirect_stderr(stderr): + return main(args) + + @contextmanager def fixture_repo(): previous = Path.cwd() @@ -60,7 +68,7 @@ def test_allows_repo_without_matching_migrations(self) -> None: previous = Path.cwd() os.chdir(repo) try: - self.assertEqual(main(["--base-ref", "base"]), 0) + self.assertEqual(run_main(["--base-ref", "base"]), 0) finally: os.chdir(previous) @@ -69,32 +77,32 @@ def test_rejects_modified_existing_migration(self) -> None: write(repo / "services/example-api/src/main/resources/db/migration/V1__init.sql", "select 10;\n") run(["git", "add", "."], repo) run(["git", "commit", "-m", "modify migration"], repo) - self.assertEqual(main(["--base-ref", "base"]), 1) + self.assertEqual(run_main(["--base-ref", "base"]), 1) def test_allows_modified_existing_migration_with_override(self) -> None: with fixture_repo() as repo: write(repo / "services/example-api/src/main/resources/db/migration/V1__init.sql", "select 10;\n") run(["git", "add", "."], repo) run(["git", "commit", "-m", "modify migration"], repo) - self.assertEqual(main(["--base-ref", "base", "--allow-change"]), 0) + self.assertEqual(run_main(["--base-ref", "base", "--allow-change"]), 0) def test_rejects_new_migration_below_base_max(self) -> None: with fixture_repo() as repo: write(repo / "services/example-api/src/main/resources/db/migration/V1_1__late.sql", "select 11;\n") run(["git", "add", "."], repo) - self.assertEqual(main(["--base-ref", "base"]), 1) + self.assertEqual(run_main(["--base-ref", "base"]), 1) def test_allows_new_migration_above_base_max(self) -> None: with fixture_repo() as repo: write(repo / "services/example-api/src/main/resources/db/migration/V3__new.sql", "select 3;\n") run(["git", "add", "."], repo) - self.assertEqual(main(["--base-ref", "base"]), 0) + self.assertEqual(run_main(["--base-ref", "base"]), 0) def test_rejects_duplicate_versions_across_split_dirs(self) -> None: with fixture_repo() as repo: write(repo / "services/example-api/src/main/resources/db/migration-pg/V2__pg.sql", "select 2;\n") run(["git", "add", "."], repo) - self.assertEqual(main(["--base-ref", "base"]), 1) + self.assertEqual(run_main(["--base-ref", "base"]), 1) def test_custom_scope_regex_groups_by_database_directory(self) -> None: with fixture_repo() as repo: @@ -106,7 +114,7 @@ def test_custom_scope_regex_groups_by_database_directory(self) -> None: write(repo / "database/main/migrations/V1__dupe.sql", "select 1;\n") run(["git", "add", "."], repo) self.assertEqual( - main( + run_main( [ "--base-ref", "base",