This site: a portfolio that operates itself
A static site whose content and live status diagram are kept current by its own pipeline, and which only shows claims a repository can back.
- Role
- Design, build and operations
- Period
- Oct 2026
- Status
- Public source
- Problem
- A portfolio goes stale the week it is published, and most of what portfolios claim cannot be checked.
- Key decision
- Make keeping it current the pipeline's job, show the pipeline on the home page, and publish nothing a repository cannot back.
- Result
- Nightly rebuilds from the GitHub and dev.to APIs, a live status diagram measured from the visitor's browser, and an agent that can only open pull requests.
Architecture
Components and flows as text
| Component | Kind | Technology | Flows out |
|---|---|---|---|
| Case-study agent | pipeline | scheduled · PR-gated | ⇢ portfolio repo: pull request, merged only after review |
| GitHub API | external service | 423 contributions / yr | → Deploy workflow: repos, upstream PRs, contributions |
| dev.to API | external service | 6 recent articles | → Deploy workflow: articles |
| portfolio repo | service | main @ 18436cd | ⇢ Deploy workflow: push to main |
| Deploy workflow | pipeline | run #5 · success | ⇢ Cloudflare Pages: deploy build + status.json |
| Cloudflare Pages | edge | checking… | → You: pages |
| You | client | this browser | → Cloudflare Pages: live status check |
How a build works
- A push to
main, the nightly schedule or a manual run starts the deploy workflow. scripts/fetch-data.mjspulls repositories, upstream pull requests, the contribution total and articles. Each source is wrapped so that a failure is recorded rather than thrown:
async function attempt(source, fn, fallback) {
try {
return await fn();
} catch (err) {
errors.push({ source, message: String(err.message ?? err) });
return fallback;
}
}
- Astro type-checks and builds static HTML. Diagrams are laid out at build time and emitted as SVG, so no layout code runs in the browser.
- The build writes
/status.json, the same data the home-page diagram is drawn from, and deploys to Cloudflare Pages. - In the browser, one small script measures a round trip to
/status.json, marks the edge node, and reports whether a newer build is live.
Verification
astro checktype-checks every build.- CI fails if the output contains inline scripts, inline styles or
data:URIs that the CSP would block. - Links, anchors and layouts at 375, 768 and 1440 pixels are checked with Playwright before changes ship.
Decisions
Every source is optional; failures are shown, not hidden
The build fetches each source inside a wrapper that records failures instead of throwing. A broken API produces a warning on the diagram, not a broken site.
Options considered
- Fail the build when an API fails: the site stops updating whenever GitHub has a bad hour.
- Ship with the last good data and say so.
Trade-off accepted
A build can ship with a partial view; the status diagram makes that visible.
Static, rebuilt nightly
No server and no runtime API calls. GitHub Actions rebuilds on every push to main and every night, and deploys to Cloudflare Pages.
Options considered
- Server-side rendering with live API calls: always fresh, and a server to run and rate limits to manage.
- Static output on a schedule.
Trade-off accepted
Content can be up to a day old; the footer and the diagram say exactly how old.
A strict CSP with nothing inline
script-src and style-src are both self. All JavaScript is in external files, stylesheets are never inlined, assets are never inlined as data: URIs, and diagrams are SVG with no style attributes. CI fails the build if anything inline appears.
Options considered
- Allow unsafe-inline: easier, and it removes most of what a CSP is for.
- Make the build produce nothing inline, and test for it.
Trade-off accepted
Some framework conveniences are off, and code highlighting is plain.
The agent proposes; review commits
The repository includes a scheduled workflow that runs an AI agent against my public repositories. When one is new or has changed significantly, it drafts a case study and opens a pull request labelled case-study-draft, listing the files it relied on. It has no path to main except a reviewed merge.
Options considered
- Let the agent commit to main: always current, and unreviewed claims in public.
- Pull requests only.
Trade-off accepted
Content waits for review.
Only evidence-backed content
Repositories appear only if an audit marked them worth showing, and every claim in a case study traces to a file, test or pull request.
Options considered
- List every public repository automatically.
- Curate against an evidence ledger.
Trade-off accepted
Less on the page; everything on it holds up.
Failure modes
| Failure | Detection | Handling | Evidence |
|---|---|---|---|
| GitHub or dev.to API down | Fetch wrapper records the error | Build ships; node shows a warning | fetch-data.mjs |
| Inline script or style slips in | CI grep on the build output | Build fails before deploy | ci.yml |
| Agent drafts an unsupported claim | PR review against cited files | Not merged | case-study-agent.yml |
| Visitor has a stale page | Live status.json SHA differs | Status line says a newer build is live | control-plane.js |
What I would change next
- Add an uptime monitor to the status diagram once the site is on its own domain.
- Run the link and layout checks as a Playwright job in CI.
Evidence index
Links point to the public repository.