feat: add Nix flake dev shell #60
Workflow file for this run
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
| name: CI | |
| # Cross-SDK parity and asset-drift gate on every push to main and PR. | |
| # Actions pinned to full commit SHAs for supply-chain hardening. | |
| on: | |
| push: | |
| # rc/*: a release candidate proves the full matrix here before main sees it. | |
| branches: [main, "rc/*"] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| jobs: | |
| assets: | |
| name: Asset sync (no drift across SDKs) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - run: npm ci | |
| - run: npm run assets:check | |
| secret-scan: | |
| name: Secret scan (TruffleHog) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 # full history so the scanner sees every commit | |
| # TruffleHog installed as a pinned, checksum-verified release binary | |
| # (not a third-party Action), run over full history. | |
| - name: TruffleHog (verified secrets, full history) | |
| env: | |
| TRUFFLEHOG_VERSION: "3.95.6" | |
| run: | | |
| asset="trufflehog_${TRUFFLEHOG_VERSION}_linux_amd64.tar.gz" | |
| base="https://github.com/trufflesecurity/trufflehog/releases/download/v${TRUFFLEHOG_VERSION}" | |
| curl -sSfL "${base}/${asset}" -o "$asset" | |
| curl -sSfL "${base}/trufflehog_${TRUFFLEHOG_VERSION}_checksums.txt" -o checksums.txt | |
| grep " ${asset}$" checksums.txt | sha256sum -c - | |
| tar -xzf "$asset" trufflehog | |
| # Lob excluded: its test-key pattern matches pytest function names (FP | |
| # since trufflehog 3.96.0); keep in sync with .husky/pre-commit. | |
| ./trufflehog git "file://$PWD" --only-verified --fail --no-update --exclude-detectors=lob | |
| dco: | |
| name: DCO (Signed-off-by on every commit) | |
| # Contributor terms are DCO-only, with no CLA, so the sign-off line is the | |
| # whole record: it is checked on the pull request, where it can still be | |
| # fixed by rewriting the branch (CONTRIBUTING.md, "Contributor terms"). | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 # full history so every commit in the range is readable | |
| - name: Every commit carries a Signed-off-by line | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| missing=0 | |
| for sha in $(git rev-list "$BASE_SHA..$HEAD_SHA"); do | |
| if ! git show -s --format=%B "$sha" | grep -qiE '^Signed-off-by: .+ <.+@.+>[[:space:]]*$'; then | |
| echo "::error::$sha has no Signed-off-by line; sign off with 'git commit -s' (CONTRIBUTING.md#contributor-terms)" | |
| missing=1 | |
| fi | |
| done | |
| exit "$missing" | |
| lint: | |
| name: Prettier | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - run: npm ci | |
| - run: npm run lint | |
| version: | |
| name: Version coherence (manifests + prose) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| # A stale version in prose is only noticed once it is already inside a | |
| # published tarball or on pkg.go.dev, where it cannot be corrected in place. | |
| - run: npm run version:check | |
| release-pins: | |
| name: Release-path pins (checker + self-test) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # The hook that runs this locally is bypassable with --no-verify, so the | |
| # authoritative run is here. | |
| - run: sh scripts/check-release-pins.sh | |
| # And the checker itself is checked: a scanner that matches nothing also | |
| # exits 0, so the fixtures prove each rule still fires. | |
| - run: sh scripts/check-release-pins.sh --self-test | |
| release-version: | |
| name: Release version (packages/sdk/package.json) | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.read.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| # The rehearsal below validates a version the way the release workflow | |
| # validates a tag. Off the release path there is no tag, so the version | |
| # comes from the manifest the whole repository is versioned against. | |
| - id: read | |
| run: echo "version=$(node -p "require('./packages/sdk/package.json').version")" >> "$GITHUB_OUTPUT" | |
| release-rehearsal: | |
| name: Release rehearsal | |
| needs: release-version | |
| # Everything a release does short of the writes, run here rather than | |
| # meeting a runner for the first time at the tag. An `rc/*` candidate | |
| # rehearses all of it, Node 22 container and cross-platform Go build | |
| # included; every other run takes the cheaper half, so the artifact path is | |
| # still proven on every pull request. | |
| uses: ./.github/workflows/release-checks.yml | |
| with: | |
| version: ${{ needs.release-version.outputs.version }} | |
| docker_required: ${{ startsWith(github.ref, 'refs/heads/rc/') }} | |
| full: ${{ startsWith(github.ref, 'refs/heads/rc/') }} | |
| node-sdk: | |
| name: Node SDK (build + test, Node ${{ matrix.node-version }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # The declared engines floor and the version the rest of CI runs on: a | |
| # test suite exercised only on the newest runtime does not test the floor. | |
| node-version: [22, 24] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| - run: npm ci | |
| - run: npm run build -w packages/sdk | |
| - run: npm test -w packages/sdk | |
| windows-regressions: | |
| name: Node SDK (Windows regressions) | |
| runs-on: windows-latest | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - run: npm ci | |
| - run: npm run build -w packages/sdk | |
| # The tests for defects only this platform produces: forward-slashed route | |
| # keys, the docs root as disk spells it, and read-only verification against a | |
| # DENY ACE rather than a mode. Named one by one, because the point is these | |
| # tests, and asserted by pass count: a pattern matching nothing also exits 0, | |
| # so a green job on its own is no evidence that anything ran. The count pins | |
| # the shape too: a rename selects nothing, and splitting one of these into | |
| # subtests counts every subtest — both land here, as an edit someone had to mean. | |
| - name: Platform regressions, by name | |
| working-directory: packages/sdk | |
| run: | | |
| named() { | |
| local file="$1" expected="$2" pattern="$3" out status=0 | |
| out=$(node --test --test-reporter=tap --test-name-pattern="$pattern" "$file" 2>&1) || status=$? | |
| printf '%s\n' "$out" | |
| if [ "$status" -ne 0 ]; then | |
| echo "::error::$file: the named run exited $status" | |
| return 1 | |
| fi | |
| # A pattern that selects nothing reports the file itself as one passing | |
| # test, which is precisely the outcome this step exists to refuse. | |
| if grep -qF -- " - $file" <<<"$out"; then | |
| echo "::error::$file: the pattern selected no test" | |
| return 1 | |
| fi | |
| if ! grep -qE "^# pass ${expected}\$" <<<"$out"; then | |
| echo "::error::$file: expected exactly $expected named tests to pass" | |
| return 1 | |
| fi | |
| } | |
| named test/units.test.ts 2 '^(serveViewer routes a backslash-separated request to the content mount|adopt detects an existing docs root by its real name, whatever its case)( .+)?$' | |
| named test/mounts.test.ts 2 '^(mounts: --check-integrity verifies a write-denied host tree, twice, without touching it|mounts: an unusable temp directory makes verification unverifiable, never in-tree)( .+)?$' | |
| # The export's byte contract is the place a path separator or a line ending | |
| # leaks into shipped output, so the golden trees and the strict gate are | |
| # asserted on this platform too. | |
| named test/renderlint.test.ts 3 '^fixture valid-render-.+$' | |
| named test/export.test.ts 2 '^strict: .+$' | |
| mcp: | |
| name: MCP server (build + test, Node ${{ matrix.node-version }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # The server publishes engines.node >=22, so the floor is exercised here | |
| # for the same reason it is on the SDK job above: a suite that only ever | |
| # runs on the newest runtime tests everything except what it claims. | |
| node-version: [22, 24] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| - run: npm ci | |
| - run: npm run build -w packages/sdk | |
| - run: npm run build -w packages/mcp | |
| - run: npm test -w packages/mcp | |
| python-sdk: | |
| name: Python SDK (lint + type + test, Python ${{ matrix.python-version }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Both ends of what the package claims on PyPI, and the version in the | |
| # middle that the rest of CI runs on: 3.10 is the requires-python floor, | |
| # 3.14 the newest classifier, and a suite exercised only at one point in | |
| # that span tests neither promise. | |
| python-version: ["3.10", "3.12", "3.14"] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install | |
| working-directory: packages/sdk-py | |
| run: python -m pip install pip==26.2.1 && pip install -e ".[dev]" | |
| - name: Ruff | |
| working-directory: packages/sdk-py | |
| run: ruff check . && ruff format --check . | |
| - name: Mypy | |
| working-directory: packages/sdk-py | |
| run: mypy src | |
| - name: Pytest | |
| working-directory: packages/sdk-py | |
| run: pytest -q | |
| # The PyPI upload runs twine check --strict and refuses what it cannot | |
| # render, after the tag exists. The same check on every PR, on a | |
| # distribution built here, so metadata never reaches a tag unrendered. | |
| - name: twine check --strict (the publish action's own gate) | |
| run: | | |
| . scripts/lib/twine-check.sh | |
| twine_check python3 packages/sdk-py "$RUNNER_TEMP/twine-gate" | |
| go-sdk: | |
| name: Go SDK (fmt + vet + test) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: "1.26.6" | |
| cache-dependency-path: packages/sdk-go/go.sum | |
| # The toolchain the module declares is the toolchain this job runs. A | |
| # setup-go that resolved to anything else is a failure, not a footnote. | |
| - name: The Go toolchain is the pinned one (scripts/release-pins.env) | |
| run: | | |
| set -euo pipefail | |
| . scripts/lib/release-pins.sh | |
| load_release_pins scripts/release-pins.env | |
| INSTALLED="$(go env GOVERSION)" | |
| test "$INSTALLED" = "go${GO_VERSION}" || { echo "::error::Go $INSTALLED is not the pinned go${GO_VERSION}"; exit 1; } | |
| echo "Go $INSTALLED" | |
| - name: gofmt | |
| working-directory: packages/sdk-go | |
| run: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1) | |
| - name: go vet | |
| working-directory: packages/sdk-go | |
| run: go vet ./... | |
| - name: go test | |
| working-directory: packages/sdk-go | |
| run: go test ./... | |
| # Catch a broken .goreleaser.yaml on PR, not at the release tag. | |
| - name: goreleaser check | |
| uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 | |
| with: | |
| # The same goreleaser the release job installs (scripts/release-pins.env). | |
| version: "v2.18.0" | |
| workdir: packages/sdk-go | |
| args: check | |
| parity: | |
| name: Cross-SDK parity (golden CLI output) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: "1.26.6" | |
| cache-dependency-path: packages/sdk-go/go.sum | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| # Parity compares three CLIs; the Go one must be built by the pinned toolchain. | |
| - name: The Go toolchain is the pinned one (scripts/release-pins.env) | |
| run: | | |
| set -euo pipefail | |
| . scripts/lib/release-pins.sh | |
| load_release_pins scripts/release-pins.env | |
| INSTALLED="$(go env GOVERSION)" | |
| test "$INSTALLED" = "go${GO_VERSION}" || { echo "::error::Go $INSTALLED is not the pinned go${GO_VERSION}"; exit 1; } | |
| echo "Go $INSTALLED" | |
| - run: npm ci | |
| # Node, Go, Python CLIs on identical inputs; asserts byte-identical output. | |
| - run: npm run parity | |
| offline-export: | |
| name: Export fixtures with no network (all three SDKs) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: "1.26.6" | |
| cache-dependency-path: packages/sdk-go/go.sum | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| # The binary compiled below is the one the offline leg runs, so the | |
| # toolchain that compiles it is checked while the network still exists. | |
| - name: The Go toolchain is the pinned one (scripts/release-pins.env) | |
| run: | | |
| set -euo pipefail | |
| . scripts/lib/release-pins.sh | |
| load_release_pins scripts/release-pins.env | |
| INSTALLED="$(go env GOVERSION)" | |
| test "$INSTALLED" = "go${GO_VERSION}" || { echo "::error::Go $INSTALLED is not the pinned go${GO_VERSION}"; exit 1; } | |
| echo "Go $INSTALLED" | |
| - run: npm ci | |
| - run: npm run build -w packages/sdk | |
| - name: Install the Python SDK | |
| working-directory: packages/sdk-py | |
| run: python -m pip install pip==26.2.1 && pip install -e ".[dev]" | |
| # Compiled here, where the network still exists, so nothing inside the | |
| # namespace needs to reach a module proxy to build what it runs. | |
| - name: Compile the Go fixture tests | |
| working-directory: packages/sdk-go | |
| run: go test -c -o /tmp/leji-conformance.test ./internal/conformancetest | |
| # An export is a static site a user hosts anywhere: it must be produced with | |
| # no network at all, and the fixtures' committed goldens say what "produced" | |
| # means byte for byte. `unshare -rn` gives the run its own network namespace, | |
| # which holds nothing but a loopback interface — brought up because the same | |
| # fixtures also serve the layer to themselves. If a runner image ever refuses | |
| # unprivileged namespaces, the replacement is a container run with | |
| # `--network none`; this leg is release-blocking, so it fails, never skips. | |
| - name: The export fixture set, in a namespace with no network | |
| run: | | |
| set -euo pipefail | |
| # ubuntu-24.04 carries AppArmor's restriction on unprivileged user | |
| # namespaces, which is what makes `unshare -r` fail writing | |
| # /proc/self/uid_map; relaxing that one knob lets the namespace be created, | |
| # and everything inside it still runs as the unprivileged runner account. | |
| sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 | |
| unshare -rn sh -euc ' | |
| ip link set lo up | |
| # The isolation is proved before it is relied on: a runner where this | |
| # reached the internet would report a no-network guarantee it never tested. | |
| if curl -s --max-time 10 -o /dev/null https://example.com; then | |
| echo "::error::the network is reachable inside the namespace" | |
| exit 1 | |
| fi | |
| node --test packages/sdk/test/renderlint.test.ts packages/sdk/test/canary.test.ts | |
| (cd packages/sdk-py && python -m pytest -q tests/test_render_fixtures.py tests/test_canary.py) | |
| (cd packages/sdk-go/internal/conformancetest && /tmp/leji-conformance.test -test.count=1) | |
| ' | |
| site: | |
| name: Site (build) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - run: npm ci | |
| - run: npm run build -w leji-site | |
| ui-smoke: | |
| name: UI smoke (viewer, export, and site in a browser) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - run: npm ci | |
| # Both builds are inputs to the suite: it runs the CLI out of packages/sdk/dist | |
| # and previews the site out of packages/site/dist. | |
| - run: npm run build -w packages/sdk | |
| - run: npm run build -w packages/site | |
| - run: npm run browsers -w packages/e2e | |
| - run: npm run e2e -w packages/e2e | |
| # Traces and screenshots are retained only for failures, so a red job is | |
| # inspectable without a rerun and a green one uploads nothing. | |
| - name: Playwright report and traces | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ui-smoke-report | |
| path: | | |
| packages/e2e/report/ | |
| packages/e2e/test-results/ | |
| dogfood: | |
| name: Dogfood (repo conforms to its own spec) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - run: npm ci | |
| - run: npm run build -w packages/sdk | |
| # Run the reference CLI against this repo's own context layer. | |
| - run: npm run dogfood |