Reusable GitHub Actions workflows for CI/CD. Call them from any repo with workflow_call.
cicd-toolkit is an opinionated delivery platform in a repo: wire a few
caller files into any project and it gets verified builds with AI code review,
deploys to AWS / Azure / Cloudflare, fully automatic semver releases with
engineer notes and end-user-facing "what's new" feeds, and the operational
guardrails a small team never gets around to building — approval-gated
promotion, one-click rollback, policy scanning, build provenance, self-healing
releases, and an AI doctor that triages red CI. Everything is consumed at
@main (or pinned), so fixes ship to every project on merge. A bundled
Claude Code plugin teaches AI agents in consumer repos
to integrate all of it themselves.
| Capability | Provided by | Notes |
|---|---|---|
| CI: build, test, lint (Turborepo-aware) | build-verify.yml |
Node 24, npm/pnpm, shared cache |
| AI code review on every PR | embedded in build-verify.yml, standalone claude-review.yml |
Inline + sticky comments; opt-in test-gap analysis; enforced finding disposition via the Review Threads gate |
| Conventional-commit enforcement | commitlint.yml |
The contract that powers automatic versioning |
| Container images | docker-ghcr.yml |
BuildKit provenance; opt-in attestations |
| Deploy: AWS CDK | cdk-deploy.yml + cdk-synth.yml PR check |
OIDC auth; default-on report-only checkov policy scan |
| Per-PR preview environments | preview-s3-deploy.yml |
Sticky preview URL; auto-teardown on close; needs base-path-aware builds |
| Deploy: static sites | static-s3-deploy.yml |
S3 + CloudFront + cache strategy |
| Deploy: containers (scale-to-zero) | Lambda Web Adapter + Function URL behind EcsExpressEdgeStack — see br-event-platform as the canonical example; provision infra with cdk-deploy.yml |
No VPC/ECS/Fargate — those are denied by org policy |
| Deploy: Azure Container Apps | aca-provision.yml + aca-deploy.yml |
Bicep + Azure OIDC |
| DNS | cloudflare-dns.yml |
Record upsert + cache purge |
| Staged promotion, deployment tracking, rollback | Environments, Promotion & Rollback | GitHub Environments gates; DORA raw data; redeploy-a-tag rollback |
| Automatic releases | auto-version.yml → release.yml |
Merge to main = release; AI notes; orphan self-heal |
| End-user release notes in your app | What's-New summaries + lib/whats-new |
Curated context, redaction judge, deny-list |
| Red-CI triage | ci-doctor.yml |
AI diagnosis issue, auto-closed on recovery |
| Invite-only signups (Cognito) | InviteGating |
Atomic single-use codes; SSM-runbook admin; prebuilt Lambdas |
| AWS infra building blocks | CDK constructs | Static site, ECS edge, OIDC bootstrap, dashboards |
| Agent-assisted integration | Claude Code plugin | Skills for wiring workflows, secrets, OIDC |
- Workflows
- Composite actions
- Setup
- CDK constructs
StaticSiteStack(S3 + CloudFront, optional ACM + Route 53)applyTags(scope, tags)StaticSiteDashboard- SharedEdgeStack (account-level CloudFront primitives)
EcsExpressEdgeStack(CloudFront in front of ECS Express)OidcBootstrapStack(GitHub → AWS OIDC provider + deploy roles)EcsExpressDashboard/ecs-express-observability- InviteGating (Cognito invite-code gating) — packages/invite-gating
- Examples
- Claude Code plugin
- Claude PR review
- License
build-verify.yml — Install, build, test, and lint a Node.js project with Turborepo caching.
jobs:
verify:
uses: KotaHusky/cicd-toolkit/.github/workflows/build-verify.yml@main
with:
node-version: '24'| Input | Type | Default | Description |
|---|---|---|---|
node-version |
string | 24 |
Node.js version |
run-build |
boolean | true |
Run the build step |
run-tests |
boolean | true |
Run the test step |
run-lint |
boolean | true |
Run the lint step |
package-manager |
string | npm |
Package manager (npm or pnpm) |
claude-review |
boolean | true |
Advisory Claude AI review on PRs; activates only when an Anthropic secret is passed |
claude-review-prompt |
string | '' |
Extra project-specific review instructions |
require-resolved-review-threads |
boolean | true |
Status check that fails while the PR has unresolved review threads — findings must be fixed or resolved-with-a-reply before merge. Needs pull-requests: read on the caller (no-ops with a notice otherwise). Don't mark it a required branch check unless every PR runs it: skipped paths (bot PRs, push events, opt-out) leave a required check stuck on "Expected" |
review-test-gaps |
boolean | false |
Also analyze test coverage of the changed lines — flags changed code paths whose tests were not updated |
Built-in Claude review: when the caller passes ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN (directly or via secrets: inherit), pull requests get an advisory AI review (inline comments + sticky summary) with no extra workflow file. It never blocks CI: no credentials → skip with a notice; insufficient permissions → the review step is swallowed. For comments to post, grant the calling job pull-requests: write (see Claude Code Review for the standalone workflow and full permission block). Requires the Claude GitHub App on the repo. Set claude-review: false to opt out.
jobs:
verify:
uses: KotaHusky/cicd-toolkit/.github/workflows/build-verify.yml@main
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
actions: read
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}commitlint.yml — Lint PR commit messages against Conventional Commits.
jobs:
commitlint:
uses: KotaHusky/cicd-toolkit/.github/workflows/commitlint.yml@main| Input | Type | Default | Description |
|---|---|---|---|
node-version |
string | 24 |
Node.js version |
Requirement: Consumer repo must have
@commitlint/cliand a commitlint config (e.g.@commitlint/config-conventional) inpackage.json.
docker-ghcr.yml — Build a Docker image and push it to GitHub Container Registry.
jobs:
docker:
uses: KotaHusky/cicd-toolkit/.github/workflows/docker-ghcr.yml@main
permissions:
contents: read
packages: write| Input | Type | Default | Description |
|---|---|---|---|
image-name |
string | ghcr.io/{repo} |
Image name override |
dockerfile |
string | ./Dockerfile |
Path to Dockerfile |
context |
string | . |
Docker build context |
build-args |
string | Build arguments (newline-separated) | |
push |
boolean | true |
Push image to registry |
platforms |
string | linux/amd64 |
Target platforms |
version |
string | Semantic version (e.g. 1.2.3). Adds v1.2.3, v1.2, v1 tags. |
|
attest |
boolean | false |
Publish a GitHub build-provenance attestation for the pushed image. Caller must grant id-token: write and attestations: write |
| Output | Description |
|---|---|
tags |
Generated image tags |
digest |
Image digest |
Provenance & attestations: images build with BuildKit provenance (mode=max) by default; setting attest: true additionally publishes a GitHub artifact attestation binding the image digest to the exact workflow run. Consumers can verify with gh attestation verify oci://ghcr.io/<owner>/<image>@<digest> -R <owner>/<repo>. Requires the caller to grant id-token: write + attestations: write.
cdk-deploy.yml — Deploy AWS CDK applications via GitHub Actions with OIDC authentication.
jobs:
deploy:
uses: KotaHusky/cicd-toolkit/.github/workflows/cdk-deploy.yml@main
permissions:
id-token: write # OIDC to AWS
contents: read
with:
aws-region: 'us-east-1'
cdk-context: 'env=prod projectName=my-app'
secrets:
role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}| Input | Type | Default | Description |
|---|---|---|---|
cdk-context |
string | — | Space-separated key=value context pairs passed as -c flags (required) |
aws-region |
string | us-east-1 |
AWS region |
node-version |
string | 24 |
Node.js version |
stacks |
string | --all |
Stacks to deploy |
stack-prefix |
string | '' |
Stack name prefix; enables stuck-CloudFormation-stack recovery |
method |
string | change-set |
Deploy method: change-set (safe) or direct (faster, no rollback) |
hotswap |
string | off |
off, fallback (try hotswap, fall back to CFN), or force |
See the workflow file for the full list (pre-build-filter, concurrency, run-diff, recover-stacks, checkout-ref).
| Secret | Required | Description |
|---|---|---|
role-arn |
yes | ARN of the IAM role to assume via OIDC |
PR check: cdk-synth.yml is the synth-only companion — it runs cdk synth with no AWS credentials or secrets, so use it as the pull-request gate to catch template errors before merge (inputs: node-version, pre-build-filter, cdk-context, all optional). If your CDK app depends on private @kotahusky/* packages from GitHub Packages, pass node-auth-token: ${{ secrets.GITHUB_TOKEN }} (or a PAT with read:packages) and add an .npmrc in your repo routing the scope: @kotahusky:registry=https://npm.pkg.github.com. See examples/cdk-deploy.yml for the paired PR-synth + main-deploy layout.
Policy scan: cdk-synth.yml also runs a checkov policy scan over the synthesized CloudFormation (policy-scan, default true). Report-only by default (policy-soft-fail: true) — findings land in the job summary and a policy-scan-results artifact without failing the check; set policy-soft-fail: false to enforce.
static-s3-deploy.yml — Build a static site (Next.js output: 'export', Astro, SvelteKit, Vite, plain HTML), sync to S3, invalidate CloudFront. Pair with the StaticSiteStack CDK construct below for one-shot infra.
jobs:
deploy:
uses: KotaHusky/cicd-toolkit/.github/workflows/static-s3-deploy.yml@main
with:
bucket-name: my-site-bucket
distribution-id: E1234567ABCDEF
build-output-dir: out # Next 'out' / Vite 'dist' / Astro 'dist'
secrets:
role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}| Input | Type | Default | Description |
|---|---|---|---|
bucket-name |
string | — | S3 bucket hosting the site (required) |
distribution-id |
string | — | CloudFront distribution to invalidate (required) |
aws-region |
string | us-east-1 |
AWS region for the bucket |
node-version |
string | 24 |
Node.js version |
package-manager |
string | npm |
npm or pnpm |
build-command |
string | npm run build |
Command that produces the static output |
build-output-dir |
string | out |
Directory to upload (relative to working-directory) |
working-directory |
string | . |
Repo subdirectory to run the build from |
invalidation-paths |
string | /* |
Newline- or space-separated paths to invalidate |
sync-delete |
boolean | true |
Pass --delete to aws s3 sync |
sync-exclude |
string | previews/* |
Glob excluded from the sync (both directions) — reserves the preview prefix so prod deploys never delete live PR previews; '' disables (and forfeits shared-bucket previews). A warning fires if the build output contains the excluded path |
cache-control-immutable |
string | _next/static/* |
Glob to upload with long-cache headers (empty disables) |
build-args |
string | KEY=VALUE pairs (one per line) exported before the build |
|
checkout-ref |
string | Git ref to check out (defaults to triggering ref) |
| Secret | Required | Description |
|---|---|---|
role-arn |
yes | OIDC role with s3:Sync and cloudfront:CreateInvalidation on the target resources |
| Output | Description |
|---|---|
invalidation-id |
CloudFront invalidation ID |
objects-uploaded |
Count of objects synced (parsed from CLI output) |
preview-s3-deploy.yml — Per-PR preview deploys for static sites: each PR's build lands at s3://<bucket>/previews/pr-<N>/, a sticky comment posts the preview URL, and closing the PR tears the prefix down. Pairs with the production static-s3-deploy.yml on the same bucket/distribution — production syncs exclude the reserved previews/ prefix, so a prod deploy (even with sync-delete: true) never touches live previews.
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
preview:
uses: KotaHusky/cicd-toolkit/.github/workflows/preview-s3-deploy.yml@main
permissions:
id-token: write # OIDC to AWS
contents: read
pull-requests: write # sticky preview-URL comment
with:
bucket-name: my-site-bucket
distribution-id: E1234567ABCDEF
preview-domain: site.example.com
secrets:
role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}Two consumer requirements: the app's build must honor PREVIEW_BASE_PATH (exported as /previews/pr-<N> before the build — e.g. Next.js basePath: process.env.PREVIEW_BASE_PATH ?? ''), and the serving StaticSiteStack needs previewIndexRewrite: true so CloudFront resolves directory indexes under subpaths. Inputs mirror static-s3-deploy.yml (node-version, package-manager, build-command, build-output-dir, working-directory, build-args) plus preview-domain (required). Output: preview-url. Teardown deletes only the PR's own previews/pr-<N> prefix (pattern-guarded) and updates the comment. See examples/preview-env.yml.
aca-provision.yml + aca-deploy.yml — Provision Container Apps infrastructure from a Bicep template (Day-2 updates; the very first bootstrap runs locally, see examples/aca.yml), then deploy a container image to the app. Both authenticate via Azure OIDC federated credentials — grant the calling workflow id-token: write.
permissions:
id-token: write # OIDC login to Azure
contents: read
jobs:
provision:
uses: KotaHusky/cicd-toolkit/.github/workflows/aca-provision.yml@main
with:
resource-group: my-app-rg
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
deploy:
needs: provision
uses: KotaHusky/cicd-toolkit/.github/workflows/aca-deploy.yml@main
with:
resource-group: my-app-rg
container-app-name: my-app
image: ghcr.io/<owner>/my-app:latest
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}aca-provision.yml inputs:
| Input | Type | Default | Description |
|---|---|---|---|
resource-group |
string | — | Azure resource group name (required; created if missing) |
location |
string | eastus |
Azure region |
bicep-file |
string | infra/main.bicep |
Path to the Bicep template in the calling repo |
aca-deploy.yml inputs (see the workflow file for the full list):
| Input | Type | Default | Description |
|---|---|---|---|
resource-group |
string | — | Azure resource group name (required) |
container-app-name |
string | — | Container App name (required) |
image |
string | — | Full container image URI (required) |
target-port |
number | 3000 |
Container listening port |
ingress |
string | external |
external or internal |
container-app-environment |
string | '' |
Container App environment name (auto-created if missing) |
| Secret | Required | Description |
|---|---|---|
AZURE_CLIENT_ID |
yes | Entra ID app registration with a federated credential for the repo |
AZURE_TENANT_ID |
yes | Azure tenant |
AZURE_SUBSCRIPTION_ID |
yes | Azure subscription |
REGISTRY_TOKEN |
no | Registry password/token (deploy only; required if registry-url is set) |
| Output | Description |
|---|---|
fqdn |
Deployed app FQDN (deploy only) |
cloudflare-dns.yml — Create or update a DNS record via the Cloudflare API (upsert by name + type), with an optional full cache purge. Typical use: a post-deploy step pointing a CNAME at a CloudFront distribution domain or a Container Apps FQDN.
jobs:
dns:
uses: KotaHusky/cicd-toolkit/.github/workflows/cloudflare-dns.yml@main
with:
record-name: app.example.com
record-content: d1234567abcdef.cloudfront.net
secrets:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}| Input | Type | Default | Description |
|---|---|---|---|
record-name |
string | — | DNS record name, e.g. app.example.com (required) |
record-content |
string | — | Record content: IP address or hostname (required) |
record-type |
string | CNAME |
A, AAAA, or CNAME |
proxied |
boolean | true |
Cloudflare proxy (orange cloud) |
purge-cache |
boolean | false |
Purge the whole zone cache after the update |
| Secret | Required | Description |
|---|---|---|
CLOUDFLARE_API_TOKEN |
yes | Token with Zone.DNS edit (plus Zone.Cache Purge if purge-cache) |
CLOUDFLARE_ZONE_ID |
yes | Zone ID from the zone's Overview page |
All AWS deploy workflows (cdk-deploy.yml, static-s3-deploy.yml) accept two additional inputs:
| Input | Type | Default | Description |
|---|---|---|---|
environment |
string | '' |
GitHub Environment for the deploy job — enables approval gates, wait timers, and environment-scoped secrets. Empty = no environment |
track-deployment |
boolean | false |
Record a GitHub Deployment + status per run (raw data for DORA metrics). Never fails the deploy; the caller must grant deployments: write or the steps notice-and-skip |
Promotion pattern: call the same reusable workflow twice with environment: dev (no protection) and environment: prod (required reviewers on the Environment) — GitHub pauses the prod job until approved.
Rollback: redeploy the old ref — see examples/rollback.yml for a workflow_dispatch rollback that points checkout-ref at a previous release tag. Infrastructure stays put; only the deployed artifact changes.
release.yml — Create a GitHub Release with a Claude-generated title and summary when a semver tag is pushed.
on:
push:
tags: ['v*']
jobs:
release:
uses: KotaHusky/cicd-toolkit/.github/workflows/release.yml@main
permissions:
contents: write
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}| Input | Type | Default | Description |
|---|---|---|---|
model |
string | claude-haiku-4-5 |
Claude model for notes + what's-new (fast/cheap fits this task) |
draft |
boolean | false |
Create as draft release |
app-context |
string | '' |
End-user-facing app description for the public what's-new summary (combined with .github/whats-new-context.md) |
whats-new |
boolean | true |
Also generate whats-new.json + releases.json release assets |
auto-publish |
boolean | true |
When false, uploads whats-new.draft.json for human review instead |
| Secret | Required | Description |
|---|---|---|
CLAUDE_CODE_OAUTH_TOKEN |
optional | Claude Pro/Max OAuth token from claude setup-token — preferred; billed to subscription |
ANTHROPIC_API_KEY |
optional | Anthropic API key — pay-per-token fallback |
| Output | Description |
|---|---|
release-url |
URL of the created release |
The workflow compares commits between the current and previous semver tags, sends the log to Claude, and creates a release titled v1.2.0 — <AI-generated title> whose body is the AI summary plus GitHub's generated "What's Changed" (PR-level changelog, Full Changelog compare link, and contributor credit). Mention-safe by construction: PR titles and any @/# tokens the summary echoes are code-spanned, so release notes can never @-mention unrelated users; the by @author attributions are the one intentional mention, driving the release's Contributors section. The commit-level changelog feeds the AI and (when whats-new is on) is uploaded as a run artifact rather than duplicated in the body.
Auth: when CLAUDE_CODE_OAUTH_TOKEN is set it is preferred — generation runs via the Claude Code CLI on your subscription (no GitHub App needed for releases; the CLI works on any trigger, including tag pushes and auto-version.yml's main pushes). Otherwise ANTHROPIC_API_KEY is used via direct API calls. Missing credentials never block the release: with neither secret the workflow emits a warning annotation and still creates the release — title is just the tag, body is GitHub's generated notes, and the what's-new job is skipped.
auto-version.yml — Make merging to main the whole release process: computes the next semver from conventional commits since the last tag, creates the tag, and runs release.yml for it. Pair with commitlint.yml so commit messages are trustworthy.
on:
push:
branches: [main]
jobs:
auto-version:
uses: KotaHusky/cicd-toolkit/.github/workflows/auto-version.yml@main
permissions:
contents: write
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}Bump rules (matching semantic-release defaults): feat!:/BREAKING CHANGE → major, feat: → minor, fix:/perf:/revert: → patch; anything else (docs, chore, ci, refactor, …) releases nothing. Merge commits are ignored.
| Input | Type | Default | Description |
|---|---|---|---|
initial-version |
string | 0.1.0 |
First release when no semver tag exists yet (fires only once a releasable commit is present — a chore:-only history releases nothing) |
dry-run |
boolean | false |
Report the computed bump without tagging or releasing |
floating-tags |
boolean | false |
Advance floating vN / vN.M tags after the release (for repos consumed at a floating ref; leave off for apps) |
model / draft / app-context / whats-new / auto-publish |
— | — | Forwarded to release.yml (see above) |
| Output | Description |
|---|---|
tag |
The created tag, or empty when nothing was releasable |
bump |
major, minor, patch, none, or retry (re-release of an orphaned tag) |
The tag is created with the run's GITHUB_TOKEN, whose events don't trigger other workflows — release.yml is invoked directly as a nested workflow, so no PAT is needed and a tag-push release workflow can coexist without double-releasing. Manual v*.*.* tags keep working as an escape hatch and become the new baseline for the next auto bump.
If a release run fails after tagging (leaving a tag with no GitHub Release), the next run — including a manual full re-run — detects the orphan and re-releases that tag instead of computing a new bump (bump: retry); commits merged in the meantime ship in the following release. The self-heal only fires in repos that already have at least one GitHub Release — adopting this workflow in a repo with plain unreleased git tags computes a normal bump from the latest tag rather than surprise-releasing it.
Pinning caveat: the nested
release.ymlcall insideauto-version.ymlis fixed at@main(GitHub can't parameterizeuses:), so pinningauto-version.ymlto a tag or SHA does not transitively pin the release pipeline. If you need a fully pinned release path, callrelease.yml@<ref>yourself from a tag-push workflow instead.
Tag-only variant: semver-tag.yml is the lower-level workflow: it computes the conventional-commit bump and creates the vX.Y.Z tag (needs contents: write) but chains to no release — the caller wires downstream jobs off its outputs (new-version, new-tag, bumped, changelog) itself, as in examples/static-site.yml. Its default-bump input (default false = no bump) can force a bump when no conventional commit calls for one. Prefer auto-version.yml unless you're composing the pipeline yourself.
Releases are two-tier: the GitHub Release body stays engineer-focused and specific, while release.yml additionally generates a plain-language, end-user-facing summary your app can display — a whats-new.json (latest) and cumulative releases.json (last 20) attached to each release as assets. The artifact contract is versioned (schemaVersion: 1) and published at schemas/whats-new.schema.json.
How it stays app-aware and leak-free:
- Curated context — the generator sees only the commit subjects plus
.github/whats-new-context.mdin your repo (copyexamples/whats-new-context.md): app description, user vocabulary, tone, and a deny-list. It's the only app knowledge the summarizer gets — keep it updated as features change. - Generation rules — user-visible changes only; internal-only changes collapse to "Stability and performance improvements"; security fixes are never described specifically; commit text is treated as data, not instructions.
- Redaction judge — a second Claude pass reviews the draft against the context file and rewrites anything that reveals internals.
- Mechanical deny-list — publishing fails hard if any deny-listed term (yours + built-in defaults like
secret,token) appears in the final text. The release itself is unaffected.
Getting the summary into your app — enable baking at deploy/build time so the app reads a local file that always matches the deployed version (no client-side GitHub API, works for private repos):
# static sites (S3/CloudFront)
uses: KotaHusky/cicd-toolkit/.github/workflows/static-s3-deploy.yml@main
with:
whats-new-path: public/whats-new.json
# container images (GHCR) — bakes into the build context pre-build
uses: KotaHusky/cicd-toolkit/.github/workflows/docker-ghcr.yml@main
with:
whats-new-path: public/whats-new.jsonRendering it — import from this package (framework-agnostic core, optional React bindings):
import { useWhatsNew } from 'cicd-toolkit/lib/whats-new/react';
function WhatsNewBanner() {
const { release } = useWhatsNew(); // reads /whats-new.json
if (!release) return null; // absent until the first release + deploy
return (
<aside>
<h3>{release.title} <small>v{release.version}</small></h3>
<p>{release.summary}</p>
<ul>{release.highlights.map((h) => <li key={h}>{h}</li>)}</ul>
</aside>
);
}Non-React apps use fetchWhatsNew() / fetchReleaseHistory() from cicd-toolkit/lib/whats-new. Next.js users: add transpilePackages: ['cicd-toolkit'] since the package ships TypeScript sources.
claude-review.yml — Automated Claude review on pull requests via anthropics/claude-code-action. Posts inline comments for line-specific findings plus one sticky summary comment that updates on new pushes.
Requires the Claude GitHub App to be installed on the consuming repo.
on:
pull_request:
types: [opened, ready_for_review, synchronize]
jobs:
review:
uses: KotaHusky/cicd-toolkit/.github/workflows/claude-review.yml@main
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
actions: read
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}To source the key from a GitHub environment in the consuming repo instead of a repo secret, pass the environment name and inherit secrets (environment secrets only resolve with secrets: inherit):
with:
environment: claude
secrets: inherit| Input | Type | Default | Description |
|---|---|---|---|
environment |
string | '' |
GitHub environment (in the calling repo) to source secrets from; requires secrets: inherit |
model |
string | '' |
Claude model override; empty uses the action default |
review-prompt |
string | '' |
Extra project-specific review instructions |
max-turns |
string | 25 |
Max agent turns per review (cost control) |
strict |
boolean | false |
Fail the job when the review can't run (missing credentials or a review error); default is a notice annotation and a passing job |
review-test-gaps |
boolean | false |
Also analyze test coverage of the changed lines — flags changed code paths whose tests were not updated |
require-resolved-review-threads |
boolean | true |
Status check ("Review Threads Resolved") that fails while the PR has unresolved review threads — disposition each finding (fix, or resolve with a reply saying why) then re-run the failed gate job. Needs pull-requests: read; skips bot PRs. Don't mark it a required branch check unless every PR runs it — skipped paths leave a required check stuck on "Expected" |
| Secret | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
one of | Anthropic API key (pay-per-token billing) |
CLAUDE_CODE_OAUTH_TOKEN |
one of | Claude Pro/Max OAuth token from claude setup-token (uses subscription quota) |
The review is advisory by default: if no credentials are available, or the review step itself errors, the run emits a notice annotation and the job still passes — the caller's CI is never blocked. Set strict: true to fail the job with an error annotation instead.
ci-doctor.yml — When CI fails on the default branch, Claude reads the failed run's logs and files (or updates) an issue labeled ci-doctor with root cause, evidence, and a suggested fix; the next successful run closes it automatically. Claude never gets GitHub write access — issues are managed by plain gh calls, and log content is treated as data, not instructions.
on:
workflow_run:
workflows: [CI]
types: [completed]
jobs:
doctor:
if: github.event.workflow_run.head_branch == github.event.repository.default_branch
uses: KotaHusky/cicd-toolkit/.github/workflows/ci-doctor.yml@main
permissions:
contents: read
issues: write
actions: read
with:
run-id: ${{ github.event.workflow_run.id }}
conclusion: ${{ github.event.workflow_run.conclusion }}
workflow-name: ${{ github.event.workflow_run.name }}
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}| Input | Type | Default | Description |
|---|---|---|---|
run-id |
string | — | The completed workflow run to examine (required) |
conclusion |
string | — | failure diagnoses; success closes open ci-doctor issues (required) |
workflow-name |
string | — | Used in the issue title (required) |
model |
string | claude-haiku-4-5 |
Diagnosis model |
max-log-lines |
string | 400 |
Log tail sent to the model |
Secrets: CLAUDE_CODE_OAUTH_TOKEN (preferred) or ANTHROPIC_API_KEY; with neither, a bare tracking issue with the run link is still filed. See examples/ci-doctor.yml.
Step-level building blocks, referenced as uses: KotaHusky/cicd-toolkit/actions/<name>@main inside your own jobs (each has a full README):
turbo-setup— Node + npm cache + Turborepo remote cache in one stepecr-mirror— mirror a GHCR image (by digest) into ECR for ECS consumptioncfn-recover— unstick CloudFormation stacks in ROLLBACK_COMPLETE/FAILED states before a deploy
Consumer repos need to configure the following secrets depending on which workflows they use:
# For AI-powered releases (release.yml) and Claude code review (claude-review.yml)
gh secret set ANTHROPIC_API_KEY --repo <owner>/<repo>
# For CDK deployments (cdk-deploy.yml)
gh secret set AWS_DEPLOY_ROLE_ARN --repo <owner>/<repo>Generate your Anthropic API key at console.anthropic.com under API Keys. For claude-review.yml, also install the Claude GitHub App on the repo; Claude Pro/Max subscribers can set CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token) instead of an API key to draw on subscription usage rather than per-token billing.
All examples reference @main (bleeding edge — merges here ship to you immediately). Floating release tags are also maintained automatically on every release, so pick your stability tier:
| Ref | Behavior |
|---|---|
@main |
Latest, updates on every merge |
@v2 |
Latest release in major 2 — advances on each release, breaking changes gated on v3 |
@v2.5 |
Latest patch of 2.5 |
@<sha> |
Fully pinned (maximum supply-chain rigor) |
Internally, this repo pins third-party actions to commit SHAs (OpenSSF practice); Dependabot keeps the pins current.
For repos that need to deploy to AWS, bootstrap the OIDC provider and deploy role once:
npx cdk deploy --app "npx ts-node bin/bootstrap.ts"This creates an IAM OIDC Provider for token.actions.githubusercontent.com and an IAM Role trusted by your GitHub org/repo. Store the role ARN as AWS_DEPLOY_ROLE_ARN in your repo secrets.
Every role the stack creates automatically gets sts:AssumeRole on the cdk-* bootstrap roles and account-scoped cloudformation:ListStacks (ListStacks doesn't support resource-level permissions — scoping it to stack ARNs silently denies it and CDK's rollback-detection pre-check logs AccessDenied on every deploy). Roles that deploy with cdk deploy --method=direct should also set directDeployResourceOps: true to get the Cloud Control API resource actions that mode requires.
Reusable, project-agnostic constructs in lib/. Import them into your own CDK app.
Private S3 bucket + CloudFront distribution with Origin Access Control. Optionally provisions an ACM cert (us-east-1) and a Route 53 A/AAAA alias when you want a custom domain. Outputs the bucket name and distribution ID for static-s3-deploy.yml.
Custom-domain mode — pass domainName + hostedZoneName:
import { StaticSiteStack } from 'cicd-toolkit/lib/stacks/static-site-stack';
new StaticSiteStack(app, 'MySite', {
env: { account: '123456789012', region: 'us-east-1' },
domainName: 'site.example.com',
hostedZoneName: 'example.com',
spaFallback: false, // true → 403/404 → /index.html for SPAs
additionalAliases: ['www.example.com'],
});Default-CloudFront-domain mode — omit domainName entirely. No ACM cert, no DNS records; the site is reachable via the auto-generated dXXXXX.cloudfront.net. Useful for kiosk apps and internal tools where you don't want a memorable URL ("security by obscurity"):
new StaticSiteStack(app, 'MySite', {
env: { account: '123456789012', region: 'us-east-1' },
// no domainName — distribution served from its default *.cloudfront.net only
});Thin wrapper around Tags.of() that takes any flat tag map and skips blanks. Intentionally has no opinion on which keys you use — pass whatever convention your org has standardized.
import { applyTags } from 'cicd-toolkit/lib/constructs/standard-tags';
applyTags(stack, {
Project: 'kiosk',
Service: 'frontend',
Environment: 'production',
Owner: 'platform-team',
CostCenter: 'cc-100',
ManagedBy: 'cdk',
Repository: 'owner/repo',
});Enable any of those as Cost Allocation Tags in the Billing console to see spend grouped by them in Cost Explorer.
CloudWatch dashboard for a CloudFront distribution: requests, 4xx/5xx error rates, cache hit ratio, p50/p99 origin latency, bytes downloaded.
import { StaticSiteDashboard } from 'cicd-toolkit/lib/constructs/static-site-dashboard';
new StaticSiteDashboard(stack, 'SiteMetrics', {
distribution: siteStack.distribution,
dashboardName: 'kiosk-static-site',
});CloudFront cache and response-headers policies are account-scoped and capped (~20 each by default) — at two per app stack, the wall arrives around 10 apps. SharedEdgeStack creates EcsExpressEdgeStack's two capped primitives (Next-image cache policy, SSR response-headers policy) once per account and publishes their IDs to SSM under ssmPrefix (default /cicd-toolkit/edge); app stacks opt in with sharedEdge and create zero of their own. The www→apex redirect stays a per-stack CloudFront Function (functions cap at ~100/account, and viewer-request functions can't read per-distribution origin headers, so a shared one can't know the apex):
// once per account (e.g. in your bootstrap app)
new SharedEdgeStack(app, 'SharedEdge', { env });
// each app stack
new EcsExpressEdgeStack(app, 'MyAppEdge', {
env,
// ...existing props...
sharedEdge: {}, // or { ssmPrefix: '/custom/prefix' }
});Result: 1 of each policy account-wide instead of two per app — the policy quota stops being the ceiling (~200 apps; per-stack redirect functions become the next wall around ~50 alias-using apps). Omitting sharedEdge keeps the original per-stack behavior, fully backward compatible.
CloudFront distribution with a custom-domain ACM cert, alias redirects, Next.js-aware cache behaviors (static assets long-cached, /_next/image query-string-aware), and opt-in tiered observability (dashboards + alarms via observability: { tier: 'prod' | 'dev', alarmEmail }). The origin can be any HTTPS hostname — an ALB or a Lambda Function URL. This stack creates no VPC, ECS, or Fargate resources. The canonical consumer pattern is Lambda Web Adapter (scale-to-zero) behind this stack, deployed via cdk-deploy.yml. VPC/ECS/Fargate is denied by org policy.
One-time bootstrap: the GitHub OIDC provider plus a scoped deploy role per repo (RepoRole[]), each trust-limited to its repo/branch. Every role automatically gets sts:AssumeRole on the CDK bootstrap roles and account-scoped cloudformation:ListStacks; roles deploying with cdk deploy --method=direct opt into the Cloud Control grants via directDeployResourceOps: true. See OIDC Bootstrap for the deploy flow and the bootstrap-oidc plugin skill for a guided run.
CloudWatch dashboard (ALB + ECS service metrics) and the tiered alarm set used by EcsExpressEdgeStack's observability prop — usable standalone for existing services.
Self-contained L3 construct (the first resident of packages/, with its own build, tests, and prebuilt Lambda assets): gates Cognito user-pool signups behind single-use invite codes. A pre-signup Lambda atomically claims codes via conditional DynamoDB writes; admins generate/list/revoke through an SSM Automation runbook, with layered IAM (human → automation role → Lambda → table).
// npm i @kotahusky/cognito-invite-gating (requires .npmrc auth for npm.pkg.github.com)
import { InviteGating } from '@kotahusky/cognito-invite-gating';
new InviteGating(this, 'InviteGating', {
userPool, // attaches the pre-signup trigger
resourcePrefix: 'my-app', // names the table/Lambdas/runbook
appDomain: 'app.example.com',
});Operate it via SSM: aws ssm start-automation-execution --document-name <stack's runbook> --parameters 'Action=generate,...'. The package ships prebuilt Lambda zips (assets/) so consumers don't need a bundler; its test suite runs in this repo's CI alongside the root tests.
See examples/ for ready-to-copy workflow files:
aca.yml— Azure Container Apps: Bicep provision + image deploy via Azure OIDCauto-version.yml— Automatic versioning + AI release on every merge to maincdk-deploy.yml— CDK synth check on PRs, OIDC deploy on merge to mainci-doctor.yml— AI diagnosis issue when default-branch CI goes red; auto-closes on recoveryci.yml— Build verification + Docker push + commitlintclaude-review.yml— Claude PR review (inline comments + sticky summary)cloudflare-dns.yml— Upsert a Cloudflare DNS record, optional cache purgedocker-ghcr.yml— Build a Docker image and push it to GHCRrollback.yml— One-click redeploy of a previous release tag via workflow_dispatchpreview-env.yml— Per-PR static-site preview deploys with auto-teardownrelease.yml— AI-powered release on tag pushstatic-site.yml— Tag-driven release for an S3+CloudFront static sitewhats-new-context.md— Living context doc powering the end-user what's-new summaries
This repo hosts a Claude Code plugin marketplace. Installing the plugin gives Claude, in any consumer repo, skills for picking the right workflow, wiring up the caller file, setting secrets securely, and bootstrapping AWS OIDC:
/plugin marketplace add KotaHusky/cicd-toolkit
/plugin install cicd-toolkit@cicd-toolkit
| Skill | What it does |
|---|---|
/integrate-cicd-toolkit |
Picks the right workflow, adapts a caller from examples/, and walks through secrets setup — including generating a CLAUDE_CODE_OAUTH_TOKEN via claude setup-token and storing any secret with a clipboard pipe (pbpaste | gh secret set …) run in your own terminal, outside the Claude session, so values never enter the AI conversation. |
/bootstrap-oidc |
One-time GitHub → AWS OIDC provisioning via bin/bootstrap.ts (provider + deploy role), producing the AWS_DEPLOY_ROLE_ARN secret used by the AWS deploy workflows. |
Every PR to this repo is automatically reviewed by claude-review-self.yml, which dogfoods the reusable claude-review.yml documented in Workflows — inline comments for line-specific findings plus one sticky summary, with a prompt tuned for reusable-workflow risks (shell pitfalls, breaking input changes, README/examples//skills drift). Bot PRs are skipped. Credentials: the CLAUDE_CODE_OAUTH_TOKEN repo secret — run claude setup-token in a terminal, copy the token, then pbpaste | gh secret set CLAUDE_CODE_OAUTH_TOKEN -R KotaHusky/cicd-toolkit (all outside any Claude session).