Skip to content

Latest commit

 

History

611 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

karinto

curl-able GitHub Actions linter. POST a workflow or action.yml and get back JSON diagnostics. Rules are inspired by actionlint, zizmor, and ghalint — see THIRD_PARTY_LICENSES.md.

Public endpoint: https://karinto.toiroakr.workers.dev

Try it in the browser: https://toiroakr.github.io/karinto/

Coverage

73 of 84 catalogued rules are active. They cover syntax, expression typing and context availability, permissions hygiene, pinned-uses requirements, taint analysis for template injection, and a range of security policies (excessive permissions, self-hosted runners, OIDC migration, dangerous triggers, and more). The full catalogue with status, severity, and upstream origins lives in rules_catalog.md (human-readable mirror of the in-code source of truth at rules_catalog.mbt).

API

Recommended: send the YAML directly. POST the workflow / action.yml as the request body (or pass it as the content parameter). It works on every deployment, makes no GitHub round-trip, and is not subject to any GitHub rate limit. This is the path to use in CI and other automated callers.

The repo-mode endpoints (below) fetch files from GitHub for you instead of taking content. They are opt-in — disabled unless the deployment sets the REPO_MODE_ENABLED variable, so they may be off on a given endpoint (including the public karinto.toiroakr.workers.dev). When enabled they lean on GitHub's raw hosting and API, so they are far more likely to hit GitHub rate limits: whole-repo discovery in particular shares a single unauthenticated 60 req/hour/IP budget across all callers (the requests egress from the Worker's shared Cloudflare IP). Reach for repo mode for one-off, interactive checks — not high-volume automation; send content there.

GET or POST. Parameters can come from the URL path (/<owner>/<repo> to lint every workflow on the default branch; /<owner>/<repo>/<commit>[/<target/path/...>]; or a domain-swapped GitHub file URL /<owner>/<repo>/{blob,tree,raw}/<ref>/<target/path/...> — segments after the commit/ref are joined into a single nested target path), the query string, the request body (raw key=value&..., JSON, or a plain YAML blob), or any mix — body beats query, query beats path on conflict. Paths that don't match the repo-mode shape are ignored so the Worker can be served under arbitrary path prefixes.

When repo is set with no targets (and no path target), karinto discovers and lints every *.yml / *.yaml file under .github/workflows on the chosen ref — the default branch when no commit/ref is given. This is the only mode that calls the GitHub API on the request path (directory listing has no raw.githubusercontent.com equivalent), so it is subject to GitHub's unauthenticated rate limit (60 req/hour/IP) — see Whole-repo mode below.

Key Type Notes
type workflow | action | dependabot | (omit) Optional; auto-detected when blank (jobs/on → workflow, runs → action, updates → dependabot)
content string The YAML source. The recommended input — always available, no GitHub round-trip, no rate limit.
disable string Comma-separated glob patterns of rule IDs to skip. At most 64 patterns, 128 characters per pattern, and one * per pattern.
repo owner/name Repo mode; mutually exclusive with content. Opt-in (REPO_MODE_ENABLED) and GitHub-rate-limit-prone — see the note above. Returns 403 when the deployment has repo mode disabled.
commit hex SHA, 7–64 chars An immutable pin. Either this or ref is required in explicit-target mode (targets= or a URL-path target); whole-repo discovery (no targets) defaults to the repo's default branch when neither is given. Non-hex branch/tag names (e.g. main, v1.2.3) are rejected here — use ref for those. A short SHA can collide with an all-hex branch/tag (e.g. deadbee), so use the full 40-char SHA for guaranteed immutability.
ref branch | tag | HEAD | SHA Mutable ref; fetches that ref's latest commit. Use it to lint the default branch (ref=HEAD) or any branch/tag by name. Takes precedence over commit. A domain-swapped GitHub URL (…/blob/<ref>/<path>) fills this from the path. Slashy branch names (release/1.x) work via ref= but not the path form, which treats only the first post-blob segment as the ref.
targets string Comma-separated literal file paths. Globs are not supported — list each file. At most 50 paths; requests over the cap are rejected with 400 rather than silently truncated. Omit it (with no path target either) to lint all .github/workflows files on the chosen ref — see Whole-repo mode.
osv 1 / true Query OSV.dev for known-vulnerable actions (adds 50–300 ms)
forbidden string Caller-supplied denylist for forbidden-uses. Comma-separated globs matched against uses: refs.
archived string Caller-supplied owner/repo for archived-uses, merged with the daily KV-cached baseline.
no_capture 1 / true Skip persisting this request to the dark-launch capture store (see Privacy)
format json | sarif Output format; defaults to the JSON envelope. sarif returns a SARIF 2.1.0 document — see SARIF output.
path string Repo-relative path of the content (e.g. .github/workflows/ci.yml). In format=sarif it becomes the SARIF artifactLocation.uri; in either format it also resolves a ghalint exclude's workflow_file_path scope and an actionlint paths.<glob> scope. Same shape rules as targets paths. Unnecessary in repo mode, where targets already carry paths.
persona regular | pedantic | auditor Analysis profile (see Personas). Defaults to auditor (every finding). An unrecognized value is a 400.
ghalint string Verbatim ghalint config (ghalint.yaml) text; its excludes: list suppresses matching findings (see Ignoring findings). At most 64 KiB.
zizmor string Verbatim zizmor config (zizmor.yml) text; its rules.<id>.disable / rules.<id>.ignore suppress matching findings (see Ignoring findings). At most 64 KiB.
config string Verbatim karinto config (karinto.yaml) text, or a compatible .github/actionlint.yaml; see Ignoring findings. At most 64 KiB.
actionlint string Verbatim actionlint config (.github/actionlint.yaml) text; its paths.<glob>.ignore patterns suppress the karinto rule(s) they fingerprint to (see Ignoring findings). At most 64 KiB.

forbidden / archived accept at most 200 comma-separated entries of 256 characters each. The response also carries online_audit_candidates — the external uses: refs that need a live GitHub API lookup (impostor-commit, ref-version-mismatch). karinto does not resolve those; a companion action (companion-action/) checks them and reports directly. See Action-side context for the full flow.

Personas

persona mirrors zizmor's persona model: each finding declares the minimum persona at which it surfaces, and a request shows every finding at or below the requested level (regular ⊂ pedantic ⊂ auditor).

Persona Shows
regular High-confidence, real problems only — the equivalent of a default zizmor run.
pedantic The above plus code smells / likely-safe-but-noisy findings.
auditor (default) Everything, including low-confidence / defense-in-depth findings.

The default is auditor so the bare endpoint and CLI keep reporting every finding. Pass persona=regular to match what a stock zizmor invocation (no --pedantic / --persona) would flag.

Findings that are not regular (hidden unless you opt up):

  • Pedantic — anonymous-definition, self-hosted-runner, undocumented-permissions, concurrency-limits, the template-injection Info backstop (the per-${{ … }} finding; see the split below), the excessive-permissions per-key finding (<x>: write is overly broad), and the pedantic-only subset of superfluous-actions.
  • Auditor — secrets-outside-env, and misfeature's defaults.run.shell: cmd finding.

Everything else fires under regular. Several rules are persona-split per finding, pinned against zizmor's actual per-persona behaviour (see persona_gating_test.mbt / persona_regular_parity_test.mbt):

  • excessive-permissions — blanket write-all/read-all and the per-job "default permissions used" finding are regular; the workflow-level "no permissions: block" finding and the per-key over-scope finding are pedantic (matching zizmor, which only surfaces the workflow-level default under --pedantic).
  • template-injection — the high-cap event contexts are regular (Error); non-static influenceable contexts (vars.*, inputs.*, *.outputs.*, github.ref*, github.actor, github.workflow) are regular (Info); truly-static contexts (github.run_id, github.sha, matrix.*, …) and env.* (karinto can't tell static from dynamic) are the pedantic backstop.
  • superfluous-actions — regular for most of the catalogue; pedantic for the subset zizmor gates there (e.g. peter-evans/create-pull-request, dtolnay/rust-toolchain).

Ignoring findings

Beyond the disable glob list (which skips a rule everywhere), karinto honours the opt-outs authors already wrote for the upstream tools:

  • Inline comments — # karinto: ignore[rule-id] or # zizmor: ignore[rule-id] suppresses findings on that same line (line-scoped, not file-global), whether it's a trailing comment (run: foo # karinto: ignore[rule]) or, when the comment sits on its own line with nothing else on it, the line directly below:

    jobs:
      build:
        runs-on: ubuntu-latest
        permissions:
          contents: write
        steps:
          - name: Publish
            # karinto: ignore[use-trusted-publishing]
            run: pnpm publish

    This "disable next line" form only kicks in for a comment-only line — a trailing comment always applies to its own line alone, never to the line after it, so two adjacent findings for the same rule (e.g. two steps that each run publish) don't collide with each other's ignores. It's mainly useful for a finding with no owning step (a workflow- or job-level one like undocumented-permissions, about a permissions: block rather than any one step), which anchors to that field's own line (permissions:) or, absent a more specific field, the job's own key line (build:) — not whichever step happens to be nearby:

    jobs:
      build:
        runs-on: ubuntu-latest
        # karinto: ignore[undocumented-permissions]
        permissions:
          contents: write
        steps:
          - run: echo hi

    For a step-scoped finding, a standalone comment also works directly above the step's own list-item (-) line, not just directly above the specific field — handy when a step has several lines (name:, then run:) and you'd rather annotate the whole step than the one line the finding happens to point at:

    steps:
      # karinto: ignore[use-trusted-publishing]
      - name: Publish
        run: pnpm publish

    List several rules with commas (# zizmor: ignore[a, b]) and add a free-form note after the bracket (# zizmor: ignore[a] handled upstream). This matches zizmor's inline-ignore syntax; it reads straight from content, so it works on every entry point. actionlint and ghalint have no inline-comment form, so karinto recognises only the karinto and zizmor prefixes.

  • ghalint config — a ghalint.yaml excludes: list is honoured. Each entry's policy_name is mapped onto the karinto rule(s) that absorbed it (via the catalogue's origins), and the workflow_file_path / job_name / action_name / step_id scope fields are applied. Pass it through the local CLI's --ghalint-config, or over HTTP via the ghalint parameter (the config's verbatim text); the workflow_file_path scope then resolves against path in content mode or each file's repo-relative path in repo mode.

  • actionlint config — a .github/actionlint.yaml paths.<glob>.ignore list is partially honoured (#50). actionlint suppresses findings by running each ignore regex against its own diagnostic message text — a mechanism karinto can't replay verbatim, since karinto's messages don't share actionlint's wording. Instead, each pattern is compiled and executed (via moonbitlang/regexp) against a curated table of actionlint's own canonical message text (quoted from its own test fixtures) to identify which actionlint check it targets; that check's karinto rule(s) — resolved via the catalogue's origins, the same way ghalint's policy_name is — are then suppressed under the matching <glob>. This is rule-grained, not message-grained: unlike real actionlint, a pattern that in principle only ignores one specific dynamic value (e.g. one runner label) ends up suppressing every finding of the matching rule under that glob instead. A pattern that fails to compile, or doesn't match any curated check, is a silent no-op. Pass it through the local CLI's --actionlint-config, or over HTTP via the actionlint parameter; the <glob> scope resolves against path in content mode or each file's repo-relative path in repo mode, the same way as ghalint's workflow_file_path. For exact, guaranteed control instead of this best-effort fingerprinting, use karinto's own ignore-paths below.

  • zizmor config — a zizmor.yml rules: section is honoured. Each rule's disable: true skips that rule everywhere (rule ids match zizmor's audit names verbatim), and an ignore: list of filename[:line[:col]] entries drops findings of that rule in the matching file (a pinned line / col must match the finding's position; an entry without a line matches any line, and without a column any column on that line). Pass it through the local CLI's --zizmor-config, or over HTTP via the zizmor parameter; the ignore filename resolves against path in content mode or each file's repo-relative path in repo mode.

  • karinto config — a native karinto.yaml covers four things with no home in the tools above:

    • self-hosted-runner.labels: glob patterns of labels your self-hosted runners carry. Without this key at all, a runs-on: that includes self-hosted skips label validation entirely (any custom label is assumed to be a routing hint); with it — even labels: [] — the remaining labels are checked against the known set plus your globs, so a typo (gpu-a1oo vs gpu-a100) is still caught. labels: [] means "configured, no extra labels" (strict — every custom label is flagged), not "same as omitting the key".
    • config-variables: an allowlist of vars.* names. Omitted (the default) leaves the check off; [] forbids every vars.* reference; a non-empty list flags any vars.<name> not in it.
    • rules: per-rule severity override (rule-id: error|warning|info), for a finding you want to keep but re-rank.
    • ignore-paths: per-path rule suppression, keyed by a glob against the linted file's path — "*" suppresses every rule under that glob, a list suppresses just those rule ids.

    self-hosted-runner.labels and config-variables use the same key names and shapes as actionlint.yaml, so an existing .github/actionlint.yaml is already a valid (partial) karinto config for those two settings — pass it in as-is. karinto_config itself still does not read actionlint's own paths.*.ignore key (that's a separate, best-effort mechanism — see actionlint config above and --actionlint- config / the actionlint parameter); use karinto's ignore-paths for exact, rule-ID-based control instead (see the migration guide for the general actionlint → karinto mapping). Pass a config through the local CLI's --config, or over HTTP via the config parameter.

    karinto.yaml also doubles as a ghalint config and a zizmor config, so a ghalint.yaml's excludes: list or a zizmor.yml's rules: section can be pasted straight in instead of kept in separate files: karinto_config is parsed a second and third time as each of those formats, and every source is additive (an opt-out from any of them applies). rules.<id> therefore accepts either a bare severity string, or a map that combines zizmor's own disable / ignore keys with karinto's severity — the two don't conflict, since they're read independently over the same text.

    # karinto.yaml
    self-hosted-runner:
      labels:
        - "gpu-*"
    config-variables:
      - ENVIRONMENT
      - DEPLOY_TARGET
    
    # ghalint's own `excludes:` shape, read directly
    excludes:
      - policy_name: deny_inherit_secrets
        workflow_file_path: .github/workflows/release.yml
    
    rules:
      # bare severity override
      outdated-action-version: info
      # zizmor's own `{disable, ignore}` shape, plus a severity override
      template-injection:
        severity: warning
      unpinned-tools:
        ignore:
          - ci.yml:42
    
    ignore-paths:
      "fixtures/**":
        - "*"

Examples

POST with the workflow as the body:

curl -X POST --data-binary @.github/workflows/ci.yml \
     https://karinto.toiroakr.workers.dev

POST with form data:

curl https://karinto.toiroakr.workers.dev \
     --data-urlencode "content@.github/workflows/ci.yml" \
     --data "disable=permissions-*"

GET with query parameters (small payloads only):

curl -G https://karinto.toiroakr.workers.dev \
     --data-urlencode "content=$(cat workflow.yml)" \
     --data "type=workflow"

The remaining examples use repo mode, which must be enabled on the deployment (REPO_MODE_ENABLED) — otherwise they return 403. They are handy for one-off checks but draw on GitHub rate limits; prefer sending content for anything automated.

GET/POST over a public repo (single target via path):

curl "https://karinto.toiroakr.workers.dev/actions/checkout/b4ffde65f46336ab88eb53be808477a3936bae11/action.yml"

Lint the latest commit on a branch by swapping the domain of a GitHub file URL (github.com → karinto.toiroakr.workers.dev):

# https://github.com/actions/checkout/blob/main/action.yml
curl "https://karinto.toiroakr.workers.dev/actions/checkout/blob/main/action.yml"

Lint every workflow on the default branch with just owner/repo (swap the domain of a repo's GitHub URL):

# https://github.com/actions/checkout
curl "https://karinto.toiroakr.workers.dev/actions/checkout"

Or pin nothing and lint the default branch's latest commit via ref:

curl "https://karinto.toiroakr.workers.dev?repo=actions/checkout&ref=HEAD&targets=action.yml"

Or with explicit query parameters and multiple targets:

curl "https://karinto.toiroakr.workers.dev?repo=actions/checkout&commit=b4ffde65f46336ab88eb53be808477a3936bae11&targets=action.yml,.github/workflows/test.yml"

Whole-repo mode

repo with no targets (and no path target) lints every *.yml / *.yaml file under .github/workflows. The ref defaults to the repository's default branch; pass ref=/commit= to scan a specific branch, tag, or commit instead.

Listing a directory has no raw.githubusercontent.com equivalent, so this is the one mode that calls the GitHub contents API on the request path:

  • It is subject to GitHub's unauthenticated rate limit (60 req/hour/IP). When that is hit the request fails with 429 and a message pointing you at the escape hatches (retry later, pass explicit targets=, or self-host with a token).
  • Set a GITHUB_PUBLIC_READ_TOKEN repo secret and the release / staging / preview deploys mirror it into the Worker (as an encrypted secret), raising the ceiling to 5000 req/hour and reaching private repos the token can see. Unset → the deployment runs token-less and this mode stays anonymous.
  • At most 50 workflows are linted per request (the targets cap). When a repo has more, the response sets "truncated": true and "discovered": <count> so you know the result is partial; pass explicit targets= to pick the rest.
  • 404 means there is no .github/workflows directory on that ref.

Limits

  • Request body: 1 MiB. Direct payloads over the cap short-circuit with 413 Payload Too Large before reaching the parser. In repo mode the request still returns 200 and the oversized file is surfaced as files[].error so the rest of the batch is unaffected.
  • Per-IP rate limit: 60 requests / minute. Over-limit traffic gets 429. Requests originating from GitHub-hosted Actions runners are exempted (their egress IPs are shared across unrelated tenants), so noisy CI tenants can't collateral-429 other CI traffic.

Response

{
  "ok": true,
  "result": {
    "kind": "workflow",
    "stats": { "jobs": 2, "steps": 2, "lines": 11 },
    "diagnostics": [
      {
        "rule": "duplicate-job-step-ids",
        "severity": "error",
        "message": "duplicate job ID `build` (conflicts with `Build` case-insensitively)",
        "job": "build"
      }
    ]
  },
  "engine_version": "0.3.1"
}

engine_version is present on every response (success and error). It is the version of the karinto engine that produced the diagnostics — see Versioning & pinning.

Each diagnostic carries rule, severity, and message. When a finding can be tied to a location, it also includes:

  • pos — the source position, as { "line": <1-based>, "col": <1-based> }. A finding about a specific field points at that field — the uses: ref, the run: script, a multi-line permissions: key. Other job/step-scoped findings point at the job key or the step entry. Omitted for workflow-global findings.
  • job — the offending job's ID (the key under jobs:). Omitted for workflow-global findings and for action-file steps.
  • step — the step the finding is about, as { "index": <0-based position in the steps list>, "id": "<step id, when declared>" }. index is always present so a step is locatable even without an id:; id is omitted when the step declares none.

pos comes from karinto's own YAML parser, which records each node's source range and resolves line/column on demand; job/step remain the fallback handles for workflow-global findings that have no single source location.

In repo mode the result is wrapped:

{
  "ok": true,
  "repo": "actions/checkout",
  "ref": "b4ffde65f46336ab88eb53be808477a3936bae11",
  "commit": "b4ffde65f46336ab88eb53be808477a3936bae11",
  "targets": ["action.yml"],
  "files": [ { "path": "action.yml", "ok": true, "result": { ... } } ],
  "engine_version": "0.3.1"
}

ref echoes the branch / tag / HEAD / SHA that was fetched ("HEAD" for the default-branch discovery case). commit is present only when ref is an immutable SHA pin (a branch/tag ref has no resolved SHA in the response — the Worker does not resolve content SHAs on the request path). A ref=main request therefore returns "ref": "main" and no commit. In whole-repo mode, targets lists the discovered files; if discovery found more than the 50-file cap, the response also carries "truncated": true and "discovered": <count>.

SARIF output

format=sarif swaps the envelope for a SARIF 2.1.0 document (content type application/sarif+json) that uploads directly to GitHub Code Scanning via github/codeql-action/upload-sarif. It works in both content and repo mode:

# content mode (always available) — pass `path=` so findings anchor to a file
curl -X POST --data-binary @.github/workflows/ci.yml \
     "https://karinto.toiroakr.workers.dev?format=sarif&path=.github/workflows/ci.yml"

# repo mode (requires REPO_MODE_ENABLED on the deployment)
curl "https://karinto.toiroakr.workers.dev/actions/checkout/b4ffde65f46336ab88eb53be808477a3936bae11/action.yml?format=sarif"

Mapping:

Envelope SARIF
rule result.ruleId, plus a tool.driver.rules[] entry carrying the catalogue title, default severity, and upstream origins
severity result.level (error / warning; info becomes note)
message result.message.text
pos physicalLocation.region (startLine / startColumn)
job / step locations[].logicalLocations[] (kind: "job" / "step")
parse_error one error-level result under a synthetic parse-error rule
files[].error (repo mode) invocations[].toolExecutionNotifications[], with executionSuccessful: false
engine_version tool.driver.version

physicalLocation needs an artifact URI, so it is emitted when the input has a path: always in repo mode, and in content mode only when the request also passes path=. Pathless results keep their logical (job/step) locations, but GitHub Code Scanning cannot anchor those to a file — pass path= (or use repo mode) when uploading. online_audit_candidates and the dark-launch capture are envelope-mode-only.

A complete workflow that surfaces findings in the repository Security tab (public repos — repo mode fetches the files from raw.githubusercontent.com). Note this uses repo mode, so it only works against an endpoint with REPO_MODE_ENABLED set; otherwise POST each file as content with format=sarif&path=…, or run the Local CLI:

name: karinto
on:
  push:
    branches: [main]
permissions:
  contents: read
  security-events: write
jobs:
  karinto:
    runs-on: ubuntu-latest
    steps:
      - name: Lint workflows to SARIF
        run: |
          curl -sf "https://karinto.toiroakr.workers.dev/${{ github.repository }}/${{ github.sha }}?format=sarif&targets=.github/workflows/ci.yml,.github/workflows/release.yml" \
            -o karinto.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: karinto.sarif

For private repos, run the Local CLI on a checkout instead (--format sarif with file arguments) — and note the CLI exits 1 when error-severity findings exist, so give the lint step continue-on-error: true (or append || true) so the upload step still runs.

Local CLI

Lint local files without deploying anything or making a network round-trip — same engine, same JSON envelope as the Worker. Requires Node.js (the CLI targets MoonBit's js backend).

The examples below (moon run --target js cmd/main) never fire the tree-sitter-bash shell rules (github-env, unpinned-tools, unredacted-secrets, use-trusted-publishing, shell-quote-safety, shell-undefined-var) — they need an async setup step this invocation doesn't await, so those rules just no-op. For shell-rule-enabled linting, build once and use the cli.mjs wrapper instead: moon build --target js --release && node cmd/main/cli.mjs -- ci.yml.

# stdin
cat .github/workflows/ci.yml | moon run --target js cmd/main

# file arguments (kind auto-detected from filename / content)
moon run --target js cmd/main -- .github/workflows/ci.yml action.yml

# the same knobs as the Worker's `type` / `disable` parameters
moon run --target js cmd/main -- --type action --disable 'permissions-*' action.yml

# pick an analysis profile (see *Personas*); default is auditor (every finding)
moon run --target js cmd/main -- --persona regular .github/workflows/ci.yml

# honour a ghalint config's `excludes` (see *Ignoring findings*); the file
# path is matched against each exclude's `workflow_file_path`
moon run --target js cmd/main -- --ghalint-config ghalint.yaml .github/workflows/ci.yml

# honour a zizmor config's `rules.<id>.disable` / `ignore` (see *Ignoring findings*)
moon run --target js cmd/main -- --zizmor-config zizmor.yml .github/workflows/ci.yml

# honour an actionlint config's `paths.<glob>.ignore` (best-effort — see
# *Ignoring findings*); the file path resolves the `<glob>` scope
moon run --target js cmd/main -- --actionlint-config .github/actionlint.yaml .github/workflows/ci.yml

# honour a karinto config (self-hosted-runner labels, config-variables,
# per-rule severity, ignore-paths — see *Ignoring findings*); a compatible
# .github/actionlint.yaml also works here
moon run --target js cmd/main -- --config karinto.yaml .github/workflows/ci.yml

# SARIF 2.1.0 for GitHub Code Scanning (same as the Worker's `format=sarif`);
# file arguments become artifact URIs, stdin yields pathless results
moon run --target js cmd/main -- --format sarif .github/workflows/ci.yml > karinto.sarif

Exit codes are CI-friendly: 0 clean, 1 error-severity diagnostics or YAML parse errors, 2 usage / IO errors. moon run does not propagate the program's exit status, so in CI (or pre-commit hooks) run the built bundle directly:

moon build --target js --release
node _build/js/release/build/cmd/main/main.js .github/workflows/ci.yml

Versioning & pinning

https://karinto.toiroakr.workers.dev always serves the latest release. For reproducible CI, three options:

  • Exact pin — https://karinto-vX-Y-Z.toiroakr.workers.dev (immutable snapshot per release; dots → dashes).
  • Major alias — https://karinto-vX.toiroakr.workers.dev (auto-rolls within a major; shielded from a future 1.0.0).
  • Self-host on your own Cloudflare account.
curl -X POST --data-binary @workflow.yml \
     https://karinto-v0-3-1.toiroakr.workers.dev

Every response carries an engine_version field on both success and error paths, so even against the bare endpoint you can jq -e '.engine_version == "0.3.1"' to fail loudly the moment the engine drifts.

Full guide — including the auto-prune retention rule and the 404 risk for long-untouched exact pins, self-hosting steps, and a Renovate preset for auto-bumping URL pins — in docs/pinning.md. Released versions are listed under GitHub Releases.

Private repositories

For private repos pass content directly — a deployment without a token fetches repo-mode files anonymously and cannot read private repos. A deployment with a GITHUB_PUBLIC_READ_TOKEN secret (see DEVELOPMENT.md) can reach private repos the token can see in whole-repo discovery mode.

Privacy

The production deployment persists successful requests (the content plus a few non-secret query parameters) and the corresponding response into a private bucket for up to 30 days. These captures are used to replay traffic against PR previews and detect regressions before they reach prod. To opt out per-request, send either:

  • query / form parameter no_capture=1, or
  • HTTP header X-Karinto-No-Capture: 1

Requests using osv=1 or repo= are never captured; nor are requests whose content exceeds the per-deployment cap (default 100 KiB, tunable via the CAPTURE_CONTENT_LIMIT_KIB Worker variable — see DEVELOPMENT.md).

Development

Build, test, deploy, and rule-catalog notes live in DEVELOPMENT.md.

Contributors

Languages