@slamb2k/mad-skills 2.0.54 → 2.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mad-skills",
3
3
  "description": "AI-assisted planning, development and governance tools",
4
- "version": "2.0.54",
4
+ "version": "2.0.56",
5
5
  "author": {
6
6
  "name": "slamb2k",
7
7
  "url": "https://github.com/slamb2k"
@@ -2,28 +2,61 @@
2
2
 
3
3
  const config = require('./config.cjs');
4
4
 
5
+ // Unicode banner (Delta Corps Priest 1) — used when the terminal supports UTF-8.
5
6
  // prettier-ignore
6
- const BANNER_LINES = [
7
- '::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::',
8
- ":::::'##::::'##::::'###::::'########::::::'######::'##:::'##:'####:'##:::::::'##::::::::'######::::::::",
9
- "::::: ###::'###:::'## ##::: ##.... ##::::'##... ##: ##::'##::. ##:: ##::::::: ##:::::::'##... ##:::::::",
10
- "::::: ####'####::'##:. ##:: ##:::: ##:::: ##:::..:: ##:'##:::: ##:: ##::::::: ##::::::: ##:::..::::::::",
11
- "::::: ## ### ##:'##:::. ##: ##:::: ##::::. ######:: #####::::: ##:: ##::::::: ##:::::::. ######::::::::",
12
- "::::: ##. #: ##: #########: ##:::: ##:::::..... ##: ##. ##:::: ##:: ##::::::: ##::::::::..... ##:::::::",
13
- "::::: ##:.:: ##: ##.... ##: ##:::: ##::::'##::: ##: ##:. ##::: ##:: ##::::::: ##:::::::'##::: ##:::::::",
14
- "::::: ##:::: ##: ##:::: ##: ########:::::. ######:: ##::. ##:'####: ########: ########:. ######::::::::",
15
- "::::::..:::::..::..:::::..::........:::::::......:::..::::..::....::........::........:::......:::::::::",
7
+ const BANNER_UNICODE = [
8
+ " ▄▄▄▄███▄▄▄▄ ▄████████ ████████▄",
9
+ "▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ▀███",
10
+ "███ ███ ███ ███ ███ ███ ███",
11
+ "███ ███ ███ ███ ███ ███ ███",
12
+ "███ ███ ███ ▀███████████ ███ ███",
13
+ "███ ███ ███ ███ ███ ███ ███",
14
+ "███ ███ ███ ███ ███ ███ ▄███",
15
+ " ▀█ ███ █▀ ███ █▀ ████████▀",
16
+ "",
17
+ " ▄████████ ▄█ ▄█▄ ▄█ ▄█ ▄█ ▄████████",
18
+ " ███ ███ ███ ▄███▀ ███ ███ ███ ███ ███",
19
+ " ███ █▀ ███▐██▀ ███▌ ███ ███ ███ █▀",
20
+ " ███ ▄█████▀ ███▌ ███ ███ ███",
21
+ "▀███████████ ▀▀█████▄ ███▌ ███ ███ ▀███████████",
22
+ " ███ ███▐██▄ ███ ███ ███ ███",
23
+ " ▄█ ███ ███ ▀███▄ ███ ███▌ ▄ ███▌ ▄ ▄█ ███",
24
+ " ▄████████▀ ███ ▀█▀ █▀ █████▄▄██ █████▄▄██ ▄████████▀",
25
+ " ▀ ▀ ▀",
16
26
  ];
17
27
 
18
- const SEPARATOR = '\u2500'.repeat(70);
28
+ // ASCII fallback (Rowan Cap) — used when the terminal locale is not UTF-8.
29
+ // prettier-ignore
30
+ const BANNER_ASCII = [
31
+ " dMMMMMMMMb .aMMMb dMMMMb",
32
+ " dMP\"dMP\"dMP dMP\"dMP dMP VMP",
33
+ " dMP dMP dMP dMMMMMP dMP dMP",
34
+ " dMP dMP dMP dMP dMP dMP.aMP",
35
+ "dMP dMP dMP dMP dMP dMMMMP\"",
36
+ "",
37
+ " .dMMMb dMP dMP dMP dMP dMP .dMMMb",
38
+ " dMP\" VP dMP.dMP amr dMP dMP dMP\" VP",
39
+ " VMMMb dMMMMK\" dMP dMP dMP VMMMb",
40
+ "dP .dMP dMP\"AMF dMP dMP dMP dP .dMP",
41
+ "VMMMP\" dMP dMP dMP dMMMMMP dMMMMMP VMMMP\"",
42
+ ];
43
+
44
+ // True when the terminal locale advertises UTF-8, so box-drawing glyphs render.
45
+ function supportsUnicode() {
46
+ const locale = process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || '';
47
+ return /utf-?8/i.test(locale);
48
+ }
19
49
 
20
50
  function getBanner() {
51
+ const unicode = supportsUnicode();
52
+ const lines = unicode ? BANNER_UNICODE : BANNER_ASCII;
53
+ const separator = (unicode ? '─' : '-').repeat(70);
21
54
  return [
22
- ...BANNER_LINES,
23
- SEPARATOR,
55
+ ...lines,
56
+ separator,
24
57
  ` Session Guard v${config.version}`,
25
- SEPARATOR,
58
+ separator,
26
59
  ].join('\n');
27
60
  }
28
61
 
29
- module.exports = { getBanner, BANNER_MARKER: '::::::::::::::::::::' };
62
+ module.exports = { getBanner, supportsUnicode, BANNER_MARKER: 'Session Guard' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slamb2k/mad-skills",
3
- "version": "2.0.54",
3
+ "version": "2.0.56",
4
4
  "description": "Claude Code skills collection — full lifecycle development tools",
5
5
  "type": "module",
6
6
  "repository": {
@@ -280,6 +280,14 @@ with development overrides (volume mounts, hot reload, debug ports).
280
280
  Detect CI system from Phase 1 (GitHub Actions / Azure Pipelines / GitLab CI).
281
281
  Read the appropriate template from `references/pipeline-templates.md`.
282
282
 
283
+ For GitHub Actions and Azure Pipelines, the templates are **hardened by default**
284
+ — read `references/hardening.md` for the OIDC auth, keyless cosign signing,
285
+ idempotency guards, and digest-pinned promotion these templates implement. The
286
+ generated workflow MUST include `id-token: write`, `provenance: true`, a cosign
287
+ `sign` in the build job and `verify` in every promotion job, an existence guard
288
+ before build, and deploy steps that reference the image by `@sha256:` digest.
289
+ (GitLab CI remains credential-based.)
290
+
283
291
  Generate a workflow that implements:
284
292
 
285
293
  **On pull request:**
@@ -349,6 +357,14 @@ If the detected framework doesn't already have a health endpoint, add a comment
349
357
  in the generated workflow noting that a `/healthz` or `/health` endpoint is
350
358
  recommended for deployment readiness probes.
351
359
 
360
+ ### 3.7 — Deployment Setup Guide (deploy/SETUP.md)
361
+
362
+ Because the pipelines are secure by default, first-run requires one-time
363
+ cloud-side trust. Generate `deploy/SETUP.md` from the **Generated deploy/SETUP.md**
364
+ section of `references/hardening.md`, substituting the target repo (`OWNER/REPO`),
365
+ chosen registry, and cloud identifiers. This file lists the federated-credential
366
+ / IAM-trust steps and the cosign verification command.
367
+
352
368
  ---
353
369
 
354
370
  ## Phase 4: Verify
@@ -366,6 +382,10 @@ Also validate generated workflow files:
366
382
  - GitHub Actions: check YAML syntax
367
383
  - Azure Pipelines: check YAML syntax
368
384
  - GitLab CI: check YAML syntax
385
+ - For the GitHub workflow, confirm it sets `id-token: write`; for Azure Pipelines,
386
+ confirm it authenticates via a Workload Identity Federation service connection
387
+ (`azureSubscription`). Both must contain a cosign `sign`+`verify` pair and
388
+ reference deploy images by `@sha256:` digest. Flag any absence as a generation error.
369
389
 
370
390
  Present the user with a summary of all generated files before writing.
371
391
 
@@ -396,6 +416,11 @@ After all files are generated and verified, present:
396
416
  │ Merge → build + test → push → deploy dev → smoke
397
417
  │ Tag → retag (no rebuild) → staging → e2e → prod
398
418
 
419
+ │ 🔐 Required cloud setup
420
+ │ • Configure federated identity (see deploy/SETUP.md)
421
+ │ • {registry-specific: IAM role / ACR federated cred / GCP WIP}
422
+ │ • First run fails until trust is configured — this is expected
423
+
399
424
  │ 🔗 Links
400
425
  │ Dockerfile: {path}
401
426
  │ Workflow: {path}
@@ -0,0 +1,104 @@
1
+ # Pipeline Hardening Reference
2
+
3
+ How `/dock` hardens the pipelines it generates. These patterns mirror
4
+ mad-skills' own release workflow, translated from npm publishing to OCI
5
+ containers. Hardened is the only mode for GitHub Actions and Azure Pipelines.
6
+
7
+ ## OIDC Registry Auth
8
+
9
+ Never store long-lived cloud registry credentials in CI. Authenticate with the
10
+ CI provider's OIDC identity, federated to a cloud role/identity.
11
+
12
+ - **GHCR** — keep the ephemeral `GITHUB_TOKEN` (already short-lived, OIDC-backed).
13
+ Requires `permissions: packages: write`.
14
+ - **AWS ECR** — `aws-actions/configure-aws-credentials@v4` with `role-to-assume`
15
+ (an IAM role trusting the GitHub OIDC provider). No access keys.
16
+ - **Azure ACR** — `azure/login@v2` with Workload Identity Federation
17
+ (`client-id` + `tenant-id` + `subscription-id`, federated credential scoped to
18
+ the repo). No client secret. Then `az acr login`.
19
+ - **Google Artifact Registry** — `google-github-actions/auth@v2` with a Workload
20
+ Identity Provider + service account. No SA key JSON.
21
+
22
+ All jobs that need OIDC set `permissions: id-token: write` (plus
23
+ `contents: read`). Azure Pipelines uses a **Workload Identity Federation service
24
+ connection** (`azureSubscription:`) instead of a stored password.
25
+
26
+ ## Signing & Provenance
27
+
28
+ - Build with `provenance: true` and `sbom: true` (buildx emits SLSA provenance +
29
+ SBOM attestations attached to the image).
30
+ - Sign the image **by digest** with keyless cosign (Fulcio certificate + Rekor
31
+ transparency log, authorized by the `id-token: write` OIDC token — no signing
32
+ key in secrets).
33
+ - Every promotion job runs `cosign verify` against the digest, asserting the
34
+ certificate identity matches the workflow's OIDC issuer and
35
+ `repo:OWNER/REPO`. Verification failure blocks promotion.
36
+
37
+ > **Private-infra note:** keyless cosign writes to the public Rekor log. Orgs
38
+ > that cannot use public transparency infra can switch to a key pair
39
+ > (`cosign generate-key-pair`, private key in a CI secret,
40
+ > `cosign sign --key`). This is a documented fallback — dock does not generate it.
41
+
42
+ ## Idempotency Guards
43
+
44
+ - Before build, guard with `crane manifest <ref>` (fallback:
45
+ `docker manifest inspect`). If the artifact already exists, skip build+push and
46
+ reuse the existing digest — the container analog of mad-skills' `npm view`
47
+ publish-guard.
48
+ - Promote/retag steps are check-then-act and tolerate an already-promoted digest
49
+ ("recovery mode") so a re-run exits cleanly instead of failing.
50
+ - A `concurrency:` group keyed on workflow + ref (`cancel-in-progress`) prevents
51
+ overlapping release runs from racing.
52
+
53
+ ## Digest-Pinned Promotion
54
+
55
+ - The build job outputs `IMAGE@sha256:<digest>` as a job output.
56
+ - Dev, staging, and prod deploy by that **digest**, never a mutable tag. Tags
57
+ like `latest` / `vX.Y.Z` may be attached for humans, but deploys resolve the
58
+ digest.
59
+ - `deploy/environments.yml` records the deployed digest so the matrix captures
60
+ exactly what shipped.
61
+
62
+ ## Generated deploy/SETUP.md
63
+
64
+ `/dock` writes `deploy/SETUP.md` into the target repo, tailored to the chosen
65
+ registry. Template body (substitute `OWNER/REPO`, registry, cloud identifiers):
66
+
67
+ ```markdown
68
+ # Deployment Setup — Required Before First Run
69
+
70
+ This pipeline uses keyless OIDC and image signing. Complete the cloud-side
71
+ trust below once; no long-lived credentials are stored in CI.
72
+
73
+ ## 1. Federated identity (pick your registry)
74
+
75
+ ### AWS ECR
76
+ - Create an IAM role trusting the GitHub OIDC provider
77
+ (`token.actions.githubusercontent.com`), condition
78
+ `token.actions.githubusercontent.com:sub = repo:OWNER/REPO:ref:refs/heads/main`
79
+ (add tag refs for releases).
80
+ - Attach a policy allowing `ecr:*` push to your repository.
81
+ - Put the role ARN in the workflow `role-to-assume`.
82
+
83
+ ### Azure ACR
84
+ - Register an app / user-assigned managed identity.
85
+ - Add a **federated credential** scoped to `repo:OWNER/REPO` (branch `main` and
86
+ tag refs), subject `repo:OWNER/REPO:ref:refs/heads/main`.
87
+ - Grant it `AcrPush` on the registry.
88
+ - Set `client-id` / `tenant-id` / `subscription-id` in the workflow.
89
+
90
+ ### Google Artifact Registry
91
+ - Create a Workload Identity Pool + provider for GitHub.
92
+ - Bind a service account with `artifactregistry.writer`, allowing
93
+ `principalSet://…/attribute.repository/OWNER/REPO`.
94
+ - Set `workload_identity_provider` + `service_account` in the workflow.
95
+
96
+ ## 2. Signing
97
+
98
+ Keyless cosign needs no setup — it uses the workflow OIDC token and the public
99
+ Rekor log. To verify locally:
100
+ `cosign verify --certificate-identity-regexp 'https://github.com/OWNER/REPO/.*' --certificate-oidc-issuer https://token.actions.githubusercontent.com REGISTRY/IMAGE@sha256:...`
101
+
102
+ If public transparency logging is not allowed, switch to a key pair (see the
103
+ Signing & Provenance note in the dock hardening reference).
104
+ ```
@@ -19,12 +19,14 @@ Template variables:
19
19
 
20
20
  ## GitHub Actions
21
21
 
22
- ### Build & Deploy Workflow
22
+ ### Build & Deploy Workflow (hardened)
23
23
 
24
- File: `.github/workflows/deploy.yml`
24
+ Placeholders: `{REGISTRY}` (e.g. `ghcr.io/OWNER/REPO`), `{IMAGE_NAME}`.
25
+ GHCR uses the ephemeral `GITHUB_TOKEN`; cloud registries use OIDC (see the
26
+ commented block). Deployment is by digest and gated on signature verification.
25
27
 
26
28
  ```yaml
27
- name: Build & Deploy
29
+ name: Release
28
30
 
29
31
  on:
30
32
  pull_request:
@@ -33,8 +35,8 @@ on:
33
35
  tags: ["v*"]
34
36
 
35
37
  concurrency:
36
- group: deploy-${{ github.ref }}
37
- cancel-in-progress: ${{ github.event_name == 'pull_request' }}
38
+ group: ${{ github.workflow }}-${{ github.ref }}
39
+ cancel-in-progress: true
38
40
 
39
41
  env:
40
42
  REGISTRY: {REGISTRY}
@@ -42,155 +44,112 @@ env:
42
44
 
43
45
  permissions:
44
46
  contents: read
45
- packages: write
47
+ packages: write # GHCR push
48
+ id-token: write # OIDC: cloud login + keyless cosign
46
49
 
47
50
  jobs:
48
- # ── Build & Test ──────────────────────────────────────────────
49
51
  build:
50
52
  runs-on: ubuntu-latest
51
- # Skip for release tags — we retag, not rebuild
52
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
53
53
  outputs:
54
- image_tag: ${{ steps.meta.outputs.tags }}
55
- sha_short: ${{ steps.vars.outputs.sha_short }}
54
+ image: ${{ steps.digest.outputs.ref }} # REGISTRY/IMAGE@sha256:...
56
55
  steps:
57
56
  - uses: actions/checkout@v4
58
57
 
59
- - name: Set variables
60
- id: vars
61
- run: echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
62
-
63
- - name: Docker meta
64
- id: meta
65
- uses: docker/metadata-action@v5
66
- with:
67
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
68
- tags: |
69
- type=sha,prefix=
70
- type=ref,event=pr,prefix=pr-
71
- type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', '{DEFAULT_BRANCH}') }}
72
-
73
58
  - uses: docker/setup-buildx-action@v3
74
59
 
75
- - name: Login to registry
60
+ - uses: imjasonh/setup-crane@v0.4
61
+
62
+ # Idempotency guard: skip build if this commit's image already exists.
63
+ - name: Check existing image
64
+ id: guard
65
+ run: |
66
+ TAG="${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}"
67
+ if crane manifest "$TAG" >/dev/null 2>&1; then
68
+ echo "exists=true" >> "$GITHUB_OUTPUT"
69
+ echo "::notice::$TAG already present — reusing digest."
70
+ else
71
+ echo "exists=false" >> "$GITHUB_OUTPUT"
72
+ fi
73
+
74
+ # GHCR login (ephemeral token). For cloud registries, replace with OIDC:
75
+ # AWS ECR: aws-actions/configure-aws-credentials@v4 (role-to-assume) + aws ecr get-login-password
76
+ # Azure ACR: azure/login@v2 (WIF client-id/tenant-id/subscription-id) + az acr login
77
+ # GAR: google-github-actions/auth@v2 (workload_identity_provider + service_account) + gcloud auth configure-docker
78
+ - name: Login to GHCR
79
+ if: steps.guard.outputs.exists == 'false' && github.event_name == 'push'
76
80
  uses: docker/login-action@v3
77
81
  with:
78
82
  registry: ${{ env.REGISTRY }}
79
83
  username: ${{ github.actor }}
80
84
  password: ${{ secrets.GITHUB_TOKEN }}
81
85
 
82
- - name: Build and test
83
- uses: docker/build-push-action@v6
84
- with:
85
- context: .
86
- target: test
87
- cache-from: type=gha
88
- cache-to: type=gha,mode=max
89
-
90
- - name: Build and push production image
91
- if: github.event_name == 'push'
86
+ - name: Build and push
87
+ id: build
88
+ if: steps.guard.outputs.exists == 'false'
92
89
  uses: docker/build-push-action@v6
93
90
  with:
94
91
  context: .
95
92
  target: production
96
- push: true
97
- tags: ${{ steps.meta.outputs.tags }}
98
- labels: ${{ steps.meta.outputs.labels }}
99
- cache-from: type=gha
100
- cache-to: type=gha,mode=max
93
+ push: ${{ github.event_name == 'push' }}
94
+ tags: |
95
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
96
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
97
+ provenance: true
98
+ sbom: true
101
99
 
102
- # ── Deploy to Dev ─────────────────────────────────────────────
103
- deploy-dev:
104
- needs: build
105
- if: github.ref == 'refs/heads/{DEFAULT_BRANCH}'
106
- runs-on: ubuntu-latest
107
- environment: dev
108
- steps:
109
- - uses: actions/checkout@v4
100
+ - uses: sigstore/cosign-installer@v3
110
101
 
111
- - name: Deploy to dev
112
- run: |
113
- # {DEV_DEPLOY_COMMANDS}
114
- echo "Deploying ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.sha_short }} to dev"
102
+ - name: Sign image (keyless)
103
+ if: steps.guard.outputs.exists == 'false' && github.event_name == 'push'
104
+ run: cosign sign --yes "${REGISTRY}/${IMAGE_NAME}@${{ steps.build.outputs.digest }}"
115
105
 
116
- - name: Smoke test
106
+ - name: Resolve digest reference
107
+ id: digest
117
108
  run: |
118
- # Poll the health endpoint until ready (bounded deadline so a
119
- # genuinely broken deploy still fails fast). Do not use a blind
120
- # `sleep N && curl` — it flakes on slow cold starts and wastes
121
- # time on fast ones.
122
- deadline=$((SECONDS + 120))
123
- until curl -fs "$DEV_URL/healthz" >/dev/null; do
124
- if [ $SECONDS -ge $deadline ]; then
125
- echo "Health check failed to pass within 120s" >&2
126
- exit 1
127
- fi
128
- sleep 2
129
- done
130
-
131
- # ── Promote to Staging (on release tag) ───────────────────────
132
- promote-staging:
133
- if: startsWith(github.ref, 'refs/tags/v')
109
+ if [ "${{ steps.guard.outputs.exists }}" = "true" ]; then
110
+ DIG=$(crane digest "${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}")
111
+ else
112
+ DIG="${{ steps.build.outputs.digest }}"
113
+ fi
114
+ echo "ref=${REGISTRY}/${IMAGE_NAME}@${DIG}" >> "$GITHUB_OUTPUT"
115
+
116
+ deploy-dev:
117
+ needs: build
118
+ if: github.event_name == 'push' && github.ref == 'refs/heads/{DEFAULT_BRANCH}'
134
119
  runs-on: ubuntu-latest
135
- environment: staging
136
- outputs:
137
- version: ${{ steps.version.outputs.tag }}
120
+ permissions:
121
+ contents: read
122
+ id-token: write
138
123
  steps:
139
- - uses: actions/checkout@v4
140
-
141
- - name: Extract version
142
- id: version
143
- run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
144
-
145
- - name: Login to registry
146
- uses: docker/login-action@v3
147
- with:
148
- registry: ${{ env.REGISTRY }}
149
- username: ${{ github.actor }}
150
- password: ${{ secrets.GITHUB_TOKEN }}
151
-
152
- - name: Retag image (no rebuild)
124
+ - uses: sigstore/cosign-installer@v3
125
+ - name: Verify signature before deploy
153
126
  run: |
154
- SHA=$(git rev-parse --short HEAD)
155
- docker buildx imagetools create \
156
- --tag "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }}" \
157
- "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${SHA}"
158
-
159
- - name: Deploy to staging
160
- run: |
161
- # {STAGING_DEPLOY_COMMANDS}
162
- echo "Deploying ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }} to staging"
163
-
164
- - name: Run staging tests
165
- run: |
166
- # Integration and e2e tests against staging
167
- echo "Running staging gate tests..."
168
-
169
- # ── Promote to Production ─────────────────────────────────────
170
- promote-prod:
171
- needs: promote-staging
127
+ cosign verify \
128
+ --certificate-identity-regexp "https://github.com/${{ github.repository }}/.*" \
129
+ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
130
+ "${{ needs.build.outputs.image }}"
131
+ - name: Deploy to dev (by digest — never rebuild)
132
+ run: echo "Deploy ${{ needs.build.outputs.image }} to dev"
133
+
134
+ promote:
135
+ needs: build
172
136
  if: startsWith(github.ref, 'refs/tags/v')
173
137
  runs-on: ubuntu-latest
174
- environment: production
138
+ permissions:
139
+ contents: read
140
+ id-token: write
175
141
  steps:
176
- - uses: actions/checkout@v4
177
-
178
- - name: Deploy to production
179
- run: |
180
- # {PROD_DEPLOY_COMMANDS}
181
- echo "Deploying ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.promote-staging.outputs.version }} to production"
182
-
183
- - name: Post-deploy smoke test
142
+ - uses: sigstore/cosign-installer@v3
143
+ - name: Verify signature
184
144
  run: |
185
- # Poll until healthy, bounded deadline. Never `sleep N && curl`.
186
- deadline=$((SECONDS + 120))
187
- until curl -fs "$PROD_URL/healthz" >/dev/null; do
188
- if [ $SECONDS -ge $deadline ]; then
189
- echo "Production health check failed to pass within 120s" >&2
190
- exit 1
191
- fi
192
- sleep 2
193
- done
145
+ cosign verify \
146
+ --certificate-identity-regexp "https://github.com/${{ github.repository }}/.*" \
147
+ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
148
+ "${{ needs.build.outputs.image }}"
149
+ - name: Promote to staging (by digest)
150
+ run: echo "Deploy ${{ needs.build.outputs.image }} to staging"
151
+ - name: Promote to prod (by digest)
152
+ run: echo "Deploy ${{ needs.build.outputs.image }} to prod"
194
153
  ```
195
154
 
196
155
  ### Reusable deploy step patterns
@@ -201,112 +160,80 @@ deployment steps.
201
160
 
202
161
  ---
203
162
 
204
- ## Azure DevOps Pipelines
163
+ ## Azure DevOps Pipelines (hardened)
205
164
 
206
- File: `azure-pipelines.yml`
165
+ Uses a **Workload Identity Federation** service connection (`azureSubscription`)
166
+ for keyless ACR auth — no stored registry password. Images are signed and
167
+ deployed by digest.
207
168
 
208
169
  ```yaml
209
170
  trigger:
210
- branches:
211
- include: [{DEFAULT_BRANCH}]
212
- tags:
213
- include: ["v*"]
214
-
215
- pr:
216
- branches:
217
- include: [{DEFAULT_BRANCH}]
171
+ branches: { include: [main] }
172
+ tags: { include: ["v*"] }
218
173
 
219
174
  variables:
175
+ azureSubscription: "<WIF-service-connection-name>" # Workload Identity Federation
220
176
  registry: {REGISTRY}
221
177
  imageName: {IMAGE_NAME}
222
- vmImageName: ubuntu-latest
223
178
 
224
179
  stages:
225
- # ── Build & Test ──────────────────────────────────────────────
226
180
  - stage: Build
227
- condition: not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
228
181
  jobs:
229
- - job: BuildAndTest
230
- pool:
231
- vmImage: $(vmImageName)
182
+ - job: BuildSignPush
183
+ pool: { vmImage: ubuntu-latest }
232
184
  steps:
233
- - task: Docker@2
234
- displayName: Build test image
235
- inputs:
236
- command: build
237
- dockerfile: Dockerfile
238
- arguments: --target test -t $(imageName):test
239
-
240
- - task: Docker@2
241
- displayName: Build production image
185
+ - task: AzureCLI@2
186
+ displayName: ACR login (WorkloadIdentityFederation)
242
187
  inputs:
243
- command: build
244
- dockerfile: Dockerfile
245
- arguments: --target production -t $(registry)/$(imageName):$(Build.SourceVersion)
246
-
247
- - task: Docker@2
248
- displayName: Push to registry
249
- condition: eq(variables['Build.SourceBranch'], 'refs/heads/{DEFAULT_BRANCH}')
250
- inputs:
251
- command: push
252
- containerRegistry: $(dockerRegistryServiceConnection)
253
- repository: $(imageName)
254
- tags: |
255
- $(Build.SourceVersion)
256
- latest
257
-
258
- # ── Deploy to Dev ─────────────────────────────────────────────
259
- - stage: DeployDev
260
- condition: eq(variables['Build.SourceBranch'], 'refs/heads/{DEFAULT_BRANCH}')
188
+ azureSubscription: $(azureSubscription)
189
+ scriptType: bash
190
+ scriptLocation: inlineScript
191
+ inlineScript: az acr login --name $(echo $(registry) | cut -d. -f1)
192
+
193
+ - script: |
194
+ TAG="$(registry)/$(imageName):$(Build.SourceVersion)"
195
+ if docker manifest inspect "$TAG" >/dev/null 2>&1; then
196
+ echo "##vso[task.setvariable variable=exists]true"
197
+ else
198
+ echo "##vso[task.setvariable variable=exists]false"
199
+ fi
200
+ displayName: Idempotency guard
201
+
202
+ - script: |
203
+ docker buildx build --target production \
204
+ --provenance=true --sbom=true --push \
205
+ -t $(registry)/$(imageName):$(Build.SourceVersion) .
206
+ condition: ne(variables.exists, 'true')
207
+ displayName: Build, provenance, push
208
+
209
+ - script: |
210
+ DIG=$(docker buildx imagetools inspect $(registry)/$(imageName):$(Build.SourceVersion) --format '{{.Manifest.Digest}}')
211
+ echo "##vso[task.setvariable variable=digestRef;isOutput=true]$(registry)/$(imageName)@$DIG"
212
+ name: build
213
+ displayName: Resolve digest (both paths)
214
+
215
+ - script: cosign sign --yes $(build.digestRef)
216
+ condition: ne(variables.exists, 'true')
217
+ displayName: Sign image (keyless)
218
+
219
+ - stage: Promote
261
220
  dependsOn: Build
262
- jobs:
263
- - deployment: DeployDev
264
- environment: dev
265
- pool:
266
- vmImage: $(vmImageName)
267
- strategy:
268
- runOnce:
269
- deploy:
270
- steps:
271
- - script: |
272
- echo "Deploying $(registry)/$(imageName):$(Build.SourceVersion) to dev"
273
- # {DEV_DEPLOY_COMMANDS}
274
-
275
- # ── Promote to Staging ────────────────────────────────────────
276
- - stage: PromoteStaging
277
221
  condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
278
222
  jobs:
279
- - deployment: DeployStaging
280
- environment: staging
281
- pool:
282
- vmImage: $(vmImageName)
283
- strategy:
284
- runOnce:
285
- deploy:
286
- steps:
287
- - script: |
288
- TAG=${BUILD_SOURCEBRANCH#refs/tags/}
289
- echo "Retagging and deploying $TAG to staging"
290
- # Retag do NOT rebuild
291
- # {STAGING_DEPLOY_COMMANDS}
292
-
293
- # ── Promote to Production ─────────────────────────────────────
294
- - stage: PromoteProd
295
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
296
- dependsOn: PromoteStaging
297
- jobs:
298
- - deployment: DeployProd
299
- environment: production
300
- pool:
301
- vmImage: $(vmImageName)
302
- strategy:
303
- runOnce:
304
- deploy:
305
- steps:
306
- - script: |
307
- TAG=${BUILD_SOURCEBRANCH#refs/tags/}
308
- echo "Deploying $TAG to production"
309
- # {PROD_DEPLOY_COMMANDS}
223
+ - job: VerifyAndDeploy
224
+ pool: { vmImage: ubuntu-latest }
225
+ variables:
226
+ digestRef: $[ stageDependencies.Build.BuildSignPush.outputs['build.digestRef'] ]
227
+ steps:
228
+ # Set identity + issuer to your signing identity — see deploy/SETUP.md.
229
+ - script: |
230
+ cosign verify \
231
+ --certificate-identity-regexp "<CERT_IDENTITY_REGEXP>" \
232
+ --certificate-oidc-issuer "<OIDC_ISSUER>" \
233
+ $(digestRef)
234
+ displayName: Verify signature (by sha256 digest)
235
+ - script: echo "Deploy $(digestRef) to staging then prod (no rebuild)"
236
+ displayName: Promote by digest
310
237
  ```
311
238
 
312
239
  ---
@@ -41,5 +41,15 @@
41
41
  { "type": "semantic", "value": "Suggests Azure Container Registry rather than ghcr.io as the default registry" },
42
42
  { "type": "semantic", "value": "References Azure Pipelines as the CI system for the generated workflow" }
43
43
  ]
44
+ },
45
+ {
46
+ "name": "hardened-pipeline",
47
+ "prompt": "/dock --skip-interview for a Node.js app deploying to AWS ECR",
48
+ "assertions": [
49
+ { "type": "semantic", "value": "Uses OIDC / keyless federated authentication to the container registry rather than stored long-lived credentials" },
50
+ { "type": "semantic", "value": "Signs the built image (cosign) and verifies the signature before promoting to an environment" },
51
+ { "type": "semantic", "value": "Promotes images by immutable sha256 digest rather than a mutable tag" },
52
+ { "type": "semantic", "value": "Generates a deploy/SETUP.md describing the cloud-side federated-identity setup required before first run" }
53
+ ]
44
54
  }
45
55
  ]
@@ -1,5 +1,5 @@
1
1
  {
2
- "generated": "2026-07-02T03:59:12.977Z",
2
+ "generated": "2026-07-06T03:27:34.322Z",
3
3
  "count": 11,
4
4
  "skills": [
5
5
  {
@@ -36,7 +36,7 @@
36
36
  "name": "dock",
37
37
  "directory": "dock",
38
38
  "description": ">- Generate container-based release pipelines that build once and promote immutable artifacts through environments (dev → staging → prod). Detects your stack, interviews for infrastructure choices, then outputs deterministic CI/CD files (Dockerfile, workflows, deployment manifests) that run without an LLM. Use when setting up deployment pipelines, containerizing an app, creating release workflows, or connecting CI to container-friendly infrastructure (Azure Container Apps, AWS Fargate, Google Cloud Run, Kubernetes, Dokku, Coolify, CapRover, etc.).",
39
- "lines": 432,
39
+ "lines": 457,
40
40
  "hasScripts": false,
41
41
  "hasReferences": true,
42
42
  "hasAssets": false,