release: v1.4.0 — the feature line, site refresh, and the Contexing s… #47
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 | |
| 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) | |
| 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 packages/sdk | |
| - run: npm run build -w packages/mcp | |
| - run: npm test -w packages/mcp | |
| python-sdk: | |
| name: Python SDK (lint + type + test) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install | |
| working-directory: packages/sdk-py | |
| run: python -m pip install --upgrade pip && 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 | |
| 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.23" | |
| cache-dependency-path: packages/sdk-go/go.sum | |
| - 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: | |
| version: "~> v2" | |
| 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.23" | |
| cache-dependency-path: packages/sdk-go/go.sum | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| - 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.23" | |
| cache-dependency-path: packages/sdk-go/go.sum | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| - 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 --upgrade pip && 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 | |
| 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 |