Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .github/workflows/ci.yml
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
Comment on lines +101 to +117

Copy link
Copy Markdown

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-1036 forwards /healthz to its upstream and returns HTTP 502 when that request fails. This job only runs unhardcoded-antseed:ci for 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_URL before starting the router, and wait for the sidecar health endpoint. If this test must validate only router process startup, replace the upstream-dependent /healthz check with a process-level readiness check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 101 - 117, Update the “Router image
boots and serves /healthz” workflow step to start the required sidecar listener
with DATABASE_URL before launching the router, then wait for the sidecar health
endpoint before polling router health. Ensure the sidecar is started from the
appropriate image/configuration and remains available on the expected listener;
alternatively, replace the upstream-dependent /healthz assertion with a
process-level router readiness check if this job is intended to validate startup
only.

51 changes: 51 additions & 0 deletions scripts/check_sidecar_modules.js
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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; fi

Repository: 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 }));
}
JS

Repository: genlayerlabs/unhardcoded

Length of output: 1937


Match all local import forms.

This scanner only catches ./ specifiers after require( or from, so it misses ../ specifiers, side-effect imports such as import "./sidecar", and dynamic imports such as import("./sidecar"). Include those forms so the check cannot pass while a local import target is missing from the shipped image.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check_sidecar_modules.js` at line 22, Update the LOCAL_IMPORT pattern
used by the sidecar import scanner to match both ./ and ../ relative specifiers,
including side-effect import statements and dynamic import() calls, while
preserving existing require() and from imports.


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`);
Loading