ctx CI/CD #181
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
| # ctx CI/CD Pipeline | |
| # github.com/GottZ/ctx — The memory your LLM pretends to have. | |
| # GottZ 4-Way RRF | GottZ Scope Model | GottZ Guard | |
| # | |
| # Branch-Strategie: root (Haupt-Branch), keine main/master-Konvention. | |
| # Alle Stages laufen auf: push to root, PR to root, workflow_dispatch. | |
| name: ctx CI/CD | |
| on: | |
| push: | |
| branches: [root] | |
| pull_request: | |
| branches: [root] | |
| workflow_dispatch: | |
| # Nightly cadence for the PV10 live tier (design 06 §4.7/§9.5, decision E3). | |
| # The whole workflow re-runs at 03:00 UTC, but only web-live is scheduled- | |
| # gated to actually do live work; docker is gated OFF schedule so a nightly | |
| # run stays a verification, not a redundant image re-push. | |
| schedule: | |
| - cron: "0 3 * * *" | |
| # Deny-all default — jeder Job deklariert eigene Permissions. | |
| permissions: {} | |
| # Schedule runs get their OWN concurrency group (wave C-W3): the nightly | |
| # tiers (web-live release gate, web-lhci timing trend) must survive a root | |
| # push around 03:00 UTC — in the shared group the push would cancel the | |
| # schedule run and the tag rule (green nightly before a version tag) would | |
| # block the morning release until a manual re-dispatch. Two parallel runs on | |
| # the same ref cannot conflict: docker (image push) is gated OFF schedule. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }}${{ github.event_name == 'schedule' && '-nightly' || '' }} | |
| cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} | |
| env: | |
| GO_VERSION: "1.26" | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ghcr.io/gottz/ctx | |
| jobs: | |
| # ─── Stage 1: Lint ──────────────────────────────────────────────────────────── | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Go ${{ env.GO_VERSION }} | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| cache-dependency-path: go/go.sum | |
| - name: golangci-lint | |
| uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 | |
| with: | |
| version: latest | |
| working-directory: go | |
| args: --timeout=5m | |
| only-new-issues: ${{ github.event_name == 'pull_request' }} | |
| # ─── Stage 1b: Web (frontend gate, parallel to Lint) ───────────────────────── | |
| # Wave PV8 (design 06 §7): PRs check the frontend for the first time. | |
| # Every bun step runs inside the digest-pinned toolchain container built | |
| # from e2e/toolchain.lock — the SINGLE digest source (decision E4), parsed | |
| # by go/web/e2e-container.sh (the same implementation e2e-visual.sh uses; | |
| # no second copy of the lock mechanics in this YAML). That container is the | |
| # canonical render environment, so @visual pixel asserts run here | |
| # (CTX_E2E_CONTAINER=1 is set by the runner script). | |
| # | |
| # Command parity with the release build (go/Dockerfile stage 0): | |
| # `bun install --frozen-lockfile` and `bun run check` are byte-identical; | |
| # `bun run build` runs inside the e2e step via the Playwright webServer | |
| # (with VITE_E2E=1 — the plain production build stays covered by the | |
| # Docker stage). Known, deliberate divergence: the release stage uses | |
| # oven/bun alpine (musl), the toolchain image the glibc bun binary on the | |
| # Playwright noble base (design 06 §4.4/S4). | |
| # | |
| # ── Sharding preparation (design 06 §6.3, wave PV11 — PREPARED, NOT ACTIVE) ── | |
| # The reporter foundation (blob) is already laid (playwright.config.ts), so | |
| # activating sharding is a YAML DIFF, not a rebuild. It stays dormant because | |
| # the calibrated budget (.github/e2e-budget.json: e2e ~77s, budget 160s) sits | |
| # far below the 270s e2e part budget — sharding activates only after the §6.3 | |
| # trigger (three consecutive runs over the e2e part budget) OR by flipping the | |
| # repo variable `vars.E2E_SHARD == '1'`. The activation diff, verbatim: | |
| # | |
| # web: | |
| # strategy: | |
| # fail-fast: false | |
| # matrix: { shard: [1, 2, 3, 4] } | |
| # steps: | |
| # ... | |
| # - name: e2e (Playwright in pinned container) | |
| # run: bash go/web/e2e-container.sh bun run test:e2e -- \ | |
| # --shard=${{ matrix.shard }}/4 # blob reporter is per-shard | |
| # # upload each shard's e2e/.results/blob as blob-${{ matrix.shard }} | |
| # web-merge: # new fan-in job | |
| # needs: [web] | |
| # steps: | |
| # - download all blob-* artifacts | |
| # - bun x playwright merge-reports --reporter html ./blobs | |
| # | |
| # Activation-day gate (design 06 §7-PV11 (c)): a 4×blob → merge-reports probe | |
| # must yield the SAME total test count as a single-run report. Not run here — | |
| # sharding is dormant, and a real 4-shard probe means four full e2e runs | |
| # (deliberately deferred until the trigger fires; not a rebuild). | |
| web: | |
| name: Web | |
| runs-on: ubuntu-latest | |
| # Hard deck above the 10-min budget fail (design 06 §6.3). | |
| timeout-minutes: 12 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Record job start (budget measurement, design 06 §6.3) | |
| run: echo "CTX_JOB_STARTED_AT=$(date +%s)" >> "$GITHUB_ENV" | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| # Full history: the baseline marker gate diffs the whole PR/push | |
| # range and reads per-commit messages (design 06 §5.5 layer 2). | |
| fetch-depth: 0 | |
| # Enforcement that survives --no-verify and dead local hooks: baseline | |
| # changes (__screenshots__, a11y debt growth) require the [baseline] | |
| # marker on every touching commit, or the PR label `baseline-update`. | |
| # Runs where a range exists: PRs (base...head) and pushes with a real | |
| # `before` SHA. Skipped for branch-creation pushes (before = zero SHA) | |
| # and workflow_dispatch — there is no range to check there. | |
| - name: Baseline marker gate (design 06 §5.5 layer 2) | |
| if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000') | |
| env: | |
| RANGE_BASE: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.event.pull_request.base.ref) || github.event.before }} | |
| PR_LABELS: ${{ github.event_name == 'pull_request' && join(github.event.pull_request.labels.*.name, ' ') || '' }} | |
| run: bash .github/scripts/baseline-marker-gate.sh | |
| - name: Build pinned toolchain container (e2e/toolchain.lock) | |
| run: bash go/web/e2e-container.sh bun --version | |
| # Byte-identical to go/Dockerfile stage 0 — the v4.1.0 lesson: bun.lock | |
| # drift was CI-invisible until the Docker build; this catches it on PR. | |
| - name: Install (bun, frozen lockfile) | |
| run: bash go/web/e2e-container.sh bun install --frozen-lockfile | |
| # K9 steps (design 05 → this job): 05 contributes lint:css and the | |
| # contrast matrix as named job components. Reading of 05 (§1 DoD + | |
| # masterplan K9): no separate MECHANISM is demanded — the contrast | |
| # matrix is a vitest test (tokens/contrast-matrix.test.ts) and would be | |
| # covered by `bun run test` below; `bun run check` re-runs lint:css. | |
| # Both get named steps anyway for failure attribution (the K9 contract | |
| # stays visible in the job UI); each re-run costs single-digit seconds. | |
| - name: Lint CSS (token gate + inline gate — K9) | |
| run: bash go/web/e2e-container.sh bun run lint:css | |
| - name: Contrast matrix (WCAG token gate — K9) | |
| run: bash go/web/e2e-container.sh bun run test tokens/contrast-matrix.test.ts | |
| - name: Check (lint:css + svelte-check + tsc) | |
| run: bash go/web/e2e-container.sh bun run check | |
| - name: Unit tests (vitest) | |
| run: bash go/web/e2e-container.sh bun run test | |
| # Full suite incl. @visual: this is the canonical pinned container, the | |
| # committed baselines are compared here (design 06 §5.5: CI has NO | |
| # update path — --update-snapshots exists only in e2e-visual.sh). | |
| - name: e2e (Playwright in pinned container) | |
| # Nightly (schedule) + on-demand (dispatch) INCLUDE the @quarantine | |
| # specs — quarantine means observed, not forgotten (design 06 §5.4, | |
| # wave PV11). PRs/pushes leave CTX_E2E_QUARANTINE empty, so the config | |
| # grepInvert excludes them from the per-PR gate. e2e-container.sh passes | |
| # the var through into the pinned container. | |
| env: | |
| CTX_E2E_QUARANTINE: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '1' || '' }} | |
| run: bash go/web/e2e-container.sh bun run test:e2e | |
| # ── Bundle byte budget gate (PLc, wave C-W2) ────────────────────────── | |
| # Byte truth = the RELEASE encoding: on-disk .br siblings from a build | |
| # WITHOUT VITE_E2E (the bytes go/web/web.go actually serves). At this | |
| # point dist/ holds the VITE_E2E build from the Playwright webServer | |
| # (test hooks included — wrong bytes for a budget verdict), so the gate | |
| # step pair rebuilds fresh and measures immediately after; nothing runs | |
| # in between. Placement AFTER e2e (masterplan C-W2): the webServer build | |
| # can never overwrite the measured artifacts. `if: !cancelled()` keeps | |
| # byte-bloat attribution visible even when e2e is red; the gate itself | |
| # is guarded on the release build's outcome so it can never measure a | |
| # stale VITE_E2E dist/. PLc is the PR byte authority — the Lighthouse | |
| # layer (C-W3) is nightly, warn-only, and never judges bytes. | |
| - name: Release build (byte truth — no VITE_E2E) | |
| id: release_build | |
| if: ${{ !cancelled() }} | |
| run: bash go/web/e2e-container.sh bun run build | |
| - name: Bundle byte budget gate (PLc) | |
| if: ${{ !cancelled() && steps.release_build.outcome == 'success' }} | |
| run: bash go/web/e2e-container.sh bun run test:budget | |
| - name: Flake annotation (report.json retry statistics) | |
| if: ${{ !cancelled() }} | |
| run: bash .github/scripts/flake-annotations.sh | |
| - name: Budget check (design 06 §6.3) | |
| if: ${{ !cancelled() }} | |
| run: bash .github/scripts/e2e-budget.sh | |
| # HTML report from the blob reporter output — git hosting renders no | |
| # PNG diffs inline; without this artifact baseline review is blind | |
| # (design 06 §4.8). Runs in the same container; `bun x` (not `bunx` — | |
| # the toolchain image ships only the bun binary, no bunx symlink). | |
| - name: Render HTML report from blob | |
| if: ${{ !cancelled() }} | |
| run: | | |
| if [ -d go/web/e2e/.results/blob ]; then | |
| bash go/web/e2e-container.sh bun x playwright merge-reports --reporter html ./e2e/.results/blob | |
| else | |
| echo "no blob output (e2e step did not run) — skipping HTML render" | |
| fi | |
| - name: Upload e2e report + diffs | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: web-e2e-report | |
| path: | | |
| go/web/playwright-report/ | |
| go/web/e2e/.results/report.json | |
| # Mock-tier retention 14 days (design 06 §4.8); the live tier | |
| # (wave PV10) gets 3 — real keys flow only there. | |
| retention-days: 14 | |
| if-no-files-found: ignore | |
| # ─── Stage 1c: Web Live Tier (PV10, nightly + on-demand) ───────────────────── | |
| # Wave PV10 (design 06 §4.7/§9.5): the ≤15 @live specs run against a REAL, | |
| # throwaway ctxd + Postgres (docker-compose.e2e.yml), seeded ONLY through | |
| # production write paths (fail-closed target gate, §3.6). It proves classes | |
| # the mock `web` job cannot — server enforcement (tenant isolation), fixture- | |
| # shape truth (W10), real SSE transport — so it is intentionally OFF the | |
| # per-PR path: nightly (schedule) + on-demand (dispatch) + PR opt-in via the | |
| # `e2e-live` label (decision E3). run-live.sh needs only docker on the host; | |
| # bun + Playwright run INSIDE the pinned toolchain image (--network host). | |
| # | |
| # RELEASE-GATE RULE (design 06 §9.5, README): a version tag is pushed only | |
| # after a GREEN nightly run of THIS job — the live-tier extension of the | |
| # repo's "CI is truth, not local" rule. | |
| web-live: | |
| name: Web Live Tier | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| if: >- | |
| github.event_name == 'schedule' || | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'e2e-live')) | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Live tier (throwaway compose stack + seed + @live specs) | |
| run: bash go/web/e2e/live/run-live.sh | |
| # Live traces retention 3 (design 06 §4.8): they record Bearer headers + | |
| # the reveal-once owner_key, but every key is valid ONLY against the | |
| # job-local instance that just died — impact null (§3.6 invariant 3). | |
| - name: Upload live-tier report + traces | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: web-live-report | |
| path: go/web/e2e/live/.results/ | |
| retention-days: 3 | |
| if-no-files-found: ignore | |
| # ─── Stage 1d: Web Trend + History Budget (PV11, nightly) ──────────────────── | |
| # Wave PV11 (design 06 §6.3): two nightly bookkeeping measurements that dock | |
| # onto the nightly run without touching the release-gating web-live job. | |
| # - e2e DURATION + flaky trend line, from the mock-tier `web` job's | |
| # report.json (one JSON line per run, retention 90d — §4.7/§6.3); | |
| # - screenshot HISTORY blob-volume budget (git rev-list over the full | |
| # history, needs fetch-depth: 0 — §3.1/§6.3). | |
| # A DEDICATED job (not a step in web-live): web-live runs a 30-min live stack | |
| # and owns the LIVE report.json (e2e/live/.results), while the trend wants the | |
| # MOCK-tier duration from the `web` job — folding it into web-live would couple | |
| # trend bookkeeping to the release gate and force a mock-artifact download into | |
| # the live job. A ~2-min job stays independently green/red. Nightly + dispatch | |
| # only (no per-PR trend noise). `needs: [web]` for the report.json artifact — | |
| # a red `web` skips the trend (no point trending a failed run). | |
| web-trend: | |
| name: Web Trend + History Budget | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [web] | |
| if: >- | |
| github.event_name == 'schedule' || | |
| github.event_name == 'workflow_dispatch' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout (full history for the screenshot blob-volume measurement) | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download mock-tier e2e report (from the `web` job) | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: web-e2e-report | |
| path: web-report | |
| - name: e2e duration + flaky trend line (design 06 §6.3) | |
| run: | | |
| REPORT=$(find web-report -name report.json -print -quit 2>/dev/null || true) | |
| REPORT="${REPORT:-go/web/e2e/.results/report.json}" TREND_DIR=trend \ | |
| bash .github/scripts/e2e-trend.sh | |
| - name: Screenshot history budget (design 06 §3.1/§6.3) | |
| # Nightly runs on root ⇒ HEAD==root; the script defaults to the `root` | |
| # ref when present, HEAD otherwise. Annotates >=60 MB, escalation-due | |
| # >=150 MB — never an auto-fail (§6.3: the orphan-branch/LFS decision is | |
| # the User's, taken WITH the measurement). | |
| run: bash .github/scripts/history-budget.sh | |
| - name: Upload trend line | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: web-trend | |
| path: trend/ | |
| # Generous retention (design 06 §6.3): one line per run reconstructs | |
| # the duration/flaky trend from the retained set. | |
| retention-days: 90 | |
| if-no-files-found: ignore | |
| # ─── Stage 1e: Web LHCI Nightly (timing trend, wave C-W3) ──────────────────── | |
| # LHCI is the lab TIMING probe — nightly-only, warn-only, NEVER a PR gate | |
| # and NEVER a byte authority (DECISIONS C-E1/C-E2, keeps 06-E6: bytes are | |
| # judged by the PLc budget gate in the `web` job). A DEDICATED job, not a | |
| # step in `web` and without `needs:` — it must stay green/red independently | |
| # (web-trend dies with a red `web`; this must not). Runs the same pinned | |
| # toolchain container; Chromium + node come from the image (no | |
| # chrome-launcher dep, CHROME_PATH globbed in lighthouserc.cjs). Lab noise | |
| # floor is ±20-40% on shared runners — the lhr artifacts feed the 90-day | |
| # trend (C-W4), single runs are never interpreted. timeout-minutes is the | |
| # wall-clock budget: a hung Chromium turns the job visibly red, never a | |
| # silent multi-hour runner burn (C2-3). | |
| web-lhci: | |
| name: Web LHCI Nightly (timing trend) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # workflow_dispatch is included deliberately: it is the only way to test | |
| # the nightly path without waiting for 03:00 UTC (same rationale as | |
| # web-live/web-trend). | |
| if: >- | |
| github.event_name == 'schedule' || | |
| github.event_name == 'workflow_dispatch' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Build pinned toolchain container (e2e/toolchain.lock) | |
| run: bash go/web/e2e-container.sh bun --version | |
| - name: Install (bun, frozen lockfile) | |
| run: bash go/web/e2e-container.sh bun install --frozen-lockfile | |
| - name: Release build (no VITE_E2E — LHCI serves dist/ statically) | |
| run: bash go/web/e2e-container.sh bun run build | |
| - name: LHCI (3 runs, median, warn-only) | |
| run: bash go/web/e2e-container.sh bun run perf:lhci | |
| # e2e-container.sh writes .lhci/ as root (the pinned container runs as | |
| # root, repo bind-mounted at /work). The host-side trend step below runs | |
| # as the runner user and cannot mkdir into a root-owned dir — reclaim | |
| # ownership first. Non-fatal: a missing .lhci/ means perf:lhci already | |
| # failed and owns the red; this step must never introduce a new one. | |
| - name: Reclaim LHCI output ownership (container writes .lhci/ as root) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| if [ -d go/web/e2e/perf/.lhci ]; then | |
| sudo chown -R "$(id -u):$(id -g)" go/web/e2e/perf/.lhci | |
| fi | |
| # Trend line (wave C-W4): ONE JSON line from the representative (median) | |
| # run — the 90-day artifact set reconstructs the timing trend. Noise | |
| # floor ±20-40% (annotation only, never a gate — C1-M2/M3). | |
| - name: LHCI timing trend line (wave C-W4) | |
| if: ${{ !cancelled() }} | |
| run: bash .github/scripts/lhci-trend.sh | |
| - name: Upload trend line (90d) | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: web-lhci-trend | |
| path: go/web/e2e/perf/.lhci/trend/ | |
| retention-days: 90 | |
| if-no-files-found: ignore | |
| # .lhci is a dot-dir — upload-artifact excludes hidden paths by default. | |
| include-hidden-files: true | |
| # if-no-files-found: error — an LHCI run that produced no lhr JSON is a | |
| # broken measurement and must be visibly red, never silently green. | |
| - name: Upload lhr reports | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: web-lhci-reports | |
| path: go/web/e2e/perf/.lhci/ | |
| retention-days: 14 | |
| if-no-files-found: error | |
| # .lhci is a dot-dir — upload-artifact excludes hidden paths by default. | |
| include-hidden-files: true | |
| # ─── Stage 2: Unit Tests ────────────────────────────────────────────────────── | |
| unit-tests: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Go ${{ env.GO_VERSION }} | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| cache-dependency-path: go/go.sum | |
| - name: Run unit tests | |
| working-directory: go | |
| run: | | |
| go test \ | |
| -v \ | |
| -short \ | |
| -race \ | |
| -count=1 \ | |
| -timeout=5m \ | |
| -coverprofile=coverage.out \ | |
| -covermode=atomic \ | |
| ./... | |
| - name: Upload coverage | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: coverage-unit | |
| path: go/coverage.out | |
| retention-days: 7 | |
| # ─── Stage 3: Integration Tests ─────────────────────────────────────────────── | |
| integration-tests: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [unit-tests] | |
| permissions: | |
| contents: read | |
| env: | |
| TESTCONTAINERS_RYUK_DISABLED: "false" | |
| TESTCONTAINERS_PULL_POLICY: "default" | |
| CONTEXT_DB: context_store | |
| CONTEXT_DB_USER: context_user | |
| CONTEXT_DB_PASSWORD: testpassword | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Go ${{ env.GO_VERSION }} | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| cache-dependency-path: go/go.sum | |
| - name: Cache testcontainers Docker image | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.cache/docker-images | |
| key: docker-testcontainers-timescale-pg18-${{ runner.os }} | |
| - name: Load or pull testcontainers Docker image | |
| run: | | |
| if [ -f ~/.cache/docker-images/timescaledb-ha-pg18.tar ]; then | |
| docker load -i ~/.cache/docker-images/timescaledb-ha-pg18.tar | |
| else | |
| docker pull timescale/timescaledb-ha:pg18 | |
| mkdir -p ~/.cache/docker-images | |
| docker save timescale/timescaledb-ha:pg18 -o ~/.cache/docker-images/timescaledb-ha-pg18.tar | |
| fi | |
| - name: Run integration tests | |
| working-directory: go | |
| run: | | |
| go test \ | |
| -v \ | |
| -tags=integration \ | |
| -count=1 \ | |
| -timeout=10m \ | |
| -coverprofile=coverage-integration.out \ | |
| -covermode=atomic \ | |
| ./... | |
| - name: Upload integration coverage | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: coverage-integration | |
| path: go/coverage-integration.out | |
| retention-days: 7 | |
| # ─── Stage 4: Build Binary ──────────────────────────────────────────────────── | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [lint, unit-tests] | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Go ${{ env.GO_VERSION }} | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| cache-dependency-path: go/go.sum | |
| - name: Build binary | |
| working-directory: go | |
| env: | |
| CGO_ENABLED: "0" | |
| GOOS: linux | |
| GOARCH: amd64 | |
| run: | | |
| go build \ | |
| -trimpath \ | |
| -ldflags="-s -w \ | |
| -X main.Version=${{ github.ref_name }} \ | |
| -X main.Commit=${{ github.sha }} \ | |
| -X main.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | |
| -o ../ctxd \ | |
| ./cmd/ctxd/ | |
| - name: Verify binary | |
| run: | | |
| file ctxd | |
| ./ctxd --version 2>/dev/null || ./ctxd version 2>/dev/null || true | |
| - name: Upload binary artifact | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ctxd-linux-amd64 | |
| path: ctxd | |
| retention-days: 1 | |
| # ─── Stage 5: Docker Build + Push ───────────────────────────────────────────── | |
| docker: | |
| name: Docker Build + Push | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # web gates the image push like lint gates the binary build: a red | |
| # frontend job must not publish an image (the Dockerfile embeds web/dist, | |
| # so this only moves the red earlier and names it — wave PV8). | |
| needs: [build, integration-tests, web] | |
| # Off-schedule gate (PV10): the nightly run is a verification pass for the | |
| # live tier — it must never re-push an image of an unchanged ref. | |
| if: >- | |
| github.event_name != 'schedule' && | |
| (github.ref == 'refs/heads/root' || | |
| github.event_name == 'workflow_dispatch') | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Download binary artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ctxd-linux-amd64 | |
| - name: Make binary executable | |
| run: chmod +x ctxd | |
| - name: Setup Docker Buildx | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable=${{ github.ref == 'refs/heads/root' }} | |
| type=sha,prefix=sha-,format=short | |
| type=raw,value={{date 'YYYY-MM-DD'}} | |
| labels: | | |
| org.opencontainers.image.title=ctx | |
| org.opencontainers.image.description=The memory your LLM pretends to have. GottZ 4-Way RRF + GottZ Scope Model. | |
| org.opencontainers.image.url=https://github.com/GottZ/ctx | |
| org.opencontainers.image.source=https://github.com/GottZ/ctx | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.licenses=MPL-2.0 | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 | |
| with: | |
| context: ./go | |
| file: ./go/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: | | |
| type=registry,ref=${{ env.IMAGE_NAME }}:buildcache | |
| type=gha | |
| cache-to: | | |
| type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max | |
| type=gha,mode=max |