Install
$ agentstack add skill-san-npm-skills-ws-cicd-pipelines ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
CI/CD Pipelines
Concrete, runnable patterns for production GitHub Actions pipelines. Every snippet below is self-contained — copy it, swap the placeholders, and ship. Action versions are current as of June 2026; pin by SHA in regulated/high-trust repos (see [Supply-Chain Baseline](#supply-chain-baseline-2026)). For sibling depth on container internals see docker-production; for cloud IAM specifics see aws-production-deploy.
Action Version Matrix (June 2026)
Pin to these majors (or the exact SHA for the major). All listed majors run on the Node 24 runtime — GitHub forced the Actions runtime default to Node 24 on 2026-06-02, so any action still on Node 20 will warn/fail; bump them.
| Action | Pin | Notes | |---|---|---| | actions/checkout | @v6 | v6 changed git config injection; bare git commands still work via includeIf. | | actions/setup-node | @v6 | | | actions/cache | @v5 | | | actions/upload-artifact | @v7 | v7 adds non-zip uploads (compression-level/archive). Not symmetric with download. | | actions/download-artifact | @v8 | Pairs with upload v7; major numbers differ — don't assume they match. | | actions/attest-build-provenance | @v4 | Now a thin wrapper over actions/attest@v4. | | docker/build-push-action | @v7 | v7 = Node 24 default; requires runner ≥ v2.327.1 (GitHub-hosted is fine). | | docker/setup-buildx-action | @v4 | | | docker/login-action | @v4 | | | docker/metadata-action | @v6 | | | aws-actions/configure-aws-credentials | @v6 | | | actions/dependency-review-action | @v5 | | | github/codeql-action | @v3 | v4 exists but v3 is the stable default line; check the docs before jumping. | | codecov/codecov-action | @v7 | Requires CODECOV_TOKEN for public repos since v4. | | sigstore/cosign-installer | @v4 | | | anchore/sbom-action | @v0.24 | 0.x — pin the exact minor, no stable major yet. | | aquasecurity/trivy-action | @v0.36 | 0.x — pin the exact minor. | | step-security/harden-runner | @v2 | Egress filtering / runtime monitoring. | | changesets/action | @v1 (v1.9+) | |
> These move fast. The durable source of truth is each action's releases page; verify before pinning a SHA for production.
GitHub Actions — Core CI Workflow
Set least-privilege permissions at the top level (contents: read) so every job defaults to read-only; grant writes only on the specific job that needs them. This is the single highest-leverage hardening step — a compromised dependency in a test job then cannot push code or mint releases.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Least-privilege default for ALL jobs. Override per-job when a job needs more.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
# Pin the runner image. `ubuntu-latest` silently migrates (e.g. 24.04 -> 26.04)
# and can break builds mid-sprint. Pin the version; bump it deliberately.
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
# Node 20 reached EOL 2026-04-30 — dropped. 22 = maintenance LTS (until 2027-04),
# 24 = active LTS. Only matrix versions you actually support in production.
node: [22, 24]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test -- --coverage
- uses: actions/upload-artifact@v7
with:
name: coverage-${{ matrix.node }}
path: coverage/
retention-days: 7
# Split lint/typecheck into their own job so they run in parallel with tests,
# not as sequential steps that serialize the critical path.
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with: { node-version: 24, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm run typecheck
Required status checks & branch protection
CI only protects you if merges are blocked on it. Configure once (Settings → Branches → branch protection rule, or via API/Terraform):
- Require pull request before merging; require ≥1 approval; dismiss stale approvals on new commits.
- Require status checks to pass: add the exact job names (
test (22),test (24),lint). Matrix jobs register as separate checks — list each, or gate them behind one aggregator job. - Require branches to be up to date before merging (forces re-run against latest
main). - Require signed commits and a linear history on release branches if your compliance posture needs it.
# Aggregator pattern: make ONE check required instead of N flaky matrix entries.
ci-passed:
runs-on: ubuntu-24.04
needs: [test, lint]
if: always()
steps:
- name: Fail if any dependency failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
Caching Strategies
# 1) Node modules — let setup-node manage it (keyed on lockfile hash automatically).
- uses: actions/setup-node@v6
with: { node-version: 24, cache: npm } # use 'pnpm' or 'yarn' to match your PM
# 2) pnpm (needs the store + action-setup BEFORE setup-node's cache kicks in)
- uses: pnpm/action-setup@v6
with: { version: 9 }
- uses: actions/setup-node@v6
with: { node-version: 24, cache: pnpm }
# 3) Docker layer caching via the GitHub Actions cache backend
- uses: docker/build-push-action@v7
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max # mode=max also caches intermediate layers
# 4) Turborepo local cache (remote cache is better at scale — see monorepo section)
- uses: actions/cache@v5
with:
path: .turbo
# Include the lockfile in the key so a dep change busts the cache.
key: turbo-${{ hashFiles('**/turbo.json', '**/package-lock.json') }}-${{ github.sha }}
restore-keys: |
turbo-${{ hashFiles('**/turbo.json', '**/package-lock.json') }}-
- run: npx turbo build --cache-dir=.turbo
Cache hygiene: key on the lockfile hash (not loose globs), keep restore-keys as a prefix fallback, and never cache anything secret-derived. Untrusted PRs run in a restricted scope and cannot write to caches/branches your default branch created — don't design a workflow that depends on a PR populating a shared cache.
Secrets & OIDC
Prefer OIDC over long-lived static cloud keys: the workflow mints a short-lived token at runtime, so there is no secret to leak or rotate. id-token: write is required for OIDC and must be granted explicitly (it is not in the contents: read default).
# Repository / org secrets (Settings -> Secrets and variables -> Actions)
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
jobs:
# Environment-scoped secrets + manual gate. Secrets here are isolated from CI jobs.
deploy:
environment: production # add "Required reviewers" + secrets on this environment
permissions:
id-token: write # mint the OIDC token
contents: read
steps:
# OIDC — no stored cloud keys. Configure the trust policy on the cloud side
# to only accept tokens from THIS repo + ref (and ideally THIS environment).
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy
aws-region: us-east-1
- run: ./deploy.sh
Trust-policy scoping (do this — a wildcard repo:* subject is a takeover risk):
// AWS IAM trust policy condition — bind to exactly your repo, ref, and environment
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production"
}
}
Secret rules:
- Never
echo/printa secret; GitHub masks known values but interpolation can defeat masking. - Never pass secrets into actions triggered by
pull_requestfrom forks (usepull_request_targetonly with extreme care — it runs with write scope against untrusted code). - Prefer the auto-provisioned
GITHUB_TOKEN(scoped, short-lived) over a PAT. If you need a PAT, use a fine-grained token with minimal repo/permission scope and a short expiry. - Rotate any unavoidable static credential on a schedule and alert on use from unexpected IPs (
step-security/harden-runner@v2can enforce egress allowlists).
Docker Multi-Stage Build
The classic footgun: npm ci in the build stage (dev deps included for the build), then copying node_modules straight into the runtime image — shipping dev dependencies, tooling, and a larger attack surface to production. Fix it with a dedicated deps stage that installs production-only, and copy that into runtime.
# syntax=docker/dockerfile:1
# --- deps: production-only dependencies for the runtime image ---
FROM node:24-alpine AS deps
WORKDIR /app
COPY package*.json ./
# BuildKit cache mount keeps the npm cache warm across builds without baking it in.
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
# --- build: full deps (incl. dev) just to compile, never shipped ---
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
# --- runtime: minimal, non-root, prod deps only ---
FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 app && adduser -u 1001 -G app -s /bin/sh -D app
COPY --from=deps /app/node_modules ./node_modules # PROD deps only
COPY --from=build /app/dist ./dist
COPY package.json ./
USER app
EXPOSE 3000
# Container-level liveness; pair with your orchestrator's probes.
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "dist/index.js"]
Build, scan, sign, and attest in CI
build-image:
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write # push to GHCR
id-token: write # keyless cosign signing + attestations
attestations: write # actions/attest-build-provenance
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=semver,pattern={{version}}
- id: build
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Vulnerability scan — fail the build on fixable HIGH/CRITICAL CVEs.
- uses: aquasecurity/trivy-action@v0.36
with:
image-ref: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: sarif
output: trivy.sarif
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: '1'
# Keyless image signing (Sigstore/Fulcio — no key to manage).
- uses: sigstore/cosign-installer@v4
- run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
# Build provenance attestation (SLSA) bound to the pushed digest.
- uses: actions/attest-build-provenance@v4
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Always reference images by digest (@sha256:...), never a mutable tag, downstream — that is what cosign signs and what provenance attests. Verify at deploy time: cosign verify --certificate-identity-regexp '...' --certificate-oidc-issuer https://token.actions.githubusercontent.com IMAGE@DIGEST.
Deployment Strategies
| Strategy | Downtime | Rollback Speed | Risk | Best For | |---|---|---|---|---| | Rolling | Zero | Minutes | Medium | Stateless services | | Blue-Green | Zero | Instant (swap) | Low | Critical services | | Canary | Zero | Fast (shift back) | Lowest | High-traffic APIs | | Recreate | Yes | Slow | High | Dev/staging only |
Blue-Green with GitHub Actions
deploy:
runs-on: ubuntu-24.04
environment: production
permissions: { id-token: write, contents: read }
steps:
- uses: actions/checkout@v6
- name: Deploy to idle (green) slot
run: ./deploy.sh green
- name: Health check green before any traffic
run: |
for i in $(seq 1 30); do
curl -fsS https://green.app.example/health && exit 0
sleep 5
done
echo "green never became healthy"; exit 1
- name: Swap traffic to green
run: ./swap-traffic.sh green
- name: Keep blue warm as instant rollback
run: echo "Rollback = ./swap-traffic.sh blue (previous version still running)"
Canary (progressive traffic shift)
canary:
runs-on: ubuntu-24.04
environment: production
steps:
- uses: actions/checkout@v6
- run: ./deploy.sh canary
- name: Shift 5% → watch SLOs → 25% → 50% → 100%
run: |
for pct in 5 25 50 100; do
./set-weight.sh canary "$pct"
sleep 120
# Bail (and auto-rollback) if error rate / latency SLO breaches.
./check-slo.sh canary || { ./set-weight.sh canary 0; exit 1; }
done
Environment Promotion (dev → staging → prod)
# Trigger chain: push to main → dev → staging (auto) → prod (manual approval)
deploy-dev:
if: github.ref == 'refs/heads/main'
environment: dev
permissions: { id-token: write, contents: read }
deploy-staging:
needs: deploy-dev
environment: staging
permissions: { id-token: write, contents: read }
deploy-prod:
needs: deploy-staging
environment: production # set "Required reviewers" + a wait timer on this environment
permissions: { id-token: write, contents: read }
Release Automation
Option A — semantic-release (single package, automated versioning from commits)
semantic-release reads Conventional Commits, computes the next version, publishes to npm, creates the GitHub release, and commits the changelog — all in CI on main. The common failure is a release job missing Node setup, a clean install, or the npm auth token, so it either can't run or publishes unauthenticated.
// .releaserc.json
{
"branches": ["main", { "name": "next", "prerelease": true }],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
["@semantic-release/git", { "assets": ["CHANGELOG.md", "package.json"] }]
]
}
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: read # least-privilege default; the release job widens it below
jobs:
release:
runs-on: ubuntu-24.04
permissions:
contents: write # push the changelog/version commit + create the GitHub release
issues: write # comment on released issues
pull-requests: write # comment on released PRs
id-token: write # npm provenance (publish with verifiable origin)
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # full history — semantic-release diffs all tags
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [san-npm](https://github.com/san-npm)
- **Source:** [san-npm/skills-ws](https://github.com/san-npm/skills-ws)
- **License:** MIT
- **Homepage:** https://skills-ws.vercel.app
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.