Reusable GitHub Actions job snippets for code quality checks. Each snippet is a self-contained job definition that projects compose into their own workflows.
| Tool | Category | File |
|---|---|---|
| ESLint | linters | CI/linters/eslint.yml |
| Hadolint | linters | CI/linters/hadolint.yml |
| HTMLHint | linters | CI/linters/htmlhint.yml |
| markdownlint | linters | CI/linters/markdownlint.yml |
| mermaid-cli | linters | CI/linters/mermaid.yml |
| ShellCheck | linters | CI/linters/shellcheck.yml |
| Stylelint | linters | CI/linters/stylelint.yml |
| yamllint | linters | CI/linters/yamllint.yml |
| PHPStan | static_analysis | CI/static_analysis/phpstan.yml |
| mermaid-cli | build | CI/build/mermaid.yml |
| BATS | tests | CI/tests/bats.yml |
| Laravel | tests | CI/tests/laravel_tests.yml |
The project uses a three-layer architecture:
scripts/shell/<category>/<tool>.sh ← bash logic (tested, standalone)
↓ assembled by scripts/assemble-ci.sh
scripts/CI/<category>/<tool>.yml ← source YAML (references the script)
↓
CI/<category>/<tool>.yml ← assembled output (script inlined, ready to use)
Shell scripts (scripts/shell/) contain the actual linting/testing logic.
They run standalone in CI without needing anything from this repo's structure.
Source YAMLs (scripts/CI/) define the GitHub Actions job: install the tool,
then call the script with run: bash scripts/shell/<category>/<tool>.sh.
Assembled YAMLs (CI/) are generated by the assembler. The run: bash ... line is replaced with
run: | followed by the full script body inlined. These are the files downstream projects import.
Workflows/
├── scripts/
│ ├── assemble-ci.sh # assembles source YAMLs → CI/
│ ├── CI/ # source YAML templates
│ │ ├── linters/
│ │ │ ├── hadolint.yml
│ │ │ ├── htmlhint.yml
│ │ │ ├── markdownlint.yml
│ │ │ ├── shellcheck.yml
│ │ │ ├── stylelint.yml
│ │ │ └── yamllint.yml
│ │ ├── static_analysis/
│ │ │ └── phpstan.yml
│ │ └── tests/
│ │ ├── bats.yml
│ │ └── laravel_tests.yml
│ └── shell/ # bash scripts (one per tool)
│ └── linters/
│ ├── hadolint.sh
│ ├── htmlhint.sh
│ ├── markdownlint.sh
│ ├── shellcheck.sh
│ ├── stylelint.sh
│ └── yamllint.sh
│
├── CI/ # assembled output (ready to use)
│ ├── linters/
│ ├── static_analysis/
│ └── tests/
│
├── tests/
│ ├── assemble-ci.bats # tests for the assembler
│ ├── linters/ # unit tests for each shell script
│ │ ├── hadolint.bats
│ │ ├── htmlhint.bats
│ │ ├── markdownlint.bats
│ │ ├── shellcheck.bats
│ │ ├── stylelint.bats
│ │ └── yamllint.bats
│ └── helpers/
│ └── common.bash # shared test utilities (mocks, temp dirs)
│
└── rules/ # living documentation for contributors
├── README.md
├── _meta/how-to-write-rules.md
└── process/
├── architecture-design.md
└── ci-cd.md
Run the assembler for each category:
bash scripts/assemble-ci.sh scripts/CI/linters scripts/shell/linters CI/linters
bash scripts/assemble-ci.sh scripts/CI/static_analysis scripts/shell/static_analysis CI/static_analysis
bash scripts/assemble-ci.sh scripts/CI/tests scripts/shell/tests CI/testsThe assembler takes three arguments: <source-yamls-dir> <scripts-dir> <output-dir>.
For each .yml in the source dir it looks for a matching .sh in the scripts dir. If found, the run: bash ...
line is replaced with run: | and the script body is inlined (shebang stripped, indentation preserved).
If not found, the YAML is copied as-is.
Example transformation:
Source (scripts/CI/linters/shellcheck.yml):
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck on scripts
run: bash scripts/shell/linters/shellcheck.shAssembled (CI/linters/shellcheck.yml):
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck on scripts
run: |
ERROR_FOUND=0
DIRS=("scripts" "docker/scripts")
for DIR in "${DIRS[@]}"; do
...
done
...| Snippet | Tool | What it checks |
|---|---|---|
CI/linters/hadolint.yml |
hadolint | Dockerfiles |
CI/linters/htmlhint.yml |
htmlhint | HTML files |
CI/linters/markdownlint.yml |
markdownlint | Markdown files |
CI/linters/shellcheck.yml |
shellcheck | Shell scripts |
CI/linters/stylelint.yml |
stylelint | CSS / SCSS / LESS |
CI/linters/yamllint.yml |
yamllint | YAML files |
| Snippet | Tool | What it checks |
|---|---|---|
CI/static_analysis/phpstan.yml |
PHPStan | PHP (level from phpstan.neon) |
| Snippet | What it runs |
|---|---|
CI/tests/bats.yml |
BATS tests (tests/ directory) |
CI/tests/laravel_tests.yml |
Laravel test suite (PHP 8.2, SQLite, parallel) |
Each assembled YAML defines a single top-level job key. Copy the content into your workflow under jobs::
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
shellcheck: # paste content of CI/linters/shellcheck.yml here
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck on scripts
run: |
ERROR_FOUND=0
...
phpstan: # paste content of CI/static_analysis/phpstan.yml here
...Every script in scripts/shell/ follows the same patterns defined in rules/process/ci-cd.md:
Error accumulator — check all files before exiting, so one failure doesn't hide others:
ERROR_FOUND=0
for file in "${files[@]}"; do
some-tool "$file" || ERROR_FOUND=1
done
if [[ $ERROR_FOUND -eq 0 ]]; then
echo "✅ All files passed!"
else
echo "❌ Issues found!"
exit 1
fiConfig validation — hard fail early if a required config is missing:
if [ ! -f ".yamllint.yml" ]; then
echo "::error::Config file .yamllint.yml not found"
exit 1
fiGraceful skip — exit 0 when there are no files to check:
if [ ${#files[@]} -eq 0 ]; then
echo "⚠️ No .yml files found. Skipping."
exit 0
fiOutput conventions:
✅— all checks passed❌— issues found⚠️— skipped or non-critical warningℹ️— currently processing a file::error::— GitHub Actions error annotation (hard failures)
Tests use BATS. Each shell script has a corresponding .bats file
that mocks external tools and tests success, failure, and edge cases in isolation.
# Install (macOS)
brew install bats-core
# Run all tests
bats --recursive tests/Test helpers (tests/helpers/common.bash) provide:
setup_test_dir/teardown_test_dir— create and clean up a temp working directorymock_tool <name> <exit_code>— stub an external binary to always exit with a given codemock_tool_conditional <name>— stub that exits 1 only when its argument matches$MOCK_FAIL_PATTERN
-
Write the bash script in
scripts/shell/<category>/<tool>.shFollow the error accumulator pattern and output conventions above. -
Write tests in
tests/<category>/<tool>.batsCover: missing config, no files found, all pass, one fails, multiple files with one failing. -
Create the source YAML in
scripts/CI/<category>/<tool>.ymlInstall the tool, then call the script:run: bash scripts/shell/<category>/<tool>.sh -
Run the assembler to generate the output file in
CI/. -
Run tests to verify everything works.
The rules/ directory is the authoritative guide for contributors:
rules/process/architecture-design.md— directory layout, file naming, snippet scoperules/process/ci-cd.md— mandatory patterns for every job (error accumulator, config validation, action versions, etc.)rules/_meta/how-to-write-rules.md— how to write and format rules files