@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.
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/lib/banner.cjs +48 -15
- package/package.json +1 -1
- package/skills/dock/SKILL.md +25 -0
- package/skills/dock/references/hardening.md +104 -0
- package/skills/dock/references/pipeline-templates.md +139 -212
- package/skills/dock/tests/evals.json +10 -0
- package/skills/manifest.json +2 -2
package/hooks/lib/banner.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
...
|
|
23
|
-
|
|
55
|
+
...lines,
|
|
56
|
+
separator,
|
|
24
57
|
` Session Guard v${config.version}`,
|
|
25
|
-
|
|
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
package/skills/dock/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
37
|
-
cancel-in-progress:
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
97
|
-
tags:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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:
|
|
112
|
-
|
|
113
|
-
|
|
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:
|
|
106
|
+
- name: Resolve digest reference
|
|
107
|
+
id: digest
|
|
117
108
|
run: |
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
120
|
+
permissions:
|
|
121
|
+
contents: read
|
|
122
|
+
id-token: write
|
|
138
123
|
steps:
|
|
139
|
-
- uses:
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
--
|
|
157
|
-
"${{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
138
|
+
permissions:
|
|
139
|
+
contents: read
|
|
140
|
+
id-token: write
|
|
175
141
|
steps:
|
|
176
|
-
- uses:
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
230
|
-
pool:
|
|
231
|
-
vmImage: $(vmImageName)
|
|
182
|
+
- job: BuildSignPush
|
|
183
|
+
pool: { vmImage: ubuntu-latest }
|
|
232
184
|
steps:
|
|
233
|
-
- task:
|
|
234
|
-
displayName:
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
]
|
package/skills/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
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":
|
|
39
|
+
"lines": 457,
|
|
40
40
|
"hasScripts": false,
|
|
41
41
|
"hasReferences": true,
|
|
42
42
|
"hasAssets": false,
|