feat: scaffold Backstage application#1
Merged
Conversation
Generate Datum's Backstage developer portal and software catalog using the official @backstage/create-app scaffolder. The repo root is the Backstage monorepo with packages/app (frontend) and packages/backend (backend). Dependencies are installed in CI rather than committed (--skip-install), so node_modules are not part of the repository. Add a CI workflow that builds the backend image with the host-build flow and publishes it to ghcr.io/datum-cloud/backstage on pushes to main and on tags. Replace the default README with Datum-specific run/build instructions and links to the design proposal. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Replace the host-build backend Dockerfile, which only COPYs a pre-built skeleton.tar.gz/bundle.tar.gz and assumes yarn install/tsc/build:backend already ran on the host, with the official self-contained multi-stage Dockerfile. The new image runs yarn install --immutable, tsc, and the backend build inside the build stage from the repo-root context, so CI can build it with a plain `docker build -f packages/backend/Dockerfile .`. Key changes: - Regenerate yarn.lock as a valid Yarn 4 (Berry) lockfile; the scaffold shipped a stale Yarn v1 format lockfile that made `yarn install --immutable` fail inside the image. - Stop excluding packages/*/src and plugins from the build context in .dockerignore so the in-image build has the sources it needs. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Drop the bespoke build-image workflow and use the datum-cloud/actions reusable workflows. publish-docker.yaml builds and pushes the backend image; publish-kustomize-bundle.yaml then publishes the config/ tree to oci://ghcr.io/datum-cloud/backstage-kustomize with the freshly built image pinned into config/base. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Add the config/ tree that is published as the backstage-kustomize OCI bundle and consumed by the e2e gate. Key changes: - config/base: namespace, service account, ClusterIP service on 7007, and a Deployment running the backend image with readiness/liveness probes on the new-backend health endpoints (/.backstage/health/v1/readiness and /.backstage/health/v1/liveness). The kustomization carries an images entry for ghcr.io/datum-cloud/backstage so the bundle publisher can pin the built tag. - config/test: e2e overlay that boots Backstage without GCP secrets or SSO. Adds an ephemeral postgres:16-alpine, an app-config.e2e.yaml ConfigMap (guest auth, default auth policy disabled, pg database, github integrations/providers removed, a small static example catalog), and a Deployment patch wiring the DB env and config mount. The image is pinned to backstage:e2e so kind uses the locally loaded image. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Add an e2e workflow that proves the deployment manifests actually boot Backstage. On pull_request and push to main it builds the backend image, loads it into a kind cluster, applies config/test, waits for the postgres and backstage rollouts, and asserts the readiness, liveness, and catalog API endpoints all return HTTP 200. Key changes: - Build with docker/build-push-action and GitHub Actions layer caching (cache-from/cache-to type=gha) so the expensive yarn install / tsc / build:backend layers are reused across PR runs. - Dump pods, deployment description, and backend logs on failure. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Apply Datum's brand palette and typography to the Backstage app using the new frontend system theme extensions. Key changes: - Add packages/app/src/theme/datum.ts with createUnifiedTheme light/dark themes: Midnight Fjord primary, Pine Forge secondary, Aurora Moss sidebar indicator, flat navy page headers, and brand status colors - Register both themes via ThemeBlueprint in a new theme frontend module and disable the built-in Backstage themes in app-config.yaml - Set app title to "Datum Portal" and organization to "Datum" - Reference brand font-family names (Canela Text headings, Alliance No.1 body) with solid fallbacks; decided to use fallback fonts, the licensed brand fonts are intentionally not bundled in this public repo Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Replace the placeholder logos with the official Datum mark and wordmark. Key changes: - LogoFull renders the Datum dark wordmark (lime mark, white text) for the expanded navy sidebar - LogoIcon renders the Datum mark recolored to Aurora Moss lime for the collapsed sidebar Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Key changes: - Add packages/app/public/favicon.svg with the navy Datum mark and reference it from index.html (the generic favicon.ico stays as fallback) - Default the document title to "Datum Portal" and set theme-color to Midnight Fjord Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Restrict the publish workflow so the image and kustomize bundle only ship on main, version tags, and releases -- never on arbitrary branch pushes or pull requests. PR validation is covered by the e2e build. Harden e2e.yaml, which builds untrusted PR code: - Give the buildx GHA cache its own scope (backstage-e2e) so it cannot be cross-restored into the trusted publish build. - Add a top-level least-privilege permissions block (contents: read). - SHA-pin all third-party marketplace actions with a trailing version comment, matching the datum-cloud org convention. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Apply pod and container security hardening to the base Deployment: - Pod: disable service account token automount, run as non-root uid/gid 1000, set fsGroup and the RuntimeDefault seccomp profile. - Container: drop all capabilities, disable privilege escalation, and mount the root filesystem read-only. - Mount writable emptyDir volumes at /tmp and /home/node/.cache so the backend can write scratch and cache data under readOnlyRootFilesystem. - Add CPU/memory requests and a CPU limit alongside the memory limit. Validated end-to-end on kind: the backend rolls out clean with no restarts and readiness, liveness, and catalog endpoints return 200. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Add a Snyk security scan workflow calling the shared datum-cloud reusable workflow on pushes to main, pull requests, and a weekly schedule, with the permissions required for SARIF upload to GitHub code scanning. Requires a SNYK_TOKEN org secret to be present. Add Dependabot config for the npm and github-actions ecosystems, both on a weekly cadence with grouped minor/patch npm updates. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Force-bump the three packages carrying high-severity advisories to patched releases within their compatible majors via root resolutions, and run yarn dedupe to collapse duplicate ranges: - undici 7.24.7 -> 7.28.0 (scoped to the 7.x descriptor; 6.x untouched) - protobufjs -> 7.6.4 - tar -> 7.5.16 Drops the high-severity advisory count from 3 affected packages (14 advisory entries) to 0. Typecheck, app build, and the kind e2e all pass with the bumped dependencies. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Restructure config/ to mirror the milo-os service convention (base + components + overlays), factoring cross-cutting concerns into opt-in kustomize Components so each environment composes only what it needs. Key changes: - Add components/database-cnpg: a CloudNativePG Cluster plus a Deployment patch sourcing POSTGRES_* from the generated backstage-postgres-app secret. Replaces the raw Postgres Deployment in the kind e2e path. - Add components/app-config: config-layering wiring (ConfigMap mount + --config args) reused across environments; overlays supply content. - Add components/networking: HTTPRoute attaching to the shared internal-auth-gateway, where OIDC is enforced. - Add components/external-secrets: ExternalSecret projecting GitHub App and backend secrets from the gcp-secret-store ClusterSecretStore. - Move config/test to overlays/e2e composed from base + database-cnpg + app-config; the backend uses pluginDivisionMode: schema so the plain CNPG owner role works without CREATEDB. - e2e workflow installs the cloudnative-pg operator chart (matching infrastructure/databases/cnpg-operator) and waits on the Cluster before the Backstage rollout. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
Add a weekly (and manually dispatchable) workflow that runs `yarn backstage-cli versions:bump` and opens a PR, moving every @backstage/* package to the latest coordinated release and updating backstage.json. The E2E workflow gates the resulting PR. Scope Dependabot's npm updates to ignore @backstage/* so it does not compete with the bump workflow; it continues to manage the rest of the dependency tree and GitHub Actions. Claude-Session: https://claude.ai/code/session_01NMSkwUcaTmZr7S5XmV2aFG
The CNPG Cluster takes ~25-30s to bootstrap initdb, but the backend Deployment is applied at the same time as the Cluster. The new Backstage backend attempts its database connection once at startup; on failure it stays up in a degraded state (liveness 200, readiness 503) instead of crash-looping, so Kubernetes never restarts it and the rollout never completes. This failed the kind e2e and would equally strand the pod in production, where Flux applies the Cluster and Deployment together. Add a wait-for-postgres initContainer to the database-cnpg component so the backend only starts once Postgres accepts connections. It reuses the CNPG image (pg_isready, already cached on the nodes) and mirrors the hardened container securityContext.
ecv
approved these changes
Jun 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Establishes Datum's internal developer portal and software catalog, built on Backstage. This is the foundation for representing every infrastructure service that makes up our platform — so each one has a clear owner, a complete and trustworthy inventory exists, and runbooks, architecture, and operational docs live in a single place.
Backstage is the implementation detail; the goal is the catalog. This PR stands up the application, brands it as the Datum Portal, and ships everything needed to deploy and operate it the same GitOps way as every other Datum app.
What's included
packages/appfrontend,packages/backendbackend) generated from the official scaffolder, with local-dev and build instructions.config/) — published as an OCI artifact and consumed bydatum-cloud/infra, following the same image-plus-kustomize-bundle pattern as cloud-portal.How it deploys
The backend image (
ghcr.io/datum-cloud/backstage) and the kustomize bundle (ghcr.io/datum-cloud/backstage-kustomize) are published via the shareddatum-cloud/actionsreusable workflows. Dependencies are not committed; they are installed in CI and inside the image build. The catalog org-model and production deploy wiring live indatum-cloud/infraunder the service catalog enhancement docs, not in this repo.Next steps
app-configcatalog providers, auth (OIDC), and Kubernetes integration for production.datum-cloud/infra.SNYK_TOKENorg secret so the Snyk scan can authenticate.Tracking: datum-cloud/infra#2967