Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/jvm-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
47 changes: 47 additions & 0 deletions .github/workflows/nix-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
100 changes: 92 additions & 8 deletions .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 != '' }}
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)


Expand Down
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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:
Expand Down
24 changes: 16 additions & 8 deletions tests/test_check_migrations.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand All @@ -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",
Expand Down
Loading