397 lines
15 KiB
HTML
397 lines
15 KiB
HTML
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Deploy pipeline — annotated flowchart</title>
|
|
<style>
|
|
:root {
|
|
--ivory: #FAF9F5;
|
|
--slate: #141413;
|
|
--clay: #D97757;
|
|
--oat: #E3DACC;
|
|
--olive: #788C5D;
|
|
--rust: #B04A3F;
|
|
--gray-150:#F0EEE6;
|
|
--gray-300:#D1CFC5;
|
|
--gray-500:#87867F;
|
|
--gray-700:#3D3D3A;
|
|
--serif: ui-serif, Georgia, "Times New Roman", serif;
|
|
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
background: var(--ivory);
|
|
color: var(--slate);
|
|
font-family: var(--sans);
|
|
-webkit-font-smoothing: antialiased;
|
|
padding: 56px 24px 96px;
|
|
}
|
|
.sheet { max-width: 980px; margin: 0 auto; }
|
|
|
|
header { margin-bottom: 40px; }
|
|
.eyebrow {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--gray-500);
|
|
margin-bottom: 10px;
|
|
}
|
|
h1 {
|
|
font-family: var(--serif);
|
|
font-weight: 500;
|
|
font-size: 34px;
|
|
letter-spacing: -0.01em;
|
|
margin-bottom: 12px;
|
|
}
|
|
.lead {
|
|
font-size: 14.5px;
|
|
line-height: 1.6;
|
|
color: var(--gray-700);
|
|
max-width: 640px;
|
|
}
|
|
.lead code { font-family: var(--mono); font-size: 13px; }
|
|
|
|
/* ── layout ───────────────────────────── */
|
|
.layout {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 300px;
|
|
gap: 32px;
|
|
align-items: start;
|
|
}
|
|
@media (max-width: 920px) { .layout { grid-template-columns: 1fr; } }
|
|
|
|
/* ── chart ────────────────────────────── */
|
|
.canvas {
|
|
border: 1.5px solid var(--gray-300);
|
|
border-radius: 14px;
|
|
background: #fff;
|
|
padding: 28px;
|
|
overflow-x: auto;
|
|
}
|
|
svg.flow { display: block; width: 100%; height: auto; }
|
|
|
|
.flow text {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
fill: var(--slate);
|
|
pointer-events: none;
|
|
}
|
|
.flow .sub { font-size: 10px; fill: var(--gray-500); }
|
|
.flow .lbl { font-size: 10px; fill: var(--gray-500); }
|
|
|
|
.node rect { fill: #fff; stroke: var(--gray-300); stroke-width: 1.5; rx: 8; }
|
|
.node.term rect { fill: var(--gray-150); rx: 22; }
|
|
.node.gate path { fill: #fff; stroke: var(--gray-300); stroke-width: 1.5; }
|
|
.node.ok rect { fill: rgba(120,140,93,0.12); stroke: var(--olive); }
|
|
.node.bad rect { fill: rgba(176,74,63,0.10); stroke: var(--rust); }
|
|
|
|
.node { cursor: pointer; transition: transform 120ms ease; }
|
|
.node:hover { transform: translateY(-1px); }
|
|
.node.active rect,
|
|
.node.active path { stroke: var(--clay); stroke-width: 2; }
|
|
|
|
.edge { stroke: var(--gray-500); stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
|
|
.edge.no { stroke: var(--rust); stroke-dasharray: 4 4; marker-end: url(#arrow-rust); }
|
|
.edge.yes { stroke: var(--olive); marker-end: url(#arrow-olive); }
|
|
|
|
/* ── side panel ───────────────────────── */
|
|
aside {
|
|
position: sticky;
|
|
top: 24px;
|
|
border: 1.5px solid var(--gray-300);
|
|
border-radius: 14px;
|
|
background: #fff;
|
|
padding: 20px 20px 22px;
|
|
}
|
|
aside .hint {
|
|
font-size: 12px;
|
|
color: var(--gray-500);
|
|
margin-bottom: 14px;
|
|
}
|
|
aside h3 {
|
|
font-family: var(--serif);
|
|
font-weight: 500;
|
|
font-size: 19px;
|
|
margin-bottom: 6px;
|
|
}
|
|
aside .meta {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--gray-500);
|
|
margin-bottom: 14px;
|
|
}
|
|
aside p {
|
|
font-size: 13.5px;
|
|
line-height: 1.6;
|
|
color: var(--gray-700);
|
|
margin-bottom: 12px;
|
|
}
|
|
aside pre {
|
|
font-family: var(--mono);
|
|
font-size: 11.5px;
|
|
line-height: 1.55;
|
|
background: var(--gray-150);
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
white-space: pre-wrap;
|
|
color: var(--slate);
|
|
}
|
|
|
|
/* ── legend ───────────────────────────── */
|
|
.legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 18px;
|
|
margin-top: 18px;
|
|
font-size: 12px;
|
|
color: var(--gray-700);
|
|
}
|
|
.legend span { display: inline-flex; align-items: center; gap: 8px; }
|
|
.chip { width: 22px; height: 14px; border: 1.5px solid var(--gray-300); border-radius: 4px; background: #fff; }
|
|
.chip.gate { transform: rotate(45deg); width: 12px; height: 12px; border-radius: 2px; }
|
|
.chip.ok { background: rgba(120,140,93,0.12); border-color: var(--olive); }
|
|
.chip.bad { background: rgba(176,74,63,0.10); border-color: var(--rust); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="sheet">
|
|
|
|
<header>
|
|
<div class="eyebrow">Illustrations & Diagrams · Flowchart</div>
|
|
<h1>What happens when you <code>git push</code></h1>
|
|
<p class="lead">
|
|
The deploy pipeline for <code>acme/web</code>, drawn from <code>.github/workflows/</code> and the
|
|
Argo manifests. Click any step to see what runs, how long it usually takes, and where it can short-circuit.
|
|
</p>
|
|
</header>
|
|
|
|
<div class="layout">
|
|
|
|
<div>
|
|
<div class="canvas">
|
|
<svg class="flow" viewBox="0 0 620 920">
|
|
<defs>
|
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
<path d="M0,0 L10,5 L0,10 z" fill="#87867F"/>
|
|
</marker>
|
|
<marker id="arrow-rust" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
<path d="M0,0 L10,5 L0,10 z" fill="#B04A3F"/>
|
|
</marker>
|
|
<marker id="arrow-olive" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
<path d="M0,0 L10,5 L0,10 z" fill="#788C5D"/>
|
|
</marker>
|
|
</defs>
|
|
|
|
<!-- edges -->
|
|
<path class="edge" d="M310,56 L310,92"/>
|
|
<path class="edge" d="M310,140 L310,176"/>
|
|
<path class="edge" d="M310,224 L310,262"/>
|
|
<path class="edge yes" d="M310,326 L310,368"/>
|
|
<text class="lbl" x="320" y="350">pass</text>
|
|
<path class="edge no" d="M268,294 C190,294 150,294 150,212 L150,212"/>
|
|
<text class="lbl" x="138" y="260" fill="#B04A3F">fail → status</text>
|
|
<path class="edge" d="M310,416 L310,452"/>
|
|
<path class="edge" d="M310,500 L310,538"/>
|
|
<path class="edge yes" d="M310,602 L310,644"/>
|
|
<text class="lbl" x="320" y="626">healthy</text>
|
|
<path class="edge no" d="M352,570 C470,570 470,690 392,720"/>
|
|
<text class="lbl" x="448" y="636" fill="#B04A3F">canary fails</text>
|
|
<path class="edge" d="M310,692 L310,730"/>
|
|
<path class="edge" d="M310,778 L310,816"/>
|
|
|
|
<!-- nodes -->
|
|
<g class="node term" data-k="push">
|
|
<rect x="230" y="12" width="160" height="44"/>
|
|
<text x="310" y="38" text-anchor="middle">git push main</text>
|
|
</g>
|
|
|
|
<g class="node" data-k="ci">
|
|
<rect x="210" y="92" width="200" height="48"/>
|
|
<text x="310" y="112" text-anchor="middle">CI · lint + typecheck</text>
|
|
<text class="sub" x="310" y="128" text-anchor="middle">~2 min · ci.yml</text>
|
|
</g>
|
|
|
|
<g class="node" data-k="test">
|
|
<rect x="210" y="176" width="200" height="48"/>
|
|
<text x="310" y="196" text-anchor="middle">Unit + integration tests</text>
|
|
<text class="sub" x="310" y="212" text-anchor="middle">~6 min · 3 shards</text>
|
|
</g>
|
|
|
|
<g class="node gate" data-k="gate-tests">
|
|
<path d="M310,262 L352,294 L310,326 L268,294 Z"/>
|
|
<text x="310" y="298" text-anchor="middle">pass?</text>
|
|
</g>
|
|
|
|
<g class="node bad" data-k="status">
|
|
<rect x="60" y="176" width="180" height="48"/>
|
|
<text x="150" y="196" text-anchor="middle">Post failure status</text>
|
|
<text class="sub" x="150" y="212" text-anchor="middle">slack #deploys</text>
|
|
</g>
|
|
|
|
<g class="node" data-k="build">
|
|
<rect x="210" y="368" width="200" height="48"/>
|
|
<text x="310" y="388" text-anchor="middle">Build + push image</text>
|
|
<text class="sub" x="310" y="404" text-anchor="middle">ghcr.io/acme/web</text>
|
|
</g>
|
|
|
|
<g class="node" data-k="canary">
|
|
<rect x="210" y="452" width="200" height="48"/>
|
|
<text x="310" y="472" text-anchor="middle">Argo canary 5%</text>
|
|
<text class="sub" x="310" y="488" text-anchor="middle">10 min soak</text>
|
|
</g>
|
|
|
|
<g class="node gate" data-k="gate-canary">
|
|
<path d="M310,538 L352,570 L310,602 L268,570 Z"/>
|
|
<text x="310" y="574" text-anchor="middle">SLO ok?</text>
|
|
</g>
|
|
|
|
<g class="node" data-k="promote">
|
|
<rect x="210" y="644" width="200" height="48"/>
|
|
<text x="310" y="664" text-anchor="middle">Promote 25 → 50 → 100%</text>
|
|
<text class="sub" x="310" y="680" text-anchor="middle">~8 min</text>
|
|
</g>
|
|
|
|
<g class="node bad" data-k="rollback">
|
|
<rect x="392" y="700" width="170" height="48"/>
|
|
<text x="477" y="720" text-anchor="middle">Auto-rollback</text>
|
|
<text class="sub" x="477" y="736" text-anchor="middle">revert image tag</text>
|
|
</g>
|
|
|
|
<g class="node" data-k="smoke">
|
|
<rect x="210" y="730" width="200" height="48"/>
|
|
<text x="310" y="750" text-anchor="middle">Smoke tests in prod</text>
|
|
<text class="sub" x="310" y="766" text-anchor="middle">playwright · 90s</text>
|
|
</g>
|
|
|
|
<g class="node ok term" data-k="done">
|
|
<rect x="230" y="816" width="160" height="44"/>
|
|
<text x="310" y="843" text-anchor="middle">✅ Deploy complete</text>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<span><i class="chip"></i> process step</span>
|
|
<span><i class="chip gate"></i> decision</span>
|
|
<span><i class="chip ok"></i> terminal success</span>
|
|
<span><i class="chip bad"></i> failure path</span>
|
|
</div>
|
|
</div>
|
|
|
|
<aside id="panel">
|
|
<div class="hint">Click a step in the chart →</div>
|
|
<h3 id="p-title">git push main</h3>
|
|
<div class="meta" id="p-meta">trigger · 0s</div>
|
|
<p id="p-body">A push or merge to <code>main</code> fires the <code>deploy</code> workflow. Pushes to other branches stop after CI and never reach the image build.</p>
|
|
<pre id="p-code">on:
|
|
push:
|
|
branches: [main]</pre>
|
|
</aside>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const DETAIL = {
|
|
push: {
|
|
title: "git push main",
|
|
meta: "trigger · 0s",
|
|
body: "A push or merge to <code>main</code> fires the <code>deploy</code> workflow. Pushes to other branches stop after CI and never reach the image build.",
|
|
code: "on:\n push:\n branches: [main]"
|
|
},
|
|
ci: {
|
|
title: "CI · lint + typecheck",
|
|
meta: "github actions · ~2 min",
|
|
body: "Runs ESLint and <code>tsc --noEmit</code> in parallel. This is the only job that also runs on pull-request branches, so it stays fast on purpose.",
|
|
code: "jobs:\n lint:\n run: pnpm lint && pnpm typecheck"
|
|
},
|
|
test: {
|
|
title: "Unit + integration tests",
|
|
meta: "github actions · ~6 min · 3 shards",
|
|
body: "Vitest unit suite plus the API integration tests against an ephemeral Postgres. Sharded three ways; the slowest shard gates the pipeline.",
|
|
code: "strategy:\n matrix:\n shard: [1, 2, 3]\nrun: pnpm test --shard ${{ matrix.shard }}/3"
|
|
},
|
|
"gate-tests": {
|
|
title: "Tests pass?",
|
|
meta: "decision",
|
|
body: "Any shard failing short-circuits here. Nothing is built, and a red status is posted back to the PR and to <code>#deploys</code>.",
|
|
code: "needs: [test]\nif: success()"
|
|
},
|
|
status: {
|
|
title: "Post failure status",
|
|
meta: "side-effect · ~5s",
|
|
body: "The <code>notify</code> job posts the failing shard's summary to Slack and sets the commit status to <code>failure</code>. Pipeline ends.",
|
|
code: "uses: slackapi/slack-github-action@v1\nwith:\n channel: '#deploys'"
|
|
},
|
|
build: {
|
|
title: "Build + push image",
|
|
meta: "github actions · ~4 min",
|
|
body: "Docker buildx with layer caching. Tags the image with the short SHA and pushes to GHCR. This is the first step that produces an artifact.",
|
|
code: "tags: ghcr.io/acme/web:${{ github.sha }}"
|
|
},
|
|
canary: {
|
|
title: "Argo canary 5%",
|
|
meta: "argo rollouts · 10 min soak",
|
|
body: "Argo shifts 5% of traffic to the new ReplicaSet and watches the error-rate and p95 SLOs for ten minutes before deciding.",
|
|
code: "steps:\n - setWeight: 5\n - pause: { duration: 10m }\n - analysis: slo-check"
|
|
},
|
|
"gate-canary": {
|
|
title: "Canary healthy?",
|
|
meta: "decision · analysis template",
|
|
body: "The <code>slo-check</code> analysis compares error rate and p95 against the previous revision. Either metric breaching for two consecutive intervals fails the gate.",
|
|
code: "successCondition: result.error_rate < 0.5\n && result.p95_ms < 800"
|
|
},
|
|
promote: {
|
|
title: "Promote 25 → 50 → 100%",
|
|
meta: "argo rollouts · ~8 min",
|
|
body: "Three more weight steps with a short pause between each. The same analysis runs at every step, so a late regression still aborts.",
|
|
code: "- setWeight: 25\n- pause: { duration: 2m }\n- setWeight: 50\n- pause: { duration: 2m }\n- setWeight: 100"
|
|
},
|
|
rollback: {
|
|
title: "Auto-rollback",
|
|
meta: "argo rollouts · ~30s",
|
|
body: "On a failed analysis Argo flips traffic back to the stable ReplicaSet and marks the rollout <code>Degraded</code>. A page goes to the on-call.",
|
|
code: "abort:\n scaleDownDelaySeconds: 30"
|
|
},
|
|
smoke: {
|
|
title: "Smoke tests in prod",
|
|
meta: "github actions · ~90s",
|
|
body: "A tiny Playwright suite hits the public URL: load the homepage, sign in with a synthetic account, create and delete one record.",
|
|
code: "run: pnpm playwright test smoke/\nenv:\n BASE_URL: https://acme.app"
|
|
},
|
|
done: {
|
|
title: "Deploy complete",
|
|
meta: "terminal · ~30 min total",
|
|
body: "Commit status flips to <code>success</code>, the rollout is marked <code>Healthy</code>, and the SHA is recorded as the new stable revision.",
|
|
code: "✓ web@a1b9e3f live on prod"
|
|
}
|
|
};
|
|
|
|
const nodes = document.querySelectorAll(".node");
|
|
const T = document.getElementById("p-title");
|
|
const M = document.getElementById("p-meta");
|
|
const B = document.getElementById("p-body");
|
|
const C = document.getElementById("p-code");
|
|
|
|
nodes.forEach(n => {
|
|
n.addEventListener("click", () => {
|
|
nodes.forEach(x => x.classList.remove("active"));
|
|
n.classList.add("active");
|
|
const d = DETAIL[n.dataset.k];
|
|
if (!d) return;
|
|
T.textContent = d.title;
|
|
M.textContent = d.meta;
|
|
B.innerHTML = d.body;
|
|
C.textContent = d.code;
|
|
});
|
|
});
|
|
document.querySelector('.node[data-k="push"]').classList.add("active");
|
|
</script>
|
|
</body>
|
|
</html>
|