Skip to content

feat: scaffold Backstage application#1

Merged
scotwells merged 15 commits into
mainfrom
feat/initial-backstage-scaffold
Jun 30, 2026
Merged

feat: scaffold Backstage application#1
scotwells merged 15 commits into
mainfrom
feat/initial-backstage-scaffold

Conversation

@scotwells

@scotwells scotwells commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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

  • The portal application — a standard Backstage monorepo (packages/app frontend, packages/backend backend) generated from the official scaffolder, with local-dev and build instructions.
  • Datum branding — Datum light/dark themes, logos, favicon, and a "Datum Portal" title, so the portal reads as a first-party Datum surface rather than stock Backstage. Licensed brand fonts are intentionally not bundled (public repo); clean fallback stacks are used.
  • A deployment bundle (config/) — published as an OCI artifact and consumed by datum-cloud/infra, following the same image-plus-kustomize-bundle pattern as cloud-portal.
  • CI that proves it boots — every PR builds the backend image, stands up Backstage on a real kind cluster against Postgres, and asserts the health and catalog APIs return 200 before merge.
  • A security baseline — hardened deployment (non-root, read-only root filesystem, dropped capabilities, seccomp), publishing gated to trusted refs only, SHA-pinned actions, Snyk + Dependabot scanning, and all high-severity dependency advisories cleared.

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 shared datum-cloud/actions reusable workflows. Dependencies are not committed; they are installed in CI and inside the image build. The catalog org-model and production deploy wiring live in datum-cloud/infra under the service catalog enhancement docs, not in this repo.

Next steps

  • Wire app-config catalog providers, auth (OIDC), and Kubernetes integration for production.
  • Add the production deploy wiring and populate the catalog org-model in datum-cloud/infra.
  • Add the SNYK_TOKEN org secret so the Snyk scan can authenticate.

Tracking: datum-cloud/infra#2967

scotwells added 14 commits June 25, 2026 10:44
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
@scotwells
scotwells requested review from ecv, kevwilliams and savme June 30, 2026 18:59
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.
@scotwells
scotwells merged commit 82664b7 into main Jun 30, 2026
2 of 3 checks passed
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.

2 participants