Repository navigation
feat(hub): P02c-1 — Hub Domain Core #6
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
| # 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 |