No description
  • Shell 78.1%
  • PowerShell 13.2%
  • JavaScript 8.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jriyuu 89e1252efe
All checks were successful
self-test / lint-self (push) Successful in 16s
self-test / fixtures (push) Successful in 29s
Merge pull request 'Add bifrost-ui action to fetch the shared theme' (#4) from bifrost-ui-action into main
Reviewed-on: #4
2026-11-01 18:44:08 +11:00
.forgejo/workflows Upload files to ".forgejo/workflows" 2026-10-29 16:11:47 +11:00
assets Upload files to "assets" 2026-10-29 14:22:08 +11:00
bifrost-ui Add bifrost-ui action to fetch the shared theme 2026-11-01 17:26:29 +11:00
gitleaks Upload files to "gitleaks" 2026-10-29 16:03:42 +11:00
hadolint Upload files to "hadolint" 2026-10-29 16:04:55 +11:00
lib Upload files to "lib" 2026-10-29 14:23:06 +11:00
lint-all Upload files to "lint-all" 2026-10-29 16:02:23 +11:00
open-code-review Upload files to "open-code-review" 2026-10-29 14:23:21 +11:00
psscriptanalyzer Upload files to "psscriptanalyzer" 2026-10-29 16:05:22 +11:00
ruff Upload files to "ruff" 2026-10-29 16:04:13 +11:00
shellcheck Upload files to "shellcheck" 2026-10-29 16:04:30 +11:00
skillspector Upload files to "skillspector" 2026-10-29 14:24:29 +11:00
templates Upload files to "templates" 2026-10-29 16:09:07 +11:00
test Update test/run-fixtures.sh 2026-10-29 16:16:15 +11:00
trivy Update trivy/run.sh 2026-10-29 18:56:48 +11:00
xml Upload files to "xml" 2026-10-29 16:05:44 +11:00
.gitleaks.toml Update .gitleaks.toml 2026-10-29 15:05:23 +11:00
.shellcheckrc Upload files to "/" 2026-10-29 14:21:07 +11:00
README.md Upload files to "/" 2026-10-29 16:06:08 +11:00

ci-actions

Shared CI checks for every jriyuu-labs repo on Nidavellir.

Each check lives here once. Repos don't copy the checks — they point at them. Fix or improve a check, or add a new one, in this repo and every repo picks it up on its next run. A repo only needs two small workflow files that never need editing, and template-default already has them.

The one rule to remember: this repo's main branch is live for every repo. Change things on a branch first, let self-test go green, then merge. See Changing a check safely.


Contents


What runs, and when

Check Catches Looks at Runs on Fails the run?
gitleaks Passwords, API keys and tokens committed by accident — including ones deleted later (still in history) Every commit Every push to main + every PR Yes
ruff Python bugs, unused imports, security slips (shell=True, hard-coded passwords) *.py Every push to main + every PR Yes
shellcheck Bash mistakes: unquoted variables, rm -rf "$unset/"*, typos in [ ] tests *.sh, *.bash, and files starting #!/bin/bash (Unraid User Scripts) Every push to main + every PR Yes, on warnings and errors
hadolint Dockerfile problems: :latest bases, cd in RUN, missing flags Dockerfile, Dockerfile.*, *.Dockerfile Every push to main + every PR Yes, on warnings and errors
PSScriptAnalyzer PowerShell bugs, plain-text passwords, and syntax that won't run on Windows PowerShell 5.1 *.ps1, *.psm1, *.psd1 Every push to main + every PR Yes, on errors
xml Broken XML — one bad tag and Unraid rejects the template *.xml Every push to main + every PR Yes
Open Code Review An AI review of the PR's changes, posted as a PR comment The PR diff PRs only Never — advisory
SkillSpector Agent skills / MCP servers that try prompt injection, secret harvesting, data exfiltration Folders with a SKILL.md (or paths you list) PRs only Never — advisory
Trivy Known vulnerabilities (CVEs) in the built image The pushed image In build.yml, after the push Yes, on CRITICAL with a fix available

The six lint checks run together as one step, lint-all, which prints each check in its own collapsible section and a PASS / FAIL summary at the end. Every check skips itself when the repo has nothing for it (no Python → ruff says "nothing to check" and passes). That's why one checks.yml works for every repo without editing.

Everything runs on Brokkr (ubuntu-latest), including the PowerShell check — it runs PowerShell 7 on Linux and checks 5.1 compatibility from there. Dvalin is left free for jobs that truly need Windows.


How it fits together

flowchart LR
    subgraph ci["jriyuu-labs/ci-actions (this repo, public)"]
        A0[lint-all/]
        A1["gitleaks/ ruff/ shellcheck/ hadolint/ psscriptanalyzer/ xml/"]
        A4[open-code-review/ skillspector/]
        A5[trivy/]
    end

    subgraph repo["any jriyuu-labs repo"]
        W1[".forgejo/workflows/checks.yml"]
        W2[".forgejo/workflows/pr-review.yml"]
        W3[".forgejo/workflows/build.yml"]
    end

    T[template-default] -- "new repos get W1 + W2" --> repo
    W1 -- "uses: …/ci-actions/lint-all@main" --> A0
    A0 -- "runs each" --> A1
    W2 -- "uses: …/open-code-review@main" --> A4
    W3 -- "uses: …/trivy@main" --> A5
    A4 -- "LiteLLM key (CI team)" --> L[ai.jriyuu.io]

A repo's workflow says, for example:

- uses: https://nidavellir.jriyuu.io/jriyuu-labs/ci-actions/lint-all@main

The runner fetches this repo at run time and runs lint-all/action.yml, which runs every check folder listed in lint-all/run.sh. That's why this repo must be public: it holds no secrets, and the runners need to read it without logging in.


One-time setup

Do these once, in order.

1. Create the repo

  • On Nidavellir: + → New repository, owner jriyuu-labs, name ci-actions.
  • Visibility: public. (Runners fetch the actions anonymously. Nothing in here is secret — only the calling repos pass secrets in.)
  • Don't use template-default for this one — it's tooling, not an app.

2. Get the files in

Pick whichever is easiest:

  • Hand it to Cisco (easiest): give Cisco the zip and ask it to push the contents to jriyuu-labs/ci-actions on a branch and open a PR. You review and merge as usual.
  • Web UI: Add File → Upload File for each folder's files. To make a folder, use New File and type the full path (e.g. ruff/action.yml) — Forgejo creates the folders.

Keep the layout exactly as in the zip — the workflows depend on the paths.

3. Org-level variables and secrets

In jriyuu-labs → Settings → Actions:

Kind Name Value Used by
Variable LITELLM_BASE_URL https://ai.jriyuu.io/v1 Open Code Review, SkillSpector
Variable CI_REVIEW_MODEL ci-reviewer (a LiteLLM alias — step 4) Open Code Review, and SkillSpector if the next one isn't set
Variable CI_SKILLSCAN_MODEL ci-reviewer-apex (optional LiteLLM alias) SkillSpector
Secret LITELLM_CI_KEY A LiteLLM key from step 4 Open Code Review, SkillSpector
Secret REVIEW_BOT_TOKEN Only if needed — see step 5 Open Code Review comments
Secret CI_BOT_TOKEN Already exists (ci-bot, Packages: Write) Trivy, to pull the image it scans

Variables are visible in logs, secrets aren't — keep the key a secret.

4. A LiteLLM key just for CI

In the LiteLLM UI (ai.jriyuu.io/ui):

  1. Add two aliases to LiteLLM's config.yaml, both on NanoGPT (same api_base and key variable as the stt-external entry). The workflows only ever see the alias names, so swapping the real model later is a one-line config.yaml change — nothing in Forgejo changes.
      - model_name: ci-reviewer          # code review: big output limit (131K)
        litellm_params:
          model: openai/z-ai/glm-5.3-flash
          api_base: <same as stt-external>
          api_key: os.environ/NANOGPT_API_KEY
    
      - model_name: ci-reviewer-apex     # skill/MCP scans: Cantina's security post-train
        litellm_params:
          model: openai/z-ai/glm-5.3-flash-apex
          api_base: <same as stt-external>
          api_key: os.environ/NANOGPT_API_KEY
    
    Both are in the NanoGPT subscription. Apex's 32.8K output limit has to hold its reasoning and its answer, which is why the bigger reviews go to plain Flash (131K).
  2. Teams → New team called ci, with access to only those two aliases. Give it a budget so a runaway loop can't run up a bill.
  3. Keys → New key on the ci team. Copy it into the LITELLM_CI_KEY secret.

Where the code goes: with NanoGPT, PR diffs and skill files are sent to NanoGPT and whichever provider it routes to. That's fine for these repos (gitleaks keeps secrets out of them). To keep a repo's code at home, point it at your local model instead — add repo-level variables CI_REVIEW_MODEL = qwen3.8-27b (and CI_SKILLSCAN_MODEL) in that repo; a repo variable overrides the org one of the same name. Give the ci team access to that model too.

5. Can the job's own token comment on PRs?

Open Code Review posts its review as a PR comment using the job's built-in token. For PRs from branches in the same repo (how Cisco works), that should be allowed. You'll find out on the first PR:

  • Comment appears → nothing to do.
  • Job log says "The token isn't allowed to comment" → make a bot user (e.g. review-bot), add it to an org team with write access to the repos, create a token for it with issue: write and repository: read, and save it as the REVIEW_BOT_TOKEN org secret. The workflow uses it automatically when it exists.

6. Run the self-test

Push anything to this repo (or Actions → self-test → Run workflow). Both jobs must go green:

  • lint-self — this repo's own scripts pass its own checks (via lint-all, the same way other repos run them).
  • fixtures — every lint check, and lint-all, passes test/fixtures/good/ and fails test/fixtures/bad/. If a check ever stops catching the bad examples, this fails.

7. Pilot on one repo first

Before rolling out everywhere, add the two workflow files to one repo (rea-mcp is a good choice — it has a Dockerfile, a shell entrypoint and an Unraid XML template) and open a PR. Check that:

  • checks runs and the lint-all summary shows each check as PASS or "nothing to check".
  • pr-review posts one comment on the PR, and edits it (not a second comment) when you push again.
  • skill-scan says "No SKILL.md files found" and passes.

The things most likely to need a tweak on first run are listed under Troubleshooting → First-run checklist.


Adding the checks to a repo

New repos

Nothing to do — create the repo from jriyuu-labs/template-default and it comes with checks.yml and pr-review.yml, alongside the protection on main.

To set that up once: copy templates/checks.yml and templates/pr-review.yml from this repo into template-default at .forgejo/workflows/, then set up the branch protection below on template-default too.

Existing repos

In the repo's web UI, New File twice:

  1. .forgejo/workflows/checks.yml — paste the contents of templates/checks.yml.
  2. .forgejo/workflows/pr-review.yml — paste templates/pr-review.yml.

Commit to a branch, open a PR, and the first PR shows you what each check thinks of the repo. Expect some red on older repos — see When a check goes red.

Repos that build an image

Also add the Trivy step to the repo's build.yml. Paste the block from templates/build-trivy-step.yml straight after the docker/build-push-action step, in the same job, and change the image name. For multi-arch builds it scans the image for the runner's own CPU (Brokkr = amd64, Eitri = arm64), so add it to each arch's job if you want both.

This is the one part that's pasted into each repo rather than shared, because every build.yml is different. The Trivy logic itself still lives here.

Branch protection

Makes Forgejo wait for the checks, and the AI review, before anything can be merged into main. Add the two workflow files first — once this is saved you can't commit straight to main any more, not even as the owner.

Repo → Settings → Branches → add (or edit) the rule for main:

Setting Value Why
Protected branch name pattern main
Push Disable push or Whitelist restricted push (you only) Changes reach main through PRs
Required approvals 0 Forgejo won't let you approve your own PR, so 1 would lock your own PRs out. The merge whitelist below already makes you the only one who can merge.
Enable status check ✅
Status check patterns the three lines below
Pull request merge Enable merge whitelist — you only Agents open PRs; only you merge
Enforce this rule for repository admins ✅ Otherwise the rule doesn't apply to you (the owner)
checks / lint (pull_request)
pr-review / ai-review (pull_request)
pr-review / skill-scan (pull_request)
  • The merge button stays disabled until all three have finished. The two pr-review jobs always end green (they're advisory), so requiring them only means "wait for the AI review", never "blocked by what it found".
  • Only add these patterns on repos that have both workflow files — otherwise the checks never appear and the PR can never be merged.
  • Keep the job names (lint, ai-review, skill-scan) and workflow names (checks, pr-review) as they are — the patterns match on them.
  • On a new repo the "Status checks found" list is empty until a PR has run. That's fine; type the patterns in anyway.

When a check goes red

  1. Open the failed run and the Lint and secret checks step. The lint-all summary at the bottom says which check failed; open that check's section above it. Every check prints the file, the line, and a code (SC2086, DL3007, F401, PSAvoidUsing…).
  2. Decide what it is:
    • A real problem → fix it, or ask Cisco: "checks failed on PR #12, fix the ruff and shellcheck findings."
    • A false alarm → silence that one finding (each check's section below says how). Don't turn the whole check off.
    • Lots of old problems in an old repo → fix them in one cleanup PR, or lower the bar for that repo with its own config file (see Per-repo tweaks).
  3. gitleaks is different. A red gitleaks means a secret may be exposed. Rotate the secret first (new password/key in Infisical/Vaultwarden), then remove it. Deleting the file isn't enough — it's still in git history.

The PR comment from Open Code Review and the SkillSpector notes are advice. They never block a merge; you decide.


The checks, one by one

gitleaks — committed secrets

  • What: scans every commit for anything that looks like a password, token or key, using gitleaks' built-in rules (GitHub/AWS/Slack tokens, private keys, generic password = "…" patterns, …).
  • History mode (default) scans the whole git history. That's why checks.yml checks out with fetch-depth: 0. To scan only the current files instead: with: { mode: files }.
  • Secrets are redacted in the log — the job log shows where, not what.
  • False positive? Two options, both files in the repo root:
    • .gitleaksignore — one fingerprint per line. The fingerprint is printed in the finding (Fingerprint: <commit>:<file>:<rule>:<line>). Best for one specific match.
    • .gitleaks.toml — for rules or whole paths. This repo has one that ignores the deliberately fake token in test/fixtures/bad/:
      [extend]
      useDefault = true
      
      [allowlist]
      paths = ['''test/fixtures/''']
      

ruff — Python

  • Default rules (ruff/ruff.toml): real errors (E4/E7/E9), pyflakes (F — undefined names, unused imports), bugbear (B — likely bugs) and bandit (S — security: shell=True, hard-coded passwords, unsafe yaml.load, …). Asserts and plain subprocess calls are allowed.
  • Repo's own settings win: if the repo has ruff.toml, .ruff.toml or a [tool.ruff] section in pyproject.toml, ruff uses that instead.
  • Auto-fix: most findings fix themselves with ruff check --fix . — or ask Cisco to run it.
  • Formatting check is off by default. Turn it on per repo with with: { format-check: "true" } once the code has been through ruff format once.
  • Ignore one line: add # noqa: S602 (with the code) at the end of it.

shellcheck — shell scripts

  • Finds scripts by extension (.sh, .bash) and by first line, so extension-less files starting #!/bin/bash (Unraid User Scripts, entrypoints) are checked too.
  • Fails on warnings and errors. Info/style notes (like "quote this to prevent word splitting", SC2086) don't fail — they're only shown when the level is lowered. Change it with with: { severity: error } (stricter repos: info).
  • Explanations: every code has a page, e.g. https://www.shellcheck.net/wiki/SC2115.
  • Ignore one line: put # shellcheck disable=SC2086 on the line above.
  • Repo-wide settings: a .shellcheckrc in the repo root is used automatically (this repo has one).

hadolint — Dockerfiles

  • Default config (hadolint/hadolint.yaml): fail on warnings and errors; version pinning for apt/apk/pip/npm installs is not required (too strict for homelab images).
  • Common findings: DL3007 (FROM image:latest — pin a tag), DL3003 (RUN cd … — use WORKDIR), DL4006 (set SHELL with -o pipefail before piping in RUN).
  • Ignore one line: # hadolint ignore=DL3007 on the line above.
  • Repo's own settings: a .hadolint.yaml in the repo root replaces the shared one. Copy hadolint/hadolint.yaml as a starting point.
  • Rule list: https://github.com/hadolint/hadolint#rules

PSScriptAnalyzer — PowerShell

  • Runs on Linux with PowerShell 7, no Windows runner needed.
  • 5.1 compatibility: the shared settings turn on PSUseCompatibleSyntax for 5.1 and 7.0, so PowerShell-7-only syntax (??=, ternary ? :, &&/|| chains) is flagged as an error — exactly what would break on Windows Server's built-in 5.1.
  • Fails on errors by default (fail-on: Error). Warnings like "variable assigned but never used" are shown but don't fail. Stricter: with: { fail-on: Warning }.
  • What it can't catch: problems that only show up when the script runs — e.g. Constrained Language Mode from AppLocker, missing modules, wrong permissions. It reads scripts; it doesn't run them.
  • Ignore one finding — put this above the function or param() block:
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'legacy interface')]
    
  • Repo's own settings: a PSScriptAnalyzerSettings.psd1 in the repo root replaces the shared one. Copy psscriptanalyzer/PSScriptAnalyzerSettings.psd1 as a starting point.
  • Rules: https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/readme
  • Run it yourself on Windows:
    Install-Module PSScriptAnalyzer -Scope CurrentUser
    Invoke-ScriptAnalyzer -Path .\MyScript.ps1 -Settings .\PSScriptAnalyzerSettings.psd1
    

xml — Unraid templates

  • Checks that every .xml file is well-formed (every tag closed, & written as &amp;, …). It does not check Unraid's own rules — only that the file can be read at all, which is what makes Unraid silently drop a template.
  • The error shows the file, line and column, e.g. template.xml:5: parser error : Opening and ending tag mismatch.

Open Code Review — AI PR review

  • What: Alibaba's Open Code Review reads the PR's changes, looks around the codebase for context, and writes line-level findings. It's tuned to report fewer, more certain issues, so it will miss some things.
  • Where the result goes: one comment on the PR, marked so later pushes edit the same comment rather than piling up new ones. The full output is also in the job log.
  • Never blocks: the job has continue-on-error: true and a 45-minute timeout. A slow model, a LiteLLM outage or a bad review can't stop a merge.
  • Which model: the CI_REVIEW_MODEL variable — by default the ci-reviewer alias (GLM-5.3 Flash on NanoGPT). To try another model on one repo, set a repo-level CI_REVIEW_MODEL there (e.g. ci-reviewer-apex) and compare its comments with other repos' on similar changes.
  • Extra context: to tell the reviewer what a repo is for, add to the step: background: "MCP server wrapping REA for OpenClaw agents; focus on input validation."
  • The job uses node:24-trixie because Open Code Review needs git 2.41 or newer, and older Debian images ship 2.39.

SkillSpector — skills and MCP servers

  • What: NVIDIA's scanner for agent skills and MCP servers — prompt injection, hidden instructions, credential harvesting, data exfiltration, curl | bash, over-broad permissions, and more (69 patterns).
  • What it scans: every folder that contains a SKILL.md. Nothing found → it skips and passes. For an MCP server repo, add paths: "." to the step to scan the whole repo.
  • Two passes: a fast static scan, plus an LLM pass through LiteLLM using CI_SKILLSCAN_MODEL (the ci-reviewer-apex alias — Cantina Security's security-focused post-train of GLM-5.3 Flash). If CI_SKILLSCAN_MODEL isn't set, it uses CI_REVIEW_MODEL. Leave llm-base-url empty for a static-only scan.
  • Scores: 0–50 is SAFE/CAUTION (passes; read any CAUTION notes), over 50 is DO NOT INSTALL (warning). It never blocks — you decide whether an agent gets the skill.
  • Accepted false alarms: SkillSpector supports a baseline file. Run it locally once with skillspector baseline <path> -o .skillspector-baseline.yaml and commit the file; see the SkillSpector docs. (Hooking the baseline into this action is a to-do.)

Trivy — image vulnerabilities

  • What: scans the built image's OS packages and libraries for known CVEs.
  • Two passes: a report of HIGH and CRITICAL findings (never fails), then a gate that fails on CRITICAL — only counting vulnerabilities that have a fixed version available (ignore-unfixed), since you can't fix the others anyway.
  • It runs after the push, so a red Trivy means the image is already published and needs an update, not that it was blocked. The usual fix is a newer base image in the FROM line.
  • Report only, never fail: add fail-on: none to the step.
  • Accept a specific CVE: add its ID to a .trivyignore file in the repo root (one per line, with a # comment saying why).
  • ⚠️ Version pinned on purpose — read before bumping. Trivy's own release pipeline was hijacked in March 2026: release v0.69.4 (and Docker images 0.69.5/0.69.6) shipped malware that stole CI secrets. This action pins v0.69.3, a known-safe release, and checks the download against SHA-256 hashes stored in trivy/run.sh. Before changing the version, check Aqua's security advisories for that release.

Per-repo tweaks

Settings go on the lint-all step in that repo's checks.yml (the template has them commented out, ready to use):

      - name: Lint and secret checks
        uses: https://nidavellir.jriyuu.io/jriyuu-labs/ci-actions/lint-all@main
        with:
          exclude: '^(third_party|old)/'      # paths every file check ignores
          skip: "psscriptanalyzer"            # leave checks out for this repo
          shellcheck-severity: error          # error | warning (default) | info | style
          psscriptanalyzer-fail-on: Warning   # Error (default) | Warning
          ruff-format-check: "true"           # also fail on unformatted Python
          gitleaks-mode: files                # history (default) | files

node_modules/, vendor/, third_party/ and .venv/ are always skipped, and so is anything in .gitignore (only tracked files are checked).

Turn one check off in one repo — skip: it. Prefer silencing individual findings over this.

Use different rules in one repo — add that tool's own config file in the repo root. It replaces the shared default for that repo only:

Tool File in the repo root
ruff ruff.toml (or [tool.ruff] in pyproject.toml)
shellcheck .shellcheckrc
hadolint .hadolint.yaml
PSScriptAnalyzer PSScriptAnalyzerSettings.psd1
gitleaks .gitleaks.toml / .gitleaksignore
Trivy .trivyignore

Each check can also still be used on its own (e.g. uses: …/ci-actions/shellcheck@main) — its inputs are listed in its action.yml. lint-all is the normal way.


Changing a check safely

main here is live for every repo, so:

  1. Make a branch in this repo (e.g. tweak-shellcheck), commit the change.
  2. self-test runs on the branch. It uses the actions from that commit (uses: ./lint-all), so a broken change shows up here, not in your other repos. Both jobs must be green.
  3. Optional real-world test: in one other repo, temporarily change ci-actions/lint-all@main to ci-actions/lint-all@tweak-shellcheck on a branch there and open a PR. (lint-all runs the check folders from the same branch, so the whole branch is tested together.)
  4. Merge to main. Every repo uses the change from its next run.

If something does break everywhere, revert the merge on main — every repo recovers on its next run.

If you add a rule that should fail, add an example of it to test/fixtures/bad/ so the fixtures prove it's caught.


Tool versions

Versions are pinned so a new release can't change results (or break things) overnight. Bump them here, on a branch, and let self-test check them.

For the lint checks, each default is set once, in the : "${INPUT_…:=…}" lines at the top of that check's run.sh — both lint-all and the check's own action.yml use it from there.

Tool Version Set in Release notes
ruff 0.12.0 top of ruff/run.sh https://github.com/astral-sh/ruff/releases
shellcheck v0.10.0 top of shellcheck/run.sh https://github.com/koalaman/shellcheck/releases
hadolint v2.12.0 top of hadolint/run.sh https://github.com/hadolint/hadolint/releases
PowerShell 7.4.6 top of psscriptanalyzer/run.sh https://github.com/PowerShell/PowerShell/releases
PSScriptAnalyzer 1.23.0 top of psscriptanalyzer/run.sh https://github.com/PowerShell/PSScriptAnalyzer/releases
gitleaks 8.21.2 top of gitleaks/run.sh https://github.com/gitleaks/gitleaks/releases
Trivy 0.69.3 trivy/action.yml + hashes in trivy/run.sh ⚠️ read the Trivy section first
uv (installs SkillSpector) 0.8.0 skillspector/action.yml https://github.com/astral-sh/uv/releases
SkillSpector main skillspector/action.yml (ref) https://github.com/NVIDIA/SkillSpector
Open Code Review latest (npm) open-code-review/action.yml https://github.com/alibaba/open-code-review

SkillSpector and Open Code Review track their latest code — pin ref / version to a release if they ever start changing behaviour under you.

Tools download from GitHub releases on each run, into the job's temp folder (never into the repo). They take a few seconds each. The pinned download is always used, even if the runner image happens to have its own copy of a tool, so results don't change with the image.


Adding a new check

Say you want to add yamllint. Everything happens in this repo — no other repo needs editing:

  1. Make a folder yamllint/ with two files, copying the shape of xml/:
    • run.sh — starts with source "$GITHUB_ACTION_PATH/../lib/common.sh", sets its defaults with : "${INPUT_VERSION:=…}" lines, then: find files with list_files '\.ya?ml$', call skip if there are none, download the tool with download, run it, and fail "…" with a helpful message if it finds problems.
    • action.yml — name, description, inputs (default ""), and one step that runs bash "$GITHUB_ACTION_PATH/run.sh" with the inputs passed as INPUT_* environment variables. Only needed if you want to be able to use the check on its own.
  2. Shared defaults go next to it (like ruff/ruff.toml), used only when the repo has no config of its own.
  3. Turn it on everywhere: add yamllint to CHECKS=( … ) at the top of lint-all/run.sh.
  4. Fixtures: add a passing example to test/fixtures/good/ and a failing one to test/fixtures/bad/. test/run-fixtures.sh reads the check list from lint-all, so it tests the new check automatically.
  5. Make the change on a branch and let self-test go green before merging — once it's on main, every repo runs it.
  6. Add it to the tables in this README.

Heads-up: a new check can turn existing repos red on their next PR (e.g. yamllint finding old problems). Expect a round of small fixes, or start the new check at a gentle setting.

Ideas parked for later: actionlint (workflow files), yamllint (LiteLLM config.yaml, compose files), Biome (JS/HTML/CSS for the game library and tools container), markdownlint, and Renovate (opens PRs when a pinned base image or dependency has a new version).

Helpers in lib/common.sh:

Helper Does
list_files REGEX Tracked files matching REGEX, minus exclude and vendored folders
skip "msg" Print why, exit 0 (check passes)
fail "msg" Print an error, exit 1
warn "msg" / info "msg" / log "msg" Log lines
download URL DEST Download with retries, fail loudly
ci_arch x86_64 on Brokkr, aarch64 on Eitri
apt_install pkg… Install OS packages on Debian/Ubuntu images
first_existing f1 f2… First of these files that exists

Troubleshooting

First-run checklist

These are the parts most likely to need a tweak the first time, because they depend on how Nidavellir and the runners are set up:

Symptom Likely cause Fix
uses: https://…/ci-actions/lint-all@main fails with "repository not found" / 401 ci-actions isn't public, or the runner can't reach nidavellir.jriyuu.io Make the repo public; check split DNS from the runner container
Steps fail with GITHUB_ACTION_PATH: unbound variable or run.sh: No such file This runner version doesn't set GITHUB_ACTION_PATH for remote actions Ask Cisco to change each action.yml to pass ${{ github.action_path }} in as an env variable instead
curl: command not found / tar: command not found The ubuntu-latest job image is very slim Add container: { image: node:24-trixie } to the job in checks.yml
vars.LITELLM_BASE_URL comes through empty Variables set at repo level, not org, or misspelled Set them under jriyuu-labs → Settings → Actions → Variables
PR comment fails with HTTP 403 The job token can't comment Set up REVIEW_BOT_TOKEN (setup step 5)
ocr llm test fails URL shape, key or model access Check LITELLM_BASE_URL is https://ai.jriyuu.io/v1 with no trailing slash (the workflow adds /chat/completions), the key is the ci team's, and that team can use the alias in CI_REVIEW_MODEL (and CI_SKILLSCAN_MODEL)

Other problems

"git X is too old - Open Code Review needs 2.41+" — the ai-review job must run in node:24-trixie (it does in the template). If someone removed the container: line, put it back.

The AI review times out — big PRs take a while, especially if a repo is pointed at the local Qwen model. Raise timeout-minutes in pr-review.yml, or for local, use the qwen3.8-27b-fast alias.

Review comment is cut off or empty — the model ran out of output room (reasoning counts against it). Make sure that repo uses ci-reviewer (131K output), not ci-reviewer-apex (32.8K).

gitleaks warns "Shallow checkout" — the checkout step needs fetch-depth: 0. Without it only the latest commit is scanned.

PSScriptAnalyzer can't install from the PowerShell Gallery — it falls back to the same package from the GitHub release automatically. If both fail, the runner has no internet access.

A check passes but should have failed — it probably skipped. Look for "Skipping (nothing to check)" in its log: the files may be untracked, .gitignored, in a vendored folder, or matched by exclude.

Trivy: "checksum MISMATCH" — stop. Don't override it. Either GitHub served a broken file, or the release was tampered with. Re-run once; if it still fails, check Aqua's security advisories before doing anything else.


What's in this repo

ci-actions/
├── README.md                     ← you are here
├── .gitleaks.toml                ← ignores the fake token in test/fixtures/bad
├── .shellcheckrc                 ← lets shellcheck follow the lib/common.sh source line
├── .forgejo/workflows/
│   └── self-test.yml             ← tests every change to this repo
├── lib/
│   └── common.sh                 ← shared helpers every run.sh uses
├── lint-all/                     ← action.yml + run.sh: runs every check below (CHECKS list)
├── gitleaks/                     ← action.yml + run.sh
├── ruff/                         ← action.yml + run.sh + ruff.toml (shared default rules)
├── shellcheck/                   ← action.yml + run.sh
├── hadolint/                     ← action.yml + run.sh + hadolint.yaml (shared default rules)
├── psscriptanalyzer/             ← action.yml + run.sh + analyze.ps1 + PSScriptAnalyzerSettings.psd1
├── xml/                          ← action.yml + run.sh
├── trivy/                        ← action.yml + run.sh (pinned version + hashes)
├── open-code-review/             ← action.yml + run.sh + post-comment.mjs
├── skillspector/                 ← action.yml + run.sh
├── templates/
│   ├── checks.yml                ← copy into repos as .forgejo/workflows/checks.yml
│   ├── pr-review.yml             ← copy into repos as .forgejo/workflows/pr-review.yml
│   └── build-trivy-step.yml      ← paste into a repo's build.yml after the push
├── test/
│   ├── run-fixtures.sh           ← good must pass, bad must fail (each check + lint-all)
│   └── fixtures/{good,bad}/      ← one example file per check
└── assets/
    └── icon.svg                  ← repo icon

Every action.yml is short and only passes inputs through; the real logic is in each folder's run.sh, which is plain bash you can read top to bottom.