This is the Lentago Labs fleet's reusable-CI hub: one repository of
workflow_call
definitions — an agentic @claude responder and reviewer, a docs-link checker,
ShellCheck, and a Terraform lint suite — that every Lentago Labs repo calls by reference rather than
copying into its own .github/. Callers pin to an immutable semver tag
(@v1.0.0); upgrading means a Dependabot-opened bump PR in the caller repo, not
silent propagation. If you manage CI for more than a couple of repos, this is the
"define the pipeline once, everyone inherits it" pattern working end to end —
the enterprise rhyme is a central pipeline library.
Authorship: The workflows and documentation in this repo are co-written with Claude (Anthropic). I direct the work and review the output; Claude writes the YAML. I'm an infrastructure operator, not a software engineer — please don't read this repo as a portfolio of coding ability.
Architecture decisions: docs/adr/ records the reasoning behind this repo's structural choices — canonical policy placement, the @main→semver-tag migration, the advisory review gate, backwards-compatible contracts.
DeepWiki maintains an AI-generated wiki over this repository — architecture pages, diagrams, and a Q&A box grounded in the actual code. Every public Lentago Labs repo is indexed (deepwiki.com/lentago); it is the fastest way to orient before reading source. It is AI-generated: trust it to orient you, verify against the code before you act on it.
Good first questions:
- How does the
claude-responder.ymlworkflow route between opus, sonnet, and haiku models, and who can trigger it? - What does
docs-check.ymldo differently from a simple link-checker, and why must callers omit thepaths:filter? - Why is the Claude PR review in
claude-review.ymladvisory/non-blocking instead of a required status check?
Every row is a pattern you can lift into your own CI, with a link to where it actually runs here.
| Pattern | How it shows up here |
|---|---|
| Reusable workflows, called by reference not copy-paste — fix or extend the pipeline in one place, consumers upgrade via a Dependabot-opened bump PR | Each definition is on: workflow_call; callers write uses: lentago/shared-workflows/.github/workflows/<name>.yml@v1.0.0 (claude-responder.yml) |
| Agentic PR responder with label-gated model routing — let an LLM act inside CI with human-controlled cost/capability tiers set by a label, not a per-repo hardcode | claude-responder.yml reads model:opus / model:sonnet / model:haiku off the issue or PR to pick --model, falling back to default_model |
| Advisory (non-blocking) automated review — decouple "useful signal" automation from "merge gate" automation so a flaky AI call never stalls delivery | claude-review.yml runs the review step continue-on-error, always exits 0, and posts a soft-fail comment instead of reddening the check |
| A required check must be unconditional — a required status check whose workflow never triggers is held "Expected" forever and deadlocks every non-matching PR | docs-check.yml documents "no paths: filter"; docs-check / docs-check is the one required check on this repo's main today |
| Dogfood a reusable workflow on its own source — catch a broken reusable in its own PR, not only in a downstream caller | docs-check-self.yml calls uses: ./.github/workflows/docs-check.yml (local path, not @main) so a PR is checked by the version inside that PR |
| Ref-resolution correctness for cross-repo checkouts — a reusable that checks out its own repo must use the caller's pinned ref, not the caller's | docs-check.yml resolves github.job_workflow_ref (not workflow_ref); a self-referential test masked the bug for external callers until #30 |
| One canonical policy source with declared, manually-audited mirrors — govern org-wide conventions from one file while being explicit about where the text is duplicated and why | CLAUDE.md's "Fleet PR-workflow (canonical source)" section names its two mirrors and states the keep-in-sync obligation (manual discipline, not automation) |
Backwards-compatible workflow_call contracts — interface discipline for shared automation: a new required input breaks every caller silently |
CLAUDE.md conventions: add inputs as optional with sensible defaults, never required |
These definitions are the fleet's CI-as-code surface. Each block below is
the exact caller YAML an operator copies into a consuming repo's
.github/workflows/ — the caller passes a thin with: block and the reusable
workflow handles auth, context, output format, and the heavy lifting.
Interactive @claude responder — the team-facing on-ramp to the agent fleet.
It fires when the literal string @claude appears in a comment, review, or
issue/PR body or title, and routes to opus/sonnet/haiku based on
model:opus / model:sonnet / model:haiku labels on the issue or PR
(re-apply the label to re-trigger with the new model). With no model:* label
it uses the caller's default_model (default sonnet). For ops readers: this
is a cost/capability dial an issue-triager controls with a label, so the
expensive model is opt-in per thread rather than baked into every repo.
name: Claude Code
on:
issue_comment:
types: [created]
pull_request:
types: [opened, synchronize, labeled]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
issues:
types: [opened, edited, labeled, assigned]
jobs:
claude:
uses: lentago/shared-workflows/.github/workflows/claude-responder.yml@v1.0.0
secrets: inherit
with:
allowed_tools: '"Bash(git add:*)" "Bash(git commit:*)" "Read" "Edit" "Write"'
# default_model: opus # optional, default "sonnet"
# max_turns: 25 # optional
# extra_args: | # optional, e.g. --verbose
# --verbose
# --output-format stream-jsonAutomated PR review pinned to Haiku. Caller passes a focus block describing the repo and what to look for; the reusable workflow wraps it with the common PR-context header, rules, and output-format scaffolding.
The review is advisory and non-blocking: the review step runs
continue-on-error and the job always exits 0, so a transient API failure
or turn-budget exhaustion never reds the check or blocks a merge — if the
review can't complete, a neutral soft-fail comment is posted instead. This is
the deliberate "useful signal, not a gate" split: it is not a required check,
and you should not treat it as one. Optional inputs: model (default haiku),
max_turns (default 40, set high enough that a normal review finishes before
the cap), allowed_bots (default "*", so the fleet's own agent-opened PRs get
reviewed too).
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
paths-ignore:
- "*.md"
- "docs/**"
- "LICENSE"
- ".gitignore"
jobs:
claude-review:
uses: lentago/shared-workflows/.github/workflows/claude-review.yml@v1.0.0
secrets: inherit
with:
review_prompt: |
This repository is X. Focus your review on:
1. **Foo** — ...
2. **Bar** — ...Resolves relative markdown links across a repo's git-tracked markdown and fails on genuinely broken ones — the fleet's most common change class ships documentation, and renames/removals silently break relative links. It is more than a generic link-checker: it understands the fleet's two known false-positive classes (below) and resolves them by rule instead of flagging them.
Trigger it unconditionally — no paths: filter. This workflow is built to
serve as a required status check, and a required check whose workflow never
triggers is held "Expected" forever and deadlocks every non-matching PR (the
hard rule in fleet-ops/required-checks.json). Trigger on pull_request
across the board; the checker is cheap and skips fast when no markdown changed.
The checker skips two link classes by rule — the two that accounted for 237 of 240 raw failures in the fleet-wide scan (lentago/.github#57):
- Site-absolute router routes (
/library/,/guides/stormwater/) — resolved by an Astro/Starlight site router at build time, not the filesystem. - Links that escape the repo root (
../../issues/5) — GitHub's repo-relative navigation convention, which resolves on github.com and can never point at a tracked file anyway.
Anything left is handled by the ignore input (newline-separated globs matched
against the source-file path or the link target) or a checked-in
.docs-check-ignore file at the repo root (one glob per line, # comments).
name: docs-check
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
# No paths: filter — this is a required check and must always report.
jobs:
docs-check:
uses: lentago/shared-workflows/.github/workflows/docs-check.yml@v1.0.0
with:
# ignore: | # optional, per-repo false positives
# */api-reference/*
# CHANGELOG.md
# paths: "*.md *.markdown" # optional, default shownThe link resolver lives at ci/check_docs_links.py —
a single, testable source promoted from lentago/.github's ci/validate.py.
Run its test suite locally with python3 ci/test_check_docs_links.py. This repo
also dogfoods the checker on its own markdown via
docs-check-self.yml, which uses the
local path ref so a PR is validated by the resolver inside that PR.
ShellCheck for bash scripts. Pass an explicit scripts list, or leave it
empty for repo-wide find -name '*.sh' discovery.
name: ShellCheck
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
jobs:
shellcheck:
uses: lentago/shared-workflows/.github/workflows/shellcheck.yml@v1.0.0
with:
scripts: |
deploy.sh
scripts/start.sh
# severity: warning # optional, default "warning"Terraform quality gate — runs four checks over a caller-specified directory:
terraform fmt -check -recursive (formatting), terraform init -backend=false + terraform validate (config validity — no cloud
credentials required, deliberately runnable on fork PRs), tflint
(idiomatic Terraform and provider-specific rules), and trivy config (IaC
misconfiguration scan; trivy is chosen for consistency with site-deploy.yml's
container scan, one tool across two surfaces).
Each gate is individually toggleable via a boolean input. Trigger it
unconditionally — no paths: filter if you intend to register it as a
required status check. A required check whose workflow never triggers is held
"Expected" forever and deadlocks every non-matching PR (the hard fleet rule).
name: Terraform Lint
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# No paths: filter if this is a required check — see tf-lint.yml header.
jobs:
tf-lint:
uses: lentago/shared-workflows/.github/workflows/tf-lint.yml@v1.2.0
with:
working_directory: terraform # optional, default "terraform"
# terraform_version: "1.9.8" # optional, default "latest"
# tflint_version: "v0.54.0" # optional, default "latest"
# enable_fmt: false # optional — disable if fmt is enforced elsewhere
# enable_validate: false # optional
# enable_tflint: false # optional
# enable_trivy: false # optionalFull input reference:
| Input | Required | Default | Description |
|---|---|---|---|
working_directory |
no | terraform |
Directory containing Terraform configuration, relative to repo root |
terraform_version |
no | latest |
Terraform version to install (e.g. 1.9.8) |
tflint_version |
no | latest |
tflint version to install (e.g. v0.54.0) |
enable_fmt |
no | true |
Run terraform fmt -check -recursive |
enable_validate |
no | true |
Run terraform init -backend=false + terraform validate |
enable_tflint |
no | true |
Run tflint |
enable_trivy |
no | true |
Run trivy config (IaC misconfiguration scan) |
Reusable Astro-site deployment pipeline: builds the static site, packages
it into a Docker image, pushes to ECR (:latest and :<sha> tags), scans
for vulnerabilities with Trivy, attests the image with
actions/attest-build-provenance, then rolls the ECS service and waits for
stabilization. Authenticates via OIDC — no long-lived AWS credentials.
SLSA Build L3: building inside a reusable workflow (hosted in a separate repository that callers cannot modify) satisfies SLSA Build L3. Attestations are stored in GitHub's Sigstore-backed store and can be verified offline:
gh attestation verify oci://<registry>/<ecr_repo>@<digest> --owner lentagoCaller permissions required (on the calling workflow's job: block):
permissions:
id-token: write # OIDC for AWS auth and attestation signing
contents: read
attestations: write # write to GitHub's attestation store
packages: read # required by actions/attest-build-provenanceThe caller wires up secrets: inherit so the OIDC token and any other
org/repo secrets are forwarded transparently.
Minimum caller example (pin to the release that introduced this workflow):
name: Build & Deploy
on:
push:
branches: [main]
workflow_dispatch: {}
jobs:
deploy:
uses: lentago/shared-workflows/.github/workflows/site-deploy.yml@v1.1.0
secrets: inherit
permissions:
id-token: write
contents: read
attestations: write
packages: read
with:
ecr_repo: solidago-dev-mysite
ecs_cluster: solidago-dev-cluster
ecs_service: solidago-dev-mysite
role_arn: arn:aws:iam::365184644049:role/solidago-dev-github-actionsFull input reference:
| Input | Required | Default | Description |
|---|---|---|---|
ecr_repo |
yes | — | ECR repository name |
ecs_cluster |
yes | — | ECS cluster name |
ecs_service |
yes | — | ECS service name |
role_arn |
yes | — | IAM role ARN to assume via OIDC |
aws_region |
no | us-east-1 |
AWS region |
node_version |
no | 20 |
Node.js version for the Astro build |
fetch_depth |
no | 1 |
Git fetch depth (0 = full history; required for Starlight lastUpdated) |
pre_build_command |
no | "" |
Shell command run after npm ci and before npm run build (e.g. a Python content-sync script) |
build_env_vars |
no | "" |
Newline-separated KEY=VALUE pairs exported as env vars for the build step (e.g. PUBLIC_ASK_ENDPOINT=${{ vars.PUBLIC_ASK_ENDPOINT }}) |
attest |
no | true |
Attest the pushed image with actions/attest-build-provenance |
Site-specific examples:
# site-icecreamtofightwith-com: Python content sync before build
with:
ecr_repo: solidago-dev-app
ecs_cluster: solidago-dev-cluster
ecs_service: solidago-dev-app
role_arn: arn:aws:iam::365184644049:role/solidago-dev-github-actions
pre_build_command: python3 sync_recipes.py
# site-pondviewlane-com: full git history for Starlight lastUpdated + build env var
with:
ecr_repo: solidago-dev-pondview
ecs_cluster: solidago-dev-cluster
ecs_service: solidago-dev-pondview
role_arn: arn:aws:iam::365184644049:role/solidago-dev-github-actions
node_version: "24"
fetch_depth: 0
build_env_vars: "PUBLIC_ASK_ENDPOINT=${{ vars.PUBLIC_ASK_ENDPOINT }}"Trivy scan runs informational-only (exit-code: 0) — it reports
CRITICAL/HIGH unfixed findings in the job log but never blocks the deploy.
This gives visibility without introducing a new breakage vector on an
existing fleet of live sites.
Callers pin to an immutable semver tag — @v1.0.0, @v1.1.0, etc.
(ADR-0005).
A full release process — what must be green before tagging, the semver policy
for reusable workflows (what counts as a breaking change vs. minor vs. patch),
and how callers upgrade — is in RELEASING.md.
@main is not a supported consumption path. It continues to work mechanically
but carries no stability guarantee; a merge here may change caller CI instantly
and silently.
This is a lab — the systems are real, the stakes are not. Pick a vector:
Invoke the fleet agent from any issue or PR. This is the lowest-friction
on-ramp — no code, just a comment. Write @claude in a comment, review, or the
body/title of an issue or PR on any repo that wires up the responder, and the
claude-responder.yml if: predicate
fires the agent. Add a model:opus, model:sonnet, or model:haiku label to
route which model answers (the label overrides the caller's default_model;
re-apply it to re-trigger). Note on access: nothing in this workflow restricts
invocation to org members — the literal-@claude predicate is all that gates
it, so who can reach the agent is governed by the caller repo's own event and
trigger permissions, not by a check here. Proof this works: the agentic
layer is actively hardened — #15 fix(claude-review): make the reviewer advisory
and non-blocking and
#10 fix(claude-review): default allowed_bots to "*" so fleet bot PRs get
reviewed.
Ship a new reusable CI check to the whole fleet. Add or edit a
workflow_call definition under .github/workflows/, test it by pointing one
caller repo's uses: at @<branch-name> for a single merge, then open a PR
here. After it merges to main, cut a release tag — caller repos pick up the
change when they bump their uses: ref to the new tag (Dependabot opens those
bumps as reviewable PRs where it is enabled). See RELEASING.md.
Proof this works (historical, pre-versioning): #28 Add reusable docs-check workflow (relative markdown
links),
#29 Dogfood docs-check on this repo's own
markdown, and
#30 docs-check: resolve the tooling ref from
job_workflow_ref — the
last fixed a cross-repo-caller bug (job_workflow_ref vs workflow_ref) that a
self-referential test had masked for every external caller.
Amend the fleet's canonical PR-workflow policy. The
CLAUDE.md "Fleet PR-workflow (canonical source)" section is the
single source of truth for the fleet's PR conventions. Editing it means
propagating the same change to its two documented mirrors — ~/repos/CLAUDE.md
and the review_prompt block in claude-review.yml — in the same PR. That sync
is a stated manual obligation, not automation, so the discipline is part of the
change. Proof this works: #11 docs: neutralize PR voice — drop the ##
Origin section and Prompt-Origin
trailer and #6 Re-anchor
fleet PR-workflow as canonical here + add CI review
checks, which folded the
rules into claude-review.yml's prompt as machine-checked review criteria.
🌱 Lentago Labs is a team learning lab — real systems, non-critical stakes, modern operations patterns demonstrated in the open. Start at the org profile, and read this repo on DeepWiki.