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
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,84 @@ jobs:
- name: Run BATS tests
run: bats --recursive tests/

markdownlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install markdownlint-cli
run: npm install -g markdownlint-cli
- name: Run markdownlint
run: |
set -euo pipefail
ERROR_FOUND=0

if [ ! -f ".markdownlint.yml" ]; then
echo "::error::Config file .markdownlint.yml not found"
exit 1
fi

mapfile -t md_files < <(find . -type f -name "*.md" \
-not -path "./node_modules/*" \
-not -path "./_site/*" \
-not -path "./.git/*" \
-not -path "./rules/*")

if [ ${#md_files[@]} -eq 0 ]; then
echo "⚠️ No .md files found. Skipping."
exit 0
fi

for file in "${md_files[@]}"; do
echo "ℹ️ Checking ${file#./}..."
markdownlint "$file" || ERROR_FOUND=1
done

if [[ $ERROR_FOUND -eq 0 ]]; then
echo "✅ All markdown files passed markdownlint checks!"
else
echo "❌ markdownlint found issues!"
exit 1
fi

yamllint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install yamllint
run: pip install yamllint
- name: Run yamllint
run: |
set -euo pipefail
ERROR_FOUND=0

if [ ! -f ".yamllint.yml" ]; then
echo "::error::Config file .yamllint.yml not found"
exit 1
fi

mapfile -t yaml_files < <(find . -type f \( -name "*.yml" -o -name "*.yaml" \) \
-not -path "./_site/*" \
-not -path "./node_modules/*" \
-not -path "./.git/*" \
-not -name ".yamllint.yml")

if [ ${#yaml_files[@]} -eq 0 ]; then
echo "⚠️ No YAML files found. Skipping."
exit 0
fi

for file in "${yaml_files[@]}"; do
echo "ℹ️ Checking ${file#./}..."
yamllint "$file" || ERROR_FOUND=1
done

if [[ $ERROR_FOUND -eq 0 ]]; then
echo "✅ All YAML files passed yamllint checks!"
else
echo "❌ yamllint found issues!"
exit 1
fi

assemble:
runs-on: ubuntu-latest
steps:
Expand All @@ -70,6 +148,7 @@ jobs:
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/tests
bash scripts/assemble-ci.sh scripts/CI/build scripts/shell/build CI/build

- name: Verify CI/ is up to date
run: |
Expand Down
6 changes: 6 additions & 0 deletions .markdownlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
default: true

MD013:
line_length: 120
MD033: false
MD060: false
1 change: 1 addition & 0 deletions .markdownlintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rules/
10 changes: 10 additions & 0 deletions .yamllint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extends: default

rules:
line-length:
max: 120
truthy:
allowed-values: ['true', 'false', 'on', 'off']
comments:
min-spaces-from-content: 1
document-start: disable
41 changes: 41 additions & 0 deletions CI/build/mermaid.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
mermaid-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mermaid-cli
run: npm install -g @mermaid-js/mermaid-cli
- name: Render Mermaid diagrams
run: |
set -euo pipefail
ERROR_FOUND=0

mapfile -t mmd_files < <(find . -type f -name "*.mmd" \
-not -path "./node_modules/*" \
-not -path "./.git/*")

if [ ${#mmd_files[@]} -eq 0 ]; then
echo "⚠️ No .mmd files found. Skipping."
exit 0
fi

PUPPETEER_CONFIG=$(mktemp /tmp/puppeteer-XXXXXX.json)
echo '{"args":["--no-sandbox"]}' > "$PUPPETEER_CONFIG"
trap 'rm -f "$PUPPETEER_CONFIG"' EXIT

for file in "${mmd_files[@]}"; do
out="${file%.mmd}.svg"
echo "ℹ️ Rendering ${file#./} → ${out#./}..."
mmdc -i "$file" -o "$out" -p "$PUPPETEER_CONFIG" || ERROR_FOUND=1
done

if [[ $ERROR_FOUND -eq 0 ]]; then
echo "✅ All .mmd files rendered successfully!"
else
echo "❌ mermaid-cli failed to render some files!"
exit 1
fi
- name: Upload SVG artifacts
uses: actions/upload-artifact@v4
with:
name: mermaid-diagrams
path: "**/*.svg"
10 changes: 8 additions & 2 deletions CI/linters/eslint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ eslint:
set -euo pipefail
ERROR_FOUND=0

ESLINT_CONFIGS=(
".eslintrc.json" ".eslintrc.js" ".eslintrc.yml" ".eslintrc.yaml"
"eslint.config.js" "eslint.config.mjs" "eslint.config.cjs"
)
CONFIG_FOUND=0
for config in ".eslintrc.json" ".eslintrc.js" ".eslintrc.yml" ".eslintrc.yaml" "eslint.config.js" "eslint.config.mjs" "eslint.config.cjs"; do
for config in "${ESLINT_CONFIGS[@]}"; do
if [ -f "$config" ]; then
CONFIG_FOUND=1
break
Expand All @@ -22,7 +26,9 @@ eslint:
exit 1
fi

mapfile -t js_files < <(find . -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.mjs" -o -name "*.cjs" \) \
mapfile -t js_files < <(find . -type f \
\( -name "*.js" -o -name "*.jsx" -o -name "*.ts" \
-o -name "*.tsx" -o -name "*.mjs" -o -name "*.cjs" \) \
-not -path "./_site/*" \
-not -path "./node_modules/*" \
-not -path "./.git/*")
Expand Down
3 changes: 2 additions & 1 deletion CI/linters/hadolint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ hadolint:

- name: Install Hadolint
run: |
sudo wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
sudo wget -O /usr/local/bin/hadolint \
https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
sudo chmod +x /usr/local/bin/hadolint

- name: Run Hadolint on Dockerfiles
Expand Down
4 changes: 3 additions & 1 deletion CI/linters/markdownlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ markdownlint:

mapfile -t md_files < <(find . -type f -name "*.md" \
-not -path "./node_modules/*" \
-not -path "./_site/*")
-not -path "./_site/*" \
-not -path "./.git/*" \
-not -path "./rules/*")

if [ ${#md_files[@]} -eq 0 ]; then
echo "⚠️ No .md files found. Skipping."
Expand Down
38 changes: 38 additions & 0 deletions CI/linters/mermaid.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
mermaid:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mermaid-cli
run: npm install -g @mermaid-js/mermaid-cli
- name: Run mermaid syntax check
run: |
set -euo pipefail
ERROR_FOUND=0

mapfile -t mmd_files < <(find . -type f -name "*.mmd" \
-not -path "./node_modules/*" \
-not -path "./.git/*")

if [ ${#mmd_files[@]} -eq 0 ]; then
echo "⚠️ No .mmd files found. Skipping."
exit 0
fi

PUPPETEER_CONFIG=$(mktemp /tmp/puppeteer-XXXXXX.json)
echo '{"args":["--no-sandbox"]}' > "$PUPPETEER_CONFIG"
trap 'rm -f "$PUPPETEER_CONFIG"' EXIT

OUT_DIR=$(mktemp -d /tmp/mermaid-XXXXXX)
trap 'rm -rf "$OUT_DIR"' EXIT

for file in "${mmd_files[@]}"; do
echo "ℹ️ Checking ${file#./}..."
mmdc -i "$file" -o "${OUT_DIR}/out.svg" -p "$PUPPETEER_CONFIG" || ERROR_FOUND=1
done

if [[ $ERROR_FOUND -eq 0 ]]; then
echo "✅ All .mmd files passed mermaid syntax check!"
else
echo "❌ mermaid-cli found syntax errors!"
exit 1
fi
108 changes: 108 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Contributing

## Adding a new snippet

Each snippet is one tool, one category. Follow these four steps:

**1. Write the bash script** — `scripts/shell/<category>/<tool>.sh`

```bash
#!/usr/bin/env bash
set -euo pipefail
ERROR_FOUND=0

# Validate config exists
if [ ! -f ".toolrc" ]; then
echo "::error::Config file .toolrc not found"
exit 1
fi

# Collect files
mapfile -t files < <(find . -type f -name "*.ext" \
-not -path "./_site/*" \
-not -path "./node_modules/*" \
-not -path "./.git/*")

# Graceful skip
if [ ${#files[@]} -eq 0 ]; then
echo "⚠️ No files found. Skipping."
exit 0
fi

# Check all files, accumulate errors
for file in "${files[@]}"; do
echo "ℹ️ Checking ${file#./}..."
tool "$file" || ERROR_FOUND=1
done

if [[ $ERROR_FOUND -eq 0 ]]; then
echo "✅ All files passed!"
else
echo "❌ Issues found!"
exit 1
fi
```

**2. Write BATS tests** — `tests/<category>/<tool>.bats`

Cover: missing config, no files found, all pass, one fails.
See existing tests in `tests/linters/` for examples.

**3. Create the source YAML** — `scripts/CI/<category>/<tool>.yml`

```yaml
<tool>:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install <tool>
run: ...
- name: Run <tool>
run: bash scripts/shell/<category>/<tool>.sh
```

**4. Assemble and verify** — generate the output file and check it's committed:

```bash
bash scripts/assemble-ci.sh scripts/CI/<category> scripts/shell/<category> CI/<category>
git diff CI/ # must be empty
```

---

## Mandatory rules

See `rules/process/ci-cd.md` for the full reference. The short version:

- `runs-on: ubuntu-latest` always
- `actions/checkout@v4` must be the first step
- Actions pinned to major version tag (`@v4`, `@v2`) — never `@latest`
- Config validated with `::error::` before the tool runs
- No files → `⚠️ ... Skipping.` + `exit 0`
- Multi-file checks use the `ERROR_FOUND=0` accumulator (never `set -e` as a substitute)
- Node.js: `npm ci`, never `npm install`
- PHP: `shivammathur/setup-php@v2` + `actions/cache@v4` for Composer
- Output emoji: `✅` pass · `❌` fail · `⚠️` skip · `ℹ️` per-file progress

---

## Running tests locally

```bash
# Install bats-core (macOS)
brew install bats-core

# Run all tests
bats --recursive tests/
```

---

## Pull request checklist

- [ ] Script is in `scripts/shell/<category>/`
- [ ] Source YAML is in `scripts/CI/<category>/`
- [ ] Assembled output is committed to `CI/<category>/`
- [ ] `git diff CI/` is clean after running the assembler
- [ ] BATS tests added and passing
- [ ] All mandatory rules from `rules/process/ci-cd.md` satisfied
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Ilya Lyashchuk

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading