-
Notifications
You must be signed in to change notification settings - Fork 5
ci: add pre-merge checks (tests, core conformance, built-image smoke) #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| name: ci | ||
|
|
||
| # Pre-merge gate. Until this existed the repo had NO pre-merge checks at all — | ||
| # notify-ci.yml fires on push to main (i.e. after the merge button) and only | ||
| # dispatches to the private CI repo. The consequence was not theoretical: | ||
| # tests/test_antseed_node.py sat red for a month across #63/#68/#69/#70/#71, | ||
| # each PR body noting "1 pre-existing failure", and every one merged. | ||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [main] | ||
|
|
||
| concurrency: | ||
| group: ci-${{ github.head_ref || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| tests: | ||
| runs-on: ubuntu-latest | ||
| services: | ||
| postgres: | ||
| image: postgres:16 | ||
| env: | ||
| POSTGRES_PASSWORD: test | ||
| # UTF8 explicitly: a SQL_ASCII cluster makes psycopg return TEXT as | ||
| # bytes, settings.reload() then silently drops every override and ~26 | ||
| # store-backed tests fail for a reason that looks like application code. | ||
| POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" | ||
| POSTGRES_DB: hoststore_test | ||
| ports: ["5432:5432"] | ||
| options: >- | ||
| --health-cmd pg_isready --health-interval 5s | ||
| --health-timeout 5s --health-retries 10 | ||
| env: | ||
| DATABASE_URL: postgresql://postgres:test@localhost:5432/hoststore_test | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| # The Lua policy core is a submodule; without it the engine-backed | ||
| # tests import nothing and the suite is quietly meaningless. | ||
| submodules: recursive | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" # matches Dockerfile's python:3.12-slim | ||
| cache: pip | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "22" # tests/test_antseed_node.py shells out to `node --test` | ||
| - run: pip install -r requirements.txt -r requirements-dev.txt | ||
| - run: python -m pytest tests -q | ||
|
|
||
| core-tests: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: { submodules: recursive } | ||
| - run: sudo apt-get update && sudo apt-get install -y lua5.4 | ||
| - name: Policy core unit + golden conformance | ||
| working-directory: core | ||
| run: lua tests/run_lua.lua | ||
|
|
||
| images: | ||
| runs-on: ubuntu-latest | ||
| services: | ||
| # The router opens a host-store pool at startup, so a boot smoke needs a | ||
| # database or it proves only that the process can fail to connect. | ||
| postgres: | ||
| image: postgres:16 | ||
| env: | ||
| POSTGRES_PASSWORD: test | ||
| POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" | ||
| POSTGRES_DB: hoststore_ci | ||
| ports: ["5432:5432"] | ||
| options: >- | ||
| --health-cmd pg_isready --health-interval 5s | ||
| --health-timeout 5s --health-retries 10 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: { submodules: recursive } | ||
|
|
||
| - run: docker build -f Dockerfile -t unhardcoded:ci . | ||
| - run: docker build -f Dockerfile.antseed -t unhardcoded-antseed:ci . | ||
|
|
||
| # The check that would have caught #95. A passing test suite proves the | ||
| # REPO is consistent; it says nothing about what the COPY actually put in | ||
| # the image. control.js gained require('./ids.js') while the Dockerfile | ||
| # still named files individually — build succeeded, tests stayed green, | ||
| # and the sidecar's control server died at import in prod with every | ||
| # wallet endpoint 502ing. Importing each shipped module inside the built | ||
| # image is the only place that divergence is observable. | ||
| - name: Every shipped sidecar module resolves its imports inside the image | ||
| run: | | ||
| docker run --rm \ | ||
| -v "$PWD/scripts/check_sidecar_modules.js:/tmp/check.js:ro" \ | ||
| --entrypoint node unhardcoded-antseed:ci /tmp/check.js | ||
|
|
||
| # --network host so the container reaches the runner's postgres service on | ||
| # localhost; a bridged container cannot. No `|| true` on the run: a | ||
| # container that fails to start must fail the job, not fall through to a | ||
| # curl loop that reports the same thing thirty seconds later. | ||
| - name: Router image boots and serves /healthz | ||
| run: | | ||
| set -euo pipefail | ||
| docker run -d --name router-ci --network host \ | ||
| -e DATABASE_URL='postgresql://postgres:test@localhost:5432/hoststore_ci' \ | ||
| --entrypoint python unhardcoded:ci \ | ||
| serve.py --config config.live.lua --metrics metrics.live.lua \ | ||
| --default-profile default --host 0.0.0.0 --port 18080 | ||
| for _ in $(seq 1 30); do | ||
| if curl -fsS localhost:18080/healthz >/dev/null 2>&1; then | ||
| echo "router image boots and serves /healthz"; exit 0 | ||
| fi | ||
| sleep 2 | ||
| done | ||
| echo "::error::router image did not become healthy within 60s" | ||
| docker logs router-ci 2>&1 | tail -60 | ||
| exit 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| #!/usr/bin/env node | ||
| // Assert every shipped sidecar module can resolve its local imports. | ||
| // | ||
| // Run INSIDE the built antseed image (CI mounts this file in): | ||
| // docker run --rm -v "$PWD/scripts/check_sidecar_modules.js:/tmp/c.js" IMG node /tmp/c.js | ||
| // | ||
| // Why it has to run in the image rather than the repo: the repo is always | ||
| // self-consistent, so the Python and Node suites pass whether or not a file | ||
| // reaches the image. control.js gained `require('./ids.js')` while | ||
| // Dockerfile.antseed still named its COPY files individually; the build | ||
| // succeeded, every test stayed green, and the sidecar's control server died at | ||
| // import in production ("Cannot find module './ids.js'") with :8379 never | ||
| // binding and every wallet endpoint returning 502. The only place that | ||
| // divergence is observable is the built artifact. | ||
| // | ||
| // Resolution-only: never executes a module, so a checker run cannot start the | ||
| // control server or touch a wallet. | ||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
|
|
||
| const DIR = process.argv[2] || "/usr/local/lib/antseed"; | ||
| const LOCAL_IMPORT = /(?:require\(\s*|from\s+)['"](\.\/[^'"]+)['"]/g; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
echo "== locate file =="
git ls-files | rg '^scripts/check_sidecar_modules\.js$|package\.json$|docker|Dockerfile' || true
echo "== file size and outline =="
wc -l scripts/check_sidecar_modules.js
ast-grep outline scripts/check_sidecar_modules.js || true
echo "== relevant file content =="
cat -n scripts/check_sidecar_modules.js | sed -n '1,180p'
echo "== package scripts/deps hints =="
if [ -f package.json ]; then jq '.scripts, .files, .devDependencies, .dependencies' package.json; fiRepository: genlayerlabs/unhardcoded Length of output: 1937 🏁 Script executed: #!/bin/bash
set -eu
echo "== locate file =="
git ls-files | rg '^scripts/check_sidecar_modules\.js$|package\.json$|Dockerfile|docker|\.docker' || true
echo "== file size and outline =="
wc -l scripts/check_sidecar_modules.js
ast-grep outline scripts/check_sidecar_modules.js || true
echo "== relevant file content =="
cat -n scripts/check_sidecar_modules.js | sed -n '1,220p'
echo "== package scripts/files hints =="
if [ -f package.json ]; then jq '.scripts, .files, .devDependencies, .dependencies' package.json; fi
echo "== deterministic regex probe =="
node - <<'JS'
const LOCAL_IMPORT = /(?:require\(\s*|from\s+)['"](\.\/[^'"]+)['"]/g;
const samples = [
'import sidecar from "./sidecar";',
'./sidecar; import "./sidecar";',
'import("./sidecar")',
'import("../sidecar")',
'require("./sidecar")',
'require("../sidecar")',
];
for (const s of samples) {
const copy = s.slice();
const matches = (() => { const m; const out = []; while ((m = LOCAL_IMPORT.exec(copy)) !== null) out.push(m[1]); return out; })();
console.log(JSON.stringify({ input: s, matches }));
}
JSRepository: genlayerlabs/unhardcoded Length of output: 1937 Match all local import forms. This scanner only catches Proposed fix-const LOCAL_IMPORT = /(?: require\(\s*|from\s+)['"](\.\/[^'"]+)['"]/g;
+const LOCAL_IMPORT = /(?: require\s*\(\s*|import\s*\(\s*|from\s+|import\s*)['"](\.{1,2}\/[^'"]+)['"]/g;🤖 Prompt for AI Agents |
||
|
|
||
| const shipped = fs.readdirSync(DIR) | ||
| .filter((f) => (f.endsWith(".js") || f.endsWith(".mjs")) && !f.endsWith(".test.js")); | ||
|
|
||
| if (shipped.length === 0) { | ||
| console.error(`no shipped modules found in ${DIR} — wrong path, or the COPY is broken`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const problems = []; | ||
| for (const name of shipped) { | ||
| const file = path.join(DIR, name); | ||
| const src = fs.readFileSync(file, "utf8"); | ||
| for (const m of src.matchAll(LOCAL_IMPORT)) { | ||
| const target = path.join(DIR, m[1]); | ||
| // .mjs imports carry the extension; require() may omit it. Accept either. | ||
| const candidates = path.extname(target) ? [target] : [target + ".js", target + ".mjs", target]; | ||
| if (!candidates.some((c) => fs.existsSync(c))) { | ||
| problems.push(`${name} imports ${m[1]} — not present in the image`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (problems.length) { | ||
| console.error("sidecar image is missing modules its own code requires:"); | ||
| for (const p of problems) console.error(" " + p); | ||
| process.exit(1); | ||
| } | ||
| console.log(`ok — ${shipped.length} shipped modules, all local imports resolve`); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Start the sidecar before polling router health.
auth_proxy.py:1030-1036forwards/healthzto its upstream and returns HTTP 502 when that request fails. This job only runsunhardcoded-antseed:cifor the one-shot import checker at Lines 91-96. It never starts a sidecar listener. The poll therefore cannot return success even when the router starts correctly.Start the sidecar with its required
DATABASE_URLbefore starting the router, and wait for the sidecar health endpoint. If this test must validate only router process startup, replace the upstream-dependent/healthzcheck with a process-level readiness check.🤖 Prompt for AI Agents