Skip to content
Open
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
19 changes: 19 additions & 0 deletions .github/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# changes

## Gate the inherited model-catalog publisher behind fork opt-in (2026-08-20)

### What changed

- `.github/workflows/publish-model-catalog.yml` now schedules the `publish` job only when the repository variable `PI_MODEL_CATALOG_PUBLISH` is exactly `true`, in addition to its existing event and manual-publish conditions.

### Why

- This fork inherited upstream's R2 endpoint and bucket without either required `PI_ARTIFACTS_R2_*` secret. Every upload attempt therefore exported empty AWS credentials and failed with `Unable to locate credentials`, while generation and validation stayed green.
- Keeping publishing opt-in makes the fork's default workflow honest: catalog generation still runs and is validated, but the unavailable upstream infrastructure no longer creates recurring default-branch failures. A maintainer can restore uploads after configuring a fork-owned R2 target, both secrets, and the opt-in variable.

### Why an extension could not handle it

- GitHub evaluates the job graph, repository variables, and environment secrets before any Senpi runtime or extension is available.

### Expected merge conflict zones

- MEDIUM: the `publish` job condition in `.github/workflows/publish-model-catalog.yml`, because upstream may continue changing its publication events or credential contract. Preserve the fork opt-in unless the fork owns a working publication target.

## Changelog-gate labels and base SHA move to env (2026-08-17)

### What changed
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-model-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
retention-days: 14

publish:
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && inputs.publish) }}
if: ${{ vars.PI_MODEL_CATALOG_PUBLISH == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && inputs.publish)) }}
needs: generate
runs-on: ubuntu-latest
environment: pi-model-upload
Expand Down
16 changes: 10 additions & 6 deletions packages/coding-agent/src/valid-cwd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { homedir } from "node:os";
// process: Node boots with the stale handle and only throws `uv_cwd` when something evaluates
// process.cwd(). The bundled agent SDK does that during module evaluation, before any user code
// can recover, so the guard must run before every other import - keep it dependency-free.
try {
process.cwd();
} catch {
const fallback = homedir();
process.chdir(fallback);
console.error(`the current working directory no longer exists; continuing from ${fallback}`);
export function recoverValidCwd(): void {
try {
process.cwd();
} catch {
const fallback = homedir();
process.chdir(fallback);
console.error(`the current working directory no longer exists; continuing from ${fallback}`);
}
}

recoverValidCwd();
20 changes: 16 additions & 4 deletions packages/coding-agent/test/valid-cwd-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,28 @@ import { describe, expect, test } from "vitest";
* recover. These tests reproduce that exact shape in a child process and pin the recovery guard.
*/

// Both modules are linked while the cwd is still valid: on macOS the ESM loader
// itself wedges when a dynamic import starts after the cwd was unlinked, which
// would hang the child instead of exercising the guard. Awaiting the imports is
// the completion event - no polling, no sleeps - and the deletion plus the
// guard/probe calls then run synchronously in the deleted-cwd state.
const driverSource = `import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const guard = process.env.WITH_GUARD === "1" ? await import(process.env.GUARD_URL) : undefined;
const probe = await import(process.env.PROBE_URL);
const dir = mkdtempSync(join(tmpdir(), "senpi-cwd-guard-"));
process.chdir(dir);
rmSync(dir, { recursive: true, force: true });
if (process.env.WITH_GUARD === "1") await import(process.env.GUARD_URL);
await import(process.env.PROBE_URL);
guard?.recoverValidCwd();
probe.resolveCwdLikeBundledSdk();
console.log("CWD_OK=" + process.cwd());
`;

const probeSource = `// Mimics the bundled agent SDK: resolves the cwd during module evaluation.
process.cwd();
const probeSource = `// Mimics the bundled agent SDK: resolves the cwd the moment it runs.
export function resolveCwdLikeBundledSdk() {
process.cwd();
}
`;

function runChild(withGuard: boolean) {
Expand All @@ -35,6 +44,9 @@ function runChild(withGuard: boolean) {
writeFileSync(probe, probeSource);
return spawnSync(process.execPath, [driver], {
encoding: "utf8",
// Deadlock backstop only; the driver's awaited imports are the real
// synchronization, so a healthy child never approaches this bound.
timeout: 30_000,
env: {
...process.env,
WITH_GUARD: withGuard ? "1" : "0",
Expand Down
Loading