Skip to content

Commit 6cde695

Browse files
authored
ci: harden SDK test and publish verification (#33)
* ci: use cwd-safe stdlib Python tests Run complete Python discovery from the python package directory in CI, publish verification, and contributor instructions so imports resolve to the checkout under test. Remove the unused pytest development dependency and enforce the runner, working directory, and documentation contract from the TypeScript policy suite. * test: make publish policy mutation CRLF-safe Match both LF and CRLF when removing the npm publish environment in the negative workflow-policy regression. This keeps the security mutation real on Windows checkouts instead of silently testing the unchanged workflow. * test: allow Python artifact failure check on slow runners Give the clean-install failure regression 60 seconds to build a wheel and exercise disposable Python environments. The Windows run completed near the old 20-second ceiling, so the previous budget was flaky despite correct behavior. * docs: correct SDK 0.2.0 release evidence Update the maintainer notes to the published npm and PyPI 0.2.0 state and record that the release used the infrastructure-unavailable require_live_canary=false override. Keep the evidence boundary explicit: build, unit, packaging, security, trusted-publish, and registry-install gates ran, but strict live-canary steps were skipped and the release is not strict-live proof. * fix: close SDK CI review gaps Fail CI and publish on zero discovered Python tests even on Python 3.11, and couple contributor cwd guidance to the exact unittest command in regression coverage. Align the live-canary guide with the documented infrastructure-unavailable override while preserving the boundary that such a release is not strict-live evidence. * docs: preserve strict canary contract wording Keep the established strict-child-canary and production-base-url contract phrases intact while documenting the infrastructure-unavailable release override. * docs: record SDK CI hardening checkpoint Record the verified branch, exact commits, full local gate totals, Python-matrix caveat, and independent RELEASE verdict for handoff. No push, publish, deployment, or production mutation was performed.
1 parent b9e0935 commit 6cde695

9 files changed

Lines changed: 199 additions & 26 deletions

File tree

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
- [ ] My code follows the style of this project (TS strict, no `any` in public surface; Python type hints + docstrings)
1717
- [ ] I have added tests for any new behavior
18-
- [ ] All tests pass locally (`pnpm test` for TS, `python -m pytest` for Python)
18+
- [ ] All tests pass locally (`pnpm --dir typescript test` for TS; from `python/`, `python -m unittest discover -s tests -v` for Python)
1919
- [ ] I have updated the relevant `CHANGELOG.md` files (typescript/ and/or python/)
2020
- [ ] I have NOT bumped the SDK version (maintainers handle that at release time)
2121
- [ ] I have NOT added any runtime dependencies (both SDKs are zero-dep by policy)

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ jobs:
143143
working-directory: python
144144
run: |
145145
python -m pip install -e .
146-
python -m pytest tests/ -v
146+
python -c "import unittest; suite = unittest.defaultTestLoader.discover('tests'); count = suite.countTestCases(); print(f'Discovered {count} tests'); raise SystemExit(0 if count else 1)"
147+
python -m unittest discover -s tests -v
147148
148149
python-required:
149150
name: Python SDK

.github/workflows/publish.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,11 @@ jobs:
114114
node scripts/verify-npm-package.mjs "$TGZ"
115115
116116
- name: Test Python
117+
working-directory: python
117118
run: |
118-
python -m pip install -e python
119-
python -m pytest python/tests/ -v
119+
python -m pip install -e .
120+
python -c "import unittest; suite = unittest.defaultTestLoader.discover('tests'); count = suite.countTestCases(); print(f'Discovered {count} tests'); raise SystemExit(0 if count else 1)"
121+
python -m unittest discover -s tests -v
120122
121123
- name: Build Python artifacts
122124
run: python -m build python --outdir artifacts/python-local

AGENT-NOTES.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ history, no workspace identifiers, and no operational secrets.
88

99
| Registry | Package | Source dir | Source version |
1010
|---|---|---|---|
11-
| npm | `@runinfra/sdk` | `typescript/` | `0.1.4` |
12-
| PyPI | `runinfra` | `python/` | `0.1.4` |
11+
| npm | `@runinfra/sdk` | `typescript/` | `0.2.0` |
12+
| PyPI | `runinfra` | `python/` | `0.2.0` |
1313

14-
Registry latest remains `0.1.3` until the protected trusted-publish workflow
15-
publishes `0.1.4` from `main`.
14+
Registry latest is `0.2.0` on both npm and PyPI. Future releases must still use
15+
the protected trusted-publish workflow from `main`.
16+
17+
The 0.2.0 release used the documented `require_live_canary=false` override
18+
because live-service canary infrastructure was unavailable. The workflow
19+
skipped strict readiness, artifact live-canary, and promotion-report steps;
20+
build, unit, packaging, security, trusted-publish, and registry-install gates
21+
still ran. Registry availability for 0.2.0 is therefore not strict live-canary evidence.
1622

1723
## What Ships
1824

@@ -72,16 +78,16 @@ Important PyPI details:
7278

7379
| Surface | Status |
7480
|---|---|
75-
| `client.chat.completions.create`, `client.responses.create` | Beta, contract-tested. Current 0.1.4 promotion artifacts are not strict-live green; publish requires fresh production artifact canaries with zero skipped or failed rows |
76-
| `client.embeddings.create` | Beta, contract-tested. Not strict live-canary verified in the current promotion artifacts |
81+
| `client.chat.completions.create`, `client.responses.create` | Beta, contract-tested. Published 0.2.0 artifacts are not strict-live verified because the infrastructure-unavailable override skipped those gates |
82+
| `client.embeddings.create` | Beta, contract-tested. Published 0.2.0 artifacts are not strict live-canary verified |
7783
| `client.images.generate` | Experimental, HTTP envelope matches the documented API but strict live canary coverage is still required |
7884
| `client.audio.speech.create` | Experimental, strict live canary coverage is still required |
7985
| `client.audio.transcriptions.create` | Experimental, strict live canary coverage is still required |
8086
| `client.voice.pipeline.create` | Experimental, pipeline-scoped route, not live-canary verified |
8187
| `client.webhooks.verifySignature`, `client.webhooks.constructEvent` | Local helpers, covered by unit tests |
8288
| Webhook delivery create/list | Not shipped and not exposed on the public SDK surface |
8389

84-
Python remains sync-only in `0.1.4`. Do not claim async support until an
90+
Python remains sync-only in `0.2.0`. Do not claim async support until an
8591
`AsyncRunInfra` client has matching unit tests, streaming tests, live canaries,
8692
and clean-install coverage.
8793

@@ -114,7 +120,10 @@ node scripts/verify-promotion-reports.mjs --readiness artifacts/sdk/live-canary-
114120
The publish workflow builds the npm tarball, Python wheel, and Python sdist
115121
once, uploads them as `runinfra-sdk-promoted-artifacts`, downloads the same
116122
artifacts for strict readiness and live promotion reports, recomputes hashes
117-
with `--artifacts-root .`, and the publish jobs publish only the downloaded `runinfra-sdk-promoted-artifacts` files. `dry_run=false` cannot bypass `promotion-gate`.
123+
with `--artifacts-root .`, and the publish jobs publish only the downloaded `runinfra-sdk-promoted-artifacts` files. `dry_run=false` cannot bypass `promotion-gate`. The infrastructure-unavailable
124+
`require_live_canary=false` override skips only the strict live-canary steps
125+
inside that gate and records that fact in the workflow summary; it does not
126+
turn the release into strict live-canary evidence.
118127

119128
Clean artifact install/import now exercises the npm tarball, Python wheel, and
120129
Python sdist. The sdist path builds and imports in a disposable consumer
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# SDK CI Test-Runner Hardening
2+
3+
Date: 2026-07-11
4+
Agent: SDK hardening lane
5+
Repo: `runinfra-sdk`
6+
Worktree: `runinfra-sdk-ci-hardening-20260711`
7+
Branch: `fix/sdk-ci-hardening-20260711`
8+
9+
## Goal
10+
11+
Make every complete Python CI, publish, and contributor test command run the
12+
stdlib `unittest` suite from `python/`, so test imports resolve to the checkout
13+
under test instead of an installed package or sibling worktree. Preserve the
14+
existing package/runtime surface and trusted-publishing policy.
15+
16+
Done means the Python suite, TypeScript suite, build, workflow/version/surface
17+
policy checks, and diff check pass from a fresh branch based on current
18+
`origin/main`; an independent hostile review finds no blocker; and the result
19+
is committed locally without pushing.
20+
21+
## Initial Verified State
22+
23+
- Fresh base: `b9e09353da858cdc5ec53a3600a1f345a89ee038` (`origin/main`).
24+
- The dirty detached SDK checkout and old PR #15 worktree are untouched.
25+
- `python -m unittest discover -s tests -v` from `python/` passed all 158 tests.
26+
- Two focused TypeScript policy regressions fail on the unmodified base:
27+
stale pytest-version expectations and an LF-only publish-workflow mutation
28+
that does not match Windows CRLF files.
29+
- npm and PyPI both report `0.2.0` as latest.
30+
- GitHub publish run `27575407723` published 0.2.0 with
31+
`require_live_canary=false`; strict live-canary steps were skipped under the
32+
documented infrastructure-unavailable override, while build, unit,
33+
packaging, security, trusted-publish, and registry-install gates ran.
34+
35+
## Scope
36+
37+
- Existing CI and publish workflow Python test steps.
38+
- Python development requirements.
39+
- Pull-request and contributor test commands.
40+
- TypeScript policy regressions for Python tooling and Windows CRLF.
41+
- The slow Python artifact-failure test timeout, isolated as its own concern.
42+
- `AGENT-NOTES.md` release-state accuracy for 0.2.0.
43+
44+
## Out Of Scope
45+
46+
- SDK runtime behavior or public API changes.
47+
- Money, pricing, billing, or credit semantics.
48+
- Live-canary activation, infrastructure provisioning, publishing, pushing,
49+
or merging.
50+
- Reusing or updating the old PR #15 branch.
51+
52+
## Guardrails
53+
54+
- Use the existing implementation paths, with no feature flag or parallel
55+
runner.
56+
- Add no package dependency.
57+
- Stage explicit files only and commit with `git commit -F`.
58+
- Do not push or merge from this lane.
59+
60+
## Checkpoint
61+
62+
Implementation is complete on local branch
63+
`fix/sdk-ci-hardening-20260711`, based on
64+
`b9e09353da858cdc5ec53a3600a1f345a89ee038` (`origin/main`). The implementation
65+
HEAD is `ae4b5fcde541ac50f8895d42c8008cd6f4c0fa95`.
66+
67+
Commits, oldest first:
68+
69+
- `5f0fb6f0de49ab17cdcd1ba15bd252d3771f23b1` - cwd-safe stdlib Python runners.
70+
- `3c1823ef937283f85f5d8e2978e8fd9b2cf05c76` - Windows CRLF-safe policy mutation.
71+
- `06d0878a8727365c8c0b1346dc9aa4805bb2e12d` - isolated slow artifact-test timeout.
72+
- `cf6c150e80d95b64d19813769c74df0f4b9f41e8` - accurate 0.2.0 release evidence.
73+
- `f71b2c3d637d3c030fc7c48b1cdd43ee2cf05639` - close hostile-review gaps.
74+
- `ae4b5fcde541ac50f8895d42c8008cd6f4c0fa95` - preserve strict canary wording.
75+
76+
Verified on the exact implementation HEAD:
77+
78+
- Python 3.14.2 from `python/`: discovery guard found 158 tests; all 158 passed.
79+
- An empty-directory discovery probe exited 1, proving the guard rejects zero tests.
80+
- A fresh virtual environment installed the revised development requirements,
81+
had no pytest package, and passed all 158 stdlib `unittest` tests.
82+
- TypeScript: all 258 Vitest tests passed, including workflow, cwd mutation,
83+
Windows CRLF, release-evidence, and isolated-timeout regressions.
84+
- TypeScript no-emit typecheck and build passed.
85+
- Workflow policy, version sync, public-surface coverage, secret scan, Python
86+
compileall, and `git diff --check` passed. Surface coverage reported zero
87+
uncovered surfaces and zero uncovered rows.
88+
- The declared CI matrix remains Python 3.9 through 3.14. Only Python 3.14.2
89+
was executable locally; the stdlib runner and guard are 3.9-compatible, but
90+
Python 3.9 through 3.13 still require the remote CI matrix after integration.
91+
- A fresh fetch found `origin/main` unchanged at the base SHA. The branch was
92+
six commits ahead and zero behind, so no rebase was required at that point.
93+
94+
The first independent hostile review returned BLOCK with three findings:
95+
the CONTRIBUTING cwd mutation was not coupled to the command block,
96+
`LIVE-CANARIES.md` contradicted the documented override, and Python 3.11 raw
97+
discovery could pass with zero tests. All three were fixed and covered by
98+
regressions. A focused independent rereview of the exact committed range
99+
`b9e09353..ae4b5fc` then returned `RELEASE`: all three findings were closed and
100+
no remaining material issue was found. CodeRabbit was unavailable locally, so
101+
both hostile reviews used the configured read-only Codex reviewer.
102+
103+
No push, merge, publish, registry mutation, live canary, deployment, or money
104+
change was performed. The old PR #15 worktree and dirty main checkout remain
105+
untouched.

CONTRIBUTING.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pnpm build
1919
# Python
2020
cd ../python
2121
python -m pip install -e .
22-
python -m pytest tests/
22+
python -m unittest discover -s tests -v
2323
```
2424

2525
## Branch + PR workflow
@@ -76,9 +76,11 @@ repo, so the PR title becomes the commit message — make it clean.
7676

7777
Both SDKs have unit tests that mock the HTTP layer:
7878
- TypeScript: `typescript/src/index.test.ts` — runs via `vitest`.
79-
- Python: `python/tests/test_runinfra_sdk.py` — runs via `pytest`.
79+
- Python: `python/tests/test_runinfra_sdk.py` — runs via stdlib `unittest`.
8080

8181
Add a test for any new behavior. Tests must not require network access.
82+
Run Python discovery from `python/`, as shown above, so imports resolve to the
83+
checkout under test instead of an installed package or sibling worktree.
8284

8385
## Releases
8486

LIVE-CANARIES.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,14 @@ satisfy strict promotion.
109109
In the trusted-publish workflow, `build-artifacts` creates the npm tarball,
110110
Python wheel, and Python sdist once and uploads them as
111111
`runinfra-sdk-promoted-artifacts`. `promotion-gate`, `publish-npm`, and
112-
`publish-pypi` download that same artifact bundle. A real publish cannot start
113-
the registry jobs until strict readiness/live reports pass for the downloaded
114-
artifacts and `verify-promotion-reports.mjs` confirms the same source digest
115-
and all-passed rows.
112+
`publish-pypi` download that same artifact bundle. By default, the registry
113+
jobs wait for strict readiness/live reports for those artifacts and for
114+
`verify-promotion-reports.mjs` to confirm the same source digest and all-passed
115+
rows. If live-service canary infrastructure is unavailable, a maintainer may
116+
explicitly dispatch with `require_live_canary=false`. The workflow records the
117+
override, skips only those strict live-canary steps, and still requires build,
118+
unit, packaging, security, trusted-publish, and registry-install gates. A
119+
release using the override is not strict live-canary evidence.
116120

117121
## Required Environment
118122

@@ -194,9 +198,12 @@ canonical live canary matrix exactly,
194198
so a shortened self-consistent report cannot satisfy the gate. The report's
195199
candidate source file count must also match the canonical live-canary source
196200
file manifest.
197-
Promotion evidence must come from strict child canaries against `https://api.runinfra.ai/v1`;
198-
reports generated with any other custom `RUNINFRA_BASE_URL` are useful for
199-
staging smoke tests but cannot satisfy the real publish gate.
201+
Strict live-canary evidence must come from
202+
strict child canaries against `https://api.runinfra.ai/v1`; reports generated with any other custom
203+
`RUNINFRA_BASE_URL` are useful for staging smoke tests but cannot satisfy that
204+
evidence requirement. The infrastructure-unavailable override permits a
205+
registry release without this evidence, but it cannot be used to claim method
206+
or model live readiness.
200207

201208
## Matrix Rows
202209

python/requirements-dev.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
build==1.4.0
2-
pytest==9.0.3
32
typing_extensions==4.15.0
43
twine==6.2.0

typescript/src/index.test.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2329,7 +2329,7 @@ class RunInfra:
23292329
rmSync(tmp, { recursive: true, force: true });
23302330
rmSync(tempRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
23312331
}
2332-
}, 20_000);
2332+
}, 60_000);
23332333

23342334
it("keeps child canaries in parity for audio OpenAI parameter coverage", () => {
23352335
const typescriptCanary = readFileSync(new URL("../../scripts/sdk-live-canary-typescript.mjs", import.meta.url), "utf8");
@@ -2915,6 +2915,13 @@ class RunInfra:
29152915
expect(liveCanaries).toContain("RUNINFRA_ASR_FIXTURE_BASE64");
29162916
expect(liveCanaries).toContain("RUNINFRA_VOICE_PIPELINE_AUDIO_BASE64");
29172917
expect(agentNotes).toContain("`dry_run=false` cannot bypass `promotion-gate`");
2918+
expect(agentNotes).toContain("Registry latest is `0.2.0` on both npm and PyPI");
2919+
expect(agentNotes).toContain("documented `require_live_canary=false` override");
2920+
expect(agentNotes).toContain("Registry availability for 0.2.0 is therefore not strict live-canary evidence.");
2921+
expect(liveCanaries).toContain("explicitly dispatch with `require_live_canary=false`");
2922+
expect(liveCanaries).toContain("release using the override is not strict live-canary evidence");
2923+
expect(liveCanaries).not.toContain("A real publish cannot start");
2924+
expect(liveCanaries).not.toContain("cannot satisfy the real publish gate");
29182925
expect(agentNotes).toContain("Clean artifact install/import now exercises the npm tarball, Python wheel, and");
29192926
expect(agentNotes).toContain("node scripts/verify-github-security-status.mjs --repo RightNow-AI/runinfra-sdk");
29202927
expect(agentNotes).toContain("the publish jobs publish only the downloaded `runinfra-sdk-promoted-artifacts` files");
@@ -3252,13 +3259,51 @@ class RunInfra:
32523259
it("keeps Python test tooling compatible with the declared Python floor", () => {
32533260
const pyproject = readFileSync(new URL("../../python/pyproject.toml", import.meta.url), "utf8");
32543261
const requirements = readFileSync(new URL("../../python/requirements-dev.txt", import.meta.url), "utf8");
3262+
const publish = readFileSync(new URL("../../.github/workflows/publish.yml", import.meta.url), "utf8");
3263+
const ci = readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8");
3264+
const pullRequestTemplate = readFileSync(new URL("../../.github/PULL_REQUEST_TEMPLATE.md", import.meta.url), "utf8");
3265+
const contributing = readFileSync(new URL("../../CONTRIBUTING.md", import.meta.url), "utf8");
3266+
const unittestCommand = "python -m unittest discover -s tests -v";
3267+
const nonzeroDiscoveryGuard = "python -c \"import unittest; suite = unittest.defaultTestLoader.discover('tests'); count = suite.countTestCases(); print(f'Discovered {count} tests'); raise SystemExit(0 if count else 1)\"";
3268+
const normalizedCi = ci.replace(/\r\n/gu, "\n");
3269+
const normalizedPublish = publish.replace(/\r\n/gu, "\n");
3270+
const normalizedContributing = contributing.replace(/\r\n/gu, "\n");
32553271

32563272
expect(pyproject).toContain('requires-python = ">=3.9"');
32573273
expect(pyproject).toContain('requires = ["setuptools==82.0.1"]');
32583274
expect(pyproject).not.toContain("setuptools>=");
3259-
expect(requirements).toContain("pytest==8.4.2");
32603275
expect(requirements).toContain("typing_extensions==4.15.0");
3261-
expect(requirements).not.toMatch(/^pytest==9\./mu);
3276+
expect(requirements).not.toMatch(/^pytest(?:[<=>~!]|$)/mu);
3277+
expect(normalizedCi).toContain([
3278+
" - name: Test",
3279+
" working-directory: python",
3280+
" run: |",
3281+
" python -m pip install -e .",
3282+
` ${nonzeroDiscoveryGuard}`,
3283+
` ${unittestCommand}`,
3284+
].join("\n"));
3285+
expect(normalizedPublish).toContain([
3286+
" - name: Test Python",
3287+
" working-directory: python",
3288+
" run: |",
3289+
" python -m pip install -e .",
3290+
` ${nonzeroDiscoveryGuard}`,
3291+
` ${unittestCommand}`,
3292+
].join("\n"));
3293+
expect(`${publish}\n${ci}`).not.toContain("python -m pytest");
3294+
expect(`${publish}\n${ci}`).not.toContain("-s python/tests");
3295+
expect(pullRequestTemplate).toContain(`from \`python/\`, \`${unittestCommand}\``);
3296+
const contributingPythonBlock = [
3297+
"# Python",
3298+
"cd ../python",
3299+
"python -m pip install -e .",
3300+
unittestCommand,
3301+
].join("\n");
3302+
expect(normalizedContributing).toContain(contributingPythonBlock);
3303+
const wrongCwdContributing = normalizedContributing.replace("cd ../python", "cd ..");
3304+
expect(wrongCwdContributing).not.toBe(normalizedContributing);
3305+
expect(wrongCwdContributing).not.toContain(contributingPythonBlock);
3306+
expect(contributing).toContain("Run Python discovery from `python/`");
32623307
});
32633308

32643309
it("installs Python build tooling before publish workflow TypeScript tests", async () => {
@@ -3310,7 +3355,10 @@ class RunInfra:
33103355
expect(evaluateWorkflowPolicy({ publish: realPublishWithoutDryRunGuard, ci, hasCustomCodeqlWorkflow: false }).find((check) => check.label === label)?.ok)
33113356
.toBe(false);
33123357

3313-
const realPublishWithoutEnvironment = publish.replace(" environment: npm\n", "");
3358+
const realPublishWithoutEnvironment = publish.replace(
3359+
/ environment: npm\r?\n/u,
3360+
"",
3361+
);
33143362
expect(realPublishWithoutEnvironment).not.toBe(publish);
33153363
expect(evaluateWorkflowPolicy({ publish: realPublishWithoutEnvironment, ci, hasCustomCodeqlWorkflow: false }).find((check) => check.label === label)?.ok)
33163364
.toBe(false);

0 commit comments

Comments
 (0)