- Shell 78.1%
- PowerShell 13.2%
- JavaScript 8.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| assets | ||
| bifrost-ui | ||
| gitleaks | ||
| hadolint | ||
| lib | ||
| lint-all | ||
| open-code-review | ||
| psscriptanalyzer | ||
| ruff | ||
| shellcheck | ||
| skillspector | ||
| templates | ||
| test | ||
| trivy | ||
| xml | ||
| .gitleaks.toml | ||
| .shellcheckrc | ||
| README.md | ||
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
mainbranch is live for every repo. Change things on a branch first, letself-testgo green, then merge. See Changing a check safely.
Contents
- What runs, and when
- How it fits together
- One-time setup
- Adding the checks to a repo (incl. branch protection)
- When a check goes red
- The checks, one by one
- Per-repo tweaks
- Changing a check safely
- Tool versions
- Adding a new check
- Troubleshooting
- What's in this repo
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-defaultfor 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-actionson 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):
- Add two aliases to LiteLLM's
config.yaml, both on NanoGPT (sameapi_baseand key variable as thestt-externalentry). The workflows only ever see the alias names, so swapping the real model later is a one-lineconfig.yamlchange — nothing in Forgejo changes.
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).- 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 - 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. - Keys → New key on the
citeam. Copy it into theLITELLM_CI_KEYsecret.
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 theREVIEW_BOT_TOKENorg 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, passestest/fixtures/good/and failstest/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:
checksruns and thelint-allsummary shows each check as PASS or "nothing to check".pr-reviewposts one comment on the PR, and edits it (not a second comment) when you push again.skill-scansays "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:
.forgejo/workflows/checks.yml— paste the contents oftemplates/checks.yml..forgejo/workflows/pr-review.yml— pastetemplates/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-reviewjobs 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
- Open the failed run and the Lint and secret checks step. The
lint-all summaryat 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…). - 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).
- 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.ymlchecks out withfetch-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 intest/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, unsafeyaml.load, …). Asserts and plainsubprocesscalls are allowed. - Repo's own settings win: if the repo has
ruff.toml,.ruff.tomlor a[tool.ruff]section inpyproject.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 throughruff formatonce. - 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=SC2086on the line above. - Repo-wide settings: a
.shellcheckrcin 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 …— useWORKDIR),DL4006(setSHELLwith-o pipefailbefore piping inRUN). - Ignore one line:
# hadolint ignore=DL3007on the line above. - Repo's own settings: a
.hadolint.yamlin the repo root replaces the shared one. Copyhadolint/hadolint.yamlas 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
PSUseCompatibleSyntaxfor 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.psd1in the repo root replaces the shared one. Copypsscriptanalyzer/PSScriptAnalyzerSettings.psd1as 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
.xmlfile is well-formed (every tag closed,&written as&, …). 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: trueand a 45-minute timeout. A slow model, a LiteLLM outage or a bad review can't stop a merge. - Which model: the
CI_REVIEW_MODELvariable — by default theci-revieweralias (GLM-5.3 Flash on NanoGPT). To try another model on one repo, set a repo-levelCI_REVIEW_MODELthere (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-trixiebecause 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, addpaths: "."to the step to scan the whole repo. - Two passes: a fast static scan, plus an LLM pass through LiteLLM using
CI_SKILLSCAN_MODEL(theci-reviewer-apexalias — Cantina Security's security-focused post-train of GLM-5.3 Flash). IfCI_SKILLSCAN_MODELisn't set, it usesCI_REVIEW_MODEL. Leavellm-base-urlempty 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.yamland 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
FROMline. - Report only, never fail: add
fail-on: noneto the step. - Accept a specific CVE: add its ID to a
.trivyignorefile 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:
- Make a branch in this repo (e.g.
tweak-shellcheck), commit the change. - 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. - Optional real-world test: in one other repo, temporarily change
ci-actions/lint-all@maintoci-actions/lint-all@tweak-shellcheckon a branch there and open a PR. (lint-allruns the check folders from the same branch, so the whole branch is tested together.) - 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:
- Make a folder
yamllint/with two files, copying the shape ofxml/:run.sh— starts withsource "$GITHUB_ACTION_PATH/../lib/common.sh", sets its defaults with: "${INPUT_VERSION:=…}"lines, then: find files withlist_files '\.ya?ml$', callskipif there are none, download the tool withdownload, run it, andfail "…"with a helpful message if it finds problems.action.yml— name, description, inputs (default""), and one step that runsbash "$GITHUB_ACTION_PATH/run.sh"with the inputs passed asINPUT_*environment variables. Only needed if you want to be able to use the check on its own.
- Shared defaults go next to it (like
ruff/ruff.toml), used only when the repo has no config of its own. - Turn it on everywhere: add
yamllinttoCHECKS=( … )at the top oflint-all/run.sh. - Fixtures: add a passing example to
test/fixtures/good/and a failing one totest/fixtures/bad/.test/run-fixtures.shreads the check list fromlint-all, so it tests the new check automatically. - Make the change on a branch and let self-test go green before merging —
once it's on
main, every repo runs it. - 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.