Skip to content

Repository files navigation

cicd-toolkit

Build Verify Commitlint Docker GHCR CDK Deploy Static S3 AI Release License: MIT

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.

Capabilities

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.ymlrelease.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

Table of Contents

Workflows

Build Verification

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

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/cli and a commitlint config (e.g. @commitlint/config-conventional) in package.json.

Docker Build & Push to GHCR

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

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 Site Deploy (S3 + CloudFront)

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 Environments

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.

Azure Container Apps

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

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

Environments, Promotion & Rollback

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.

AI-Powered Release

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.

Automatic Versioning

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.yml call inside auto-version.yml is fixed at @main (GitHub can't parameterize uses:), so pinning auto-version.yml to a tag or SHA does not transitively pin the release pipeline. If you need a fully pinned release path, call release.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.

End-User What's-New Summaries

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:

  1. Curated context — the generator sees only the commit subjects plus .github/whats-new-context.md in your repo (copy examples/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.
  2. 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.
  3. Redaction judge — a second Claude pass reviews the draft against the context file and rewrites anything that reveals internals.
  4. 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.json

Rendering 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 Code Review

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

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.

Composite actions

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 step
  • ecr-mirror — mirror a GHCR image (by digest) into ECR for ECS consumption
  • cfn-recover — unstick CloudFormation stacks in ROLLBACK_COMPLETE/FAILED states before a deploy

Setup

Secrets

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.

Pinning & Versions

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.

OIDC Bootstrap (CDK Deploy)

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.

CDK constructs

Reusable, project-agnostic constructs in lib/. Import them into your own CDK app.

StaticSiteStack (S3 + CloudFront, optional ACM + Route 53)

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
});

applyTags(scope, tags)

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.

StaticSiteDashboard

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',
});

SharedEdgeStack (account-level CloudFront primitives)

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.

EcsExpressEdgeStack (CloudFront edge — origin-agnostic)

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.

OidcBootstrapStack (GitHub → AWS OIDC provider + deploy roles)

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.

EcsExpressDashboard / ecs-express-observability

CloudWatch dashboard (ALB + ECS service metrics) and the tiered alarm set used by EcsExpressEdgeStack's observability prop — usable standalone for existing services.

InviteGating (Cognito invite-code gating) — packages/invite-gating

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.

Examples

See examples/ for ready-to-copy workflow files:

  • aca.yml — Azure Container Apps: Bicep provision + image deploy via Azure OIDC
  • auto-version.yml — Automatic versioning + AI release on every merge to main
  • cdk-deploy.yml — CDK synth check on PRs, OIDC deploy on merge to main
  • ci-doctor.yml — AI diagnosis issue when default-branch CI goes red; auto-closes on recovery
  • ci.yml — Build verification + Docker push + commitlint
  • claude-review.yml — Claude PR review (inline comments + sticky summary)
  • cloudflare-dns.yml — Upsert a Cloudflare DNS record, optional cache purge
  • docker-ghcr.yml — Build a Docker image and push it to GHCR
  • rollback.yml — One-click redeploy of a previous release tag via workflow_dispatch
  • preview-env.yml — Per-PR static-site preview deploys with auto-teardown
  • release.yml — AI-powered release on tag push
  • static-site.yml — Tag-driven release for an S3+CloudFront static site
  • whats-new-context.md — Living context doc powering the end-user what's-new summaries

Claude Code plugin

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.

Claude PR review

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).

License

MIT

About

Reusable GitHub Actions workflows, composite actions, and AWS CDK constructs for CI/CD — plus a Claude Code plugin marketplace for AI-assisted integration.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages