Skip to content

feat(hub): P02c-1 — Hub Domain Core #6

feat(hub): P02c-1 — Hub Domain Core

feat(hub): P02c-1 — Hub Domain Core #6

Workflow file for this run

# LearnStack Hub — CI baseline.
#
# Mirrors the LearnStack core CI workflow shape so Hub PRs go through the
# same checks. Differences from LearnStack core CI:
# - Solution is `LearnStack.Hub.slnx`.
# - Frontend monorepo runs `pnpm -r typecheck/lint/build/test` against
# `apps/operator-portal` + `packages/{config,sdk,ui}`.
# - `backend-integration` job is `if: false` until P02c-2 lands the first
# Testcontainers-backed test.
# - `openapi-diff` job is `if: false` until P02c-2 produces the first
# generated OpenAPI spec.
# - No `lighthouse-budget` job — operator portal is internal-only; the
# accessibility / performance budget review happens manually in Phase
# 11.
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
DOTNET_SDK_VERSION: "10.0.100"
NODE_VERSION: "20.11.0"
PNPM_VERSION: "9.12.3"
DOTNET_NOLOGO: "true"
DOTNET_CLI_TELEMETRY_OPTOUT: "true"
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true"
jobs:
# ─── Backend ────────────────────────────────────────────────────────────
backend:
name: backend (build + unit + arch + contract)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('backend/**/*.csproj', 'backend/Directory.Packages.props') }}
restore-keys: |
nuget-${{ runner.os }}-
- name: Restore
working-directory: backend
run: dotnet restore LearnStack.Hub.slnx
- name: Format verify (dotnet format)
working-directory: backend
run: dotnet format LearnStack.Hub.slnx --verify-no-changes --no-restore
- name: Build (TreatWarningsAsErrors)
working-directory: backend
env:
CI: "true"
run: dotnet build LearnStack.Hub.slnx --no-restore --configuration Release
- name: Test (unit + architecture + contract; integration excluded)
working-directory: backend
run: |
dotnet test LearnStack.Hub.slnx \
--no-restore --no-build --configuration Release \
--filter "FullyQualifiedName!~LearnStack.Hub.Tests.Integration" \
--logger "trx;LogFileName=test-results.trx" \
--results-directory ../artifacts/backend-tests
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-test-results
path: artifacts/backend-tests
if-no-files-found: warn
# ─── Backend integration (Testcontainers — Postgres) ─────────────────────
backend-integration:
name: backend integration (Testcontainers)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('backend/**/*.csproj', 'backend/Directory.Packages.props') }}
restore-keys: |
nuget-${{ runner.os }}-
- name: Restore
working-directory: backend
run: dotnet restore tests/LearnStack.Hub.Tests.Integration/LearnStack.Hub.Tests.Integration.csproj
- name: Test (integration — Testcontainers Postgres)
working-directory: backend
run: |
dotnet test tests/LearnStack.Hub.Tests.Integration/LearnStack.Hub.Tests.Integration.csproj \
--logger "trx;LogFileName=integration-results.trx" \
--results-directory ../artifacts/backend-integration-tests
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-integration-test-results
path: artifacts/backend-integration-tests
if-no-files-found: warn
# ─── Frontend ───────────────────────────────────────────────────────────
frontend:
name: frontend (typecheck + lint + build + test)
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('frontend/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm -r typecheck
- name: Lint
run: pnpm -r lint
- name: Build
run: pnpm -r build
- name: Test (Vitest)
run: pnpm -r test
# ─── OpenAPI breaking-change check (deferred — P02c-2) ────────────────
openapi-diff:
name: openapi diff (deferred to P02c-2)
runs-on: ubuntu-latest
if: false # activate when /api/v1/internal/license/verify replaces /healthz as the only documented surface
steps:
- run: echo "Placeholder — P02c-2 wires oasdiff against the prior main spec."
# ─── Meta (commit-message format, link audit) ─────────────────────────
meta:
name: meta (commit hygiene + link audit)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Markdown link audit (changed docs)
# Mirror of LearnStack core's link audit. Skips externals + anchors.
# Hub's docs cross-link to LearnStack core via `../learnstack/...`
# relative paths — those links resolve only on the developer's
# workstation (sibling layout). CI can't validate them because the
# LearnStack core repo isn't checked out alongside; the audit skips
# `../learnstack/` paths explicitly.
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
if [[ "$EVENT_NAME" == "pull_request" ]]; then
base="origin/${PR_BASE_REF}"
git fetch --no-tags --depth=1 origin "${PR_BASE_REF}"
else
base="${PUSH_BEFORE_SHA}"
fi
changed=$(git diff --name-only "$base"...HEAD -- '*.md' || true)
if [[ -z "$changed" ]]; then
echo "No changed Markdown files."
exit 0
fi
broken=0
while IFS= read -r f; do
while IFS= read -r link; do
case "$link" in
http://*|https://*|mailto:*|tel:*|ftp://*) continue ;;
esac
# Sibling-relative LearnStack link at any `../` depth.
# Hub docs reference LearnStack core at varying depths
# depending on the source file's own location:
# - root files (README, CLAUDE.md): ../learnstack/...
# - docs/*.md: ../../learnstack/...
# - docs/{architecture,decisions}/*.md: ../../../learnstack/...
# - backend/src/Modules/README.md: ../../../../learnstack/...
# The trailing piece is optional — `../learnstack` (a bare
# directory link to the sibling repo's root) is just as
# legitimate as `../learnstack/docs/...`. CI cannot reach
# the sibling repo, so skip the file-existence check for
# any `(../)+learnstack` link, with or without a trailing
# path. The links are validated locally on the developer's
# workstation.
if [[ "$link" =~ ^(\.\./)+learnstack(/|$) ]]; then
continue
fi
link_path="${link%%#*}"
link_path="${link_path%%\?*}"
[[ -z "$link_path" ]] && continue
source_relative="$(dirname "$f")/$link_path"
if [[ -e "$source_relative" || -e "$link_path" ]]; then
:
else
echo "BROKEN: $f → $link"
broken=$((broken + 1))
fi
done < <(grep -oE '\]\(([^)#][^)]*)\)' "$f" | sed -E 's/^\]\((.+)\)$/\1/')
done <<< "$changed"
if [[ $broken -gt 0 ]]; then
echo "::error::$broken broken relative link(s) in changed Markdown."
exit 1
fi
# ─── Secret scan (Leakwatch) ──────────────────────────────────────────
secret-scan:
name: secret scan (leakwatch)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Install Leakwatch
run: go install github.com/cemililik/leakwatch@v1.5.0
- name: Scan
run: |
leakwatch scan fs . \
--config .leakwatch.yaml \
--format sarif \
--output results.sarif \
--min-severity medium \
--no-verify
- name: Upload SARIF artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: leakwatch-results
path: results.sarif
if-no-files-found: warn