Skip to content

feat(deploy): reusable build workflow, strict mode and the deployment contract - #84

Merged
oto-macenauer-absa merged 1 commit into
chore/publish-docs-actionfrom
chore/deployment-workflow
Sep 4, 2026
Merged

feat(deploy): reusable build workflow, strict mode and the deployment contract#84
oto-macenauer-absa merged 1 commit into
chore/publish-docs-actionfrom
chore/deployment-workflow

Conversation

@oto-macenauer-absa

Copy link
Copy Markdown
Collaborator

Closes #78. Part of #73. Top of the stack: #82#81#80#79.

The gap

Deployment is deliberately not part of this repository, but what a deployment repo needs from it was written down nowhere. Worse, the pieces that make the public build work — a vendored fixture, prebuilt paths, optional entries — are exactly the ones that would let a production build ship a half-empty knowledge base and still exit 0.

--strict / KB_STRICT

Rejects everything a deployment must not rely on:

Rejected Why
prebuilt, localPath A path on someone's disk is not a reproducible deployment
optional Permission to ship without an app nobody noticed was missing
an entry producing no apps A registered artifact that publishes nothing is a broken deploy
an empty registry A knowledge base with no docs is not a successful build

iframe entries survive: a documented stopgap (#10) with no artifact to pin, so the rules about released artifacts cannot apply to them.

This repo's own registry fails strict mode by design — it is a fixture, and saying so out loud is the point.

build-image.yml

A workflow_call the deployment repo invokes. It checks out the caller's repo for the registry and this one for the build, runs against KB_REGISTRY, and either pushes an image or — with image-name empty — uploads dist/ and pushes nothing. That empty-name mode is the right shape for a PR check on a registry, and the skeleton uses it exactly that way.

KB_REGISTRY now resolves absolute paths, so the registry is read where it lives rather than copied into this checkout. Both the orchestrator and Astro resolve it identically, so they cannot end up reading two different files.

Provenance

Every build writes dist/kb-build.json:

{
  "builtAt": "2026-09-04T13:44:49.043Z",
  "registry": "apps.json",
  "strict": true,
  "sources": [
    { "source": "AbsaOSS/my-service-docs", "version": "v1.4.0", "slugs": ["my-service"] }
  ]
}

An image is opaque once pushed, and "which release produced this page" is not answerable from the registry alone once latest has moved. The workflow keeps it as a 90-day artifact and renders it into the job summary.

The contract and the skeleton

contract/DEPLOYMENT.md is the definition the private repo is built from: what lives where, the registry rules, the GitHub App that reads the docs repos (contents: read, installed per repo, hour-long tokens, no personal access token), the narrower token repository_dispatch needs and why it must be scoped to the deployment repo alone, the trigger set, and rollback.

examples/deployment-repo/ is the skeleton to copy: apps.json, a build.yml that mints an App token and calls the reusable workflow, and a registry-check.yml dry run on PRs touching the registry.

It deliberately stops at "an image is pushed". Getting one running is the cloud's business, and the environments and approvals around it are where the real policy lives.

Two review notes

The CI dry run is pinned to the commit under review (kb-ref: ${{ github.sha }}). Without that the job would check out the default branch, build master, and pass green while the change under review was broken.

The new spec cannot clobber dist/. Every strict case is rejected during registry validation, before anything is staged or written, and the provenance assertions read the manifest once at module load. An earlier draft ran full builds against the shared dist/ and broke build-integrity — worth knowing if you extend it.

Verification

Suite Result
npm test (embedded, +9 new) 127 passed
playwright.config.ci.js (standalone) 22 passed
Strict build against this repo's registry exits 1, names the offending field
Normal build exits 0, writes provenance for all 5 apps
All workflow YAML parses 6 files

After this lands

The epic is complete except for the example repo migration (AbsaOSS/knowledge-base-docs-example#3) and cutting v1.0.0, which turns the @v1 references throughout the contract docs into a real tag.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PYtmxSeSXFg4wEXRcKCmAi

… contract

Deployment is deliberately not part of this repository, but what a deployment
repo needs *from* it was written down nowhere. Worse, the pieces that make the
public build work — a vendored fixture, `prebuilt` paths, `optional` entries —
are exactly the ones that would let a production build ship a half-empty
knowledge base and still exit 0.

Three things close that.

`--strict` / KB_STRICT rejects everything a deployment must not rely on:
`prebuilt` and `localPath` (a path on someone's disk is not reproducible),
`optional` (permission to ship without an app nobody noticed was missing), an
empty registry, and any entry that produced no apps. iframe entries survive —
they are a documented stopgap with no artifact to pin, so the rules about
released artifacts cannot apply to them.

build-image.yml is a workflow_call the deployment repo invokes. It checks out
the caller's repo for the registry and this one for the build, runs the build
against KB_REGISTRY, and either pushes an image or, with no image-name, uploads
dist/ and pushes nothing — which is the right shape for a PR check on a
registry. KB_REGISTRY now resolves absolute paths, so the registry is read where
it lives rather than copied into this checkout.

Every build writes dist/kb-build.json: which source, at which version, produced
which slugs. An image is opaque once pushed, and "which release produced this
page" is not answerable from the registry alone once `latest` has moved. The
workflow keeps it as a 90-day artifact and renders it into the job summary.

contract/DEPLOYMENT.md is the definition the private repo is built from — what
lives where, the registry rules, the GitHub App that reads the docs repos
(`contents: read`, installed per repo, short-lived tokens, no personal access
token), the narrower token repository_dispatch needs, the triggers, and rollback.
examples/deployment-repo/ is the skeleton to copy.

CI now calls build-image.yml as a dry run, pinned to the commit under review
rather than the default branch — otherwise the check would build master and pass
while the change under review was broken.

tests/deployment.spec.js covers what strict refuses and what the provenance
records. Every case there is rejected during registry validation, before
anything is staged or written, so the spec cannot clobber the dist/ the rest of
the suite reads.

Suites green: 127 embedded, 22 standalone.

Part of #73. Closes #78.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYtmxSeSXFg4wEXRcKCmAi
@oto-macenauer-absa
oto-macenauer-absa merged commit 88a2f7d into master Sep 4, 2026
8 checks passed
@oto-macenauer-absa
oto-macenauer-absa deleted the chore/deployment-workflow branch September 4, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deployment: reusable build-image workflow, --strict registry mode, and the private deployment repo definition

1 participant