From 6c4e4bf67d2191281e5c6044e6133c4e054915e8 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sun, 18 Oct 2026 23:58:13 +0900 Subject: [PATCH] Move the phase entry points into aliased module folders --- .agents/memory/bot-agents-lint-opt-outs.md | 2 +- .../memory/bot-agents-migration-decisions.md | 2 +- .agents/memory/bot-agents-runtime-no-build.md | 2 +- .agents/memory/bot-agents-tests-root-only.md | 6 ++--- .agents/memory/imports-aliases-barrels.md | 2 +- .agents/memory/toolchain-node-vite-plus.md | 2 +- .gitea/workflows/check.yml | 2 +- AGENTS.md | 10 +++++---- action.yml | 6 ++--- package.json | 3 +++ src/post/index.ts | 22 +++++++++++++++++++ src/{ => post}/post.test.ts | 0 src/{ => post}/post.ts | 16 -------------- src/{prepare.ts => prepare/index.ts} | 0 src/{run.ts => run/index.ts} | 2 +- src/runtime/runtime.test.ts | 2 +- tests/entrypoints.test.ts | 4 ++-- tests/workflow.test.ts | 12 +++++----- 18 files changed, 53 insertions(+), 42 deletions(-) create mode 100644 src/post/index.ts rename src/{ => post}/post.test.ts (100%) rename src/{ => post}/post.ts (94%) rename src/{prepare.ts => prepare/index.ts} (100%) rename src/{run.ts => run/index.ts} (99%) diff --git a/.agents/memory/bot-agents-lint-opt-outs.md b/.agents/memory/bot-agents-lint-opt-outs.md index c80e8d5..309ab3d 100644 --- a/.agents/memory/bot-agents-lint-opt-outs.md +++ b/.agents/memory/bot-agents-lint-opt-outs.md @@ -15,7 +15,7 @@ when the migration PR was approved: ([[lint-overrides-policy]]). Size and complexity limits match Deno, which had none here, so every Oxlint -limit rule is `"off"` ([[lint-config-shape]]). `run.ts` and `post.ts` keep +limit rule is `"off"` ([[lint-config-shape]]). `src/run/index.ts` and `src/post/index.ts` keep main's single-file layout (2026-10-17 revert of the size splits). Sequential awaits in loops became recursion or a `reduce` chain (`no-await-in-loop`) to stay sequential. Add no other opt-out without the user. diff --git a/.agents/memory/bot-agents-migration-decisions.md b/.agents/memory/bot-agents-migration-decisions.md index 45d16b8..463da32 100644 --- a/.agents/memory/bot-agents-migration-decisions.md +++ b/.agents/memory/bot-agents-migration-decisions.md @@ -15,7 +15,7 @@ Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16): removed the only runtime dependency (`@std/yaml`); keep it free of runtime dependencies (`@eslint/core` and the other devDependencies serve only the tooling). -- **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. There is no +- **Node 24.2+**, not 22: `src/post/index.ts` uses `import.meta.main`. There is no `engines` (user decision 2026-10-18): the `node:24-bookworm` job image decides the version ([[bot-agents-runtime-no-build]]). - **Published lint plugins** (2026-10-18, after they reached npm): diff --git a/.agents/memory/bot-agents-runtime-no-build.md b/.agents/memory/bot-agents-runtime-no-build.md index fdb1700..0430580 100644 --- a/.agents/memory/bot-agents-runtime-no-build.md +++ b/.agents/memory/bot-agents-runtime-no-build.md @@ -10,7 +10,7 @@ dependency**. Hence `tsconfig.json` has `erasableSyntaxOnly` and properties, no decorators) and relative `.ts` imports. - **Image:** the steps assume the `node:24-bookworm` job container. Node must - be 24.2+ for `import.meta.main` in `post.ts`. That image is the only Node + be 24.2+ for `import.meta.main` in `src/post/index.ts`. That image is the only Node pin (`package.json` has no `engines`); `check.yml` prints `node --version` to confirm the cached image's minor. - **Root-owned source copy:** `scripts/protect-source.sh` copies the action diff --git a/.agents/memory/bot-agents-tests-root-only.md b/.agents/memory/bot-agents-tests-root-only.md index c0600a4..8ca01ee 100644 --- a/.agents/memory/bot-agents-tests-root-only.md +++ b/.agents/memory/bot-agents-tests-root-only.md @@ -2,20 +2,20 @@ description: Isolation and credential tests need root and use test.skipIf(!IS_ROOT); locally 5 skip, CI runs as root so all 66 run; verify isolation changes in node:24-bookworm as root --- -Some tests exercise real UID isolation, process cleanup, and `prepare.ts` +Some tests exercise real UID isolation, process cleanup, and the prepare phase's credential cleanup. They need root and are written as `test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `#testing`), so they show as skipped instead of silently passing. - As a normal user `vp test` passes 61 and skips 5. In CI (self-hosted runner, root) all 66 run. -- When changing `isolation.ts`, `runtime.ts`, `credentials.ts`, `prepare.ts`, +- When changing `isolation.ts`, `runtime.ts`, `credentials.ts`, `prepare/index.ts`, or `child.ts`, run the suite as root in `node:24-bookworm` (e.g. a container with the repo mounted) before pushing; a green non-root run proves nothing about those paths. - The migration also ran an end-to-end smoke as root with fake `codex`/`claude`/`npx` binaries and a fake Gitea through - `protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that + `protect-source.sh` -> `prepare/index.ts` -> `run/index.ts` -> `post/index.ts`; repeat that for changes to the phase flow ([[bot-agents-runtime-no-build]]). - `src/testing/index.ts` holds the shared test helpers (user IDs, temp dirs, `runCommand`) behind the `#testing` alias. Every test imports it as diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index a355cbf..3a944f4 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -97,7 +97,7 @@ It is not a plugin rule. - Every `package.json` `imports` alias points only at the `index.ts` of a direct subfolder of `src/`, e.g. `"#helpers": "./src/helpers/index.ts"`. No alias to a single file, a deeper folder, or anything outside `src/`. - Here the aliases are `#agents`, `#processes`, `#runtime`, `#profiles`, + Here the aliases are `#prepare`, `#run`, `#post`, `#agents`, `#processes`, `#runtime`, `#profiles`, `#common`, and `#testing`, in that layer order. - Code outside `src/` (`scripts/`, `tests/`, `vite.config.ts`) gets no alias of its own and imports its own files by `./` relative paths. It reaches diff --git a/.agents/memory/toolchain-node-vite-plus.md b/.agents/memory/toolchain-node-vite-plus.md index fc16cb4..8fc0c63 100644 --- a/.agents/memory/toolchain-node-vite-plus.md +++ b/.agents/memory/toolchain-node-vite-plus.md @@ -14,7 +14,7 @@ description: Node.js (no engines in apps; images pin 24) with Vite+ (vp) and npm `engines` or a `.node-version`, local `vp` uses its default Node (`vp env default`). bot-agents has no Containerfile or setup step: the self-hosted runner's `node:24-bookworm` job image runs the action and its - checks, and `src/post.ts` needs 24.2+ (`import.meta.main`). + checks, and `src/post/index.ts` needs 24.2+ (`import.meta.main`). - **Bun is not supported** (dropped 2026-10-15). No Bun CI legs, docs, or runtime branches. Oxlint's `RuleTester` refuses to run under Bun anyway, and `vp` brings its own Node, the way uv brings its own Python. diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index eef9b0f..e1d9015 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - # The action runs on this same image; src/post.ts needs Node 24.2+. + # The action runs on this same image; src/post/index.ts needs Node 24.2+. - run: node --version - run: npm ci diff --git a/AGENTS.md b/AGENTS.md index a60b245..ec3bf99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,13 +34,15 @@ import is banned in every file, owned or not (`import/no-relative-parent-imports`); runtime file URLs such as `new URL("../prompt.md", import.meta.url)` are paths, not imports, and stay. Files no alias owns need no `index.ts` and no alias, and import `./` siblings -and subfolders freely. The aliases, in layer order, are `#agents` (Codex, +and subfolders freely. Every top-level entry of `src/` is an aliased module +folder. The aliases, in layer order, are the three phases `#prepare`, `#run`, +and `#post`, then `#agents` (Codex, Claude Code and OpenCode specifics), `#processes` (child processes and the isolated profile users), `#runtime` (the prepared runtime and its credentials), `#profiles`, `#common` (error and JSON checks), and `#testing` -(test helpers), each a `src//index.ts` barrel. The phase entry points -`src/prepare.ts`, `src/run.ts`, and `src/post.ts` stay at the top of `src/`, -unowned, because `action.yml` runs them by path. Tests that use the test +(test helpers), each with a `src//index.ts` barrel. A phase's +`index.ts` is also its entry point, which `action.yml` runs as +`node src//index.ts`. Tests that use the test helpers import `#testing`, and `tests/` imports `src/` only through it. Local convention, stricter than the general rule above: in this repo, aliases point only at a direct subfolder of `src/` (`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses `./` diff --git a/action.yml b/action.yml index 437d00c..a473e93 100644 --- a/action.yml +++ b/action.yml @@ -35,7 +35,7 @@ runs: - name: Prepare credentials id: prepare shell: bash - run: node "${{ steps.source.outputs.path }}/src/prepare.ts" + run: node "${{ steps.source.outputs.path }}/src/prepare/index.ts" env: ACTION_PATH: ${{ steps.source.outputs.path }} BOT_AGENTS_MASTER_KEY: ${{ inputs.master-key }} @@ -43,7 +43,7 @@ runs: - name: Run bot id: agent shell: bash - run: node "${{ steps.source.outputs.path }}/src/run.ts" + run: node "${{ steps.source.outputs.path }}/src/run/index.ts" env: BOT_AGENTS_MANIFEST: ${{ steps.prepare.outputs.manifest }} GITEA_TOKEN: ${{ gitea.token }} @@ -56,7 +56,7 @@ runs: id: post if: always() && steps.prepare.outputs.manifest != '' shell: bash - run: node "${{ steps.source.outputs.path }}/src/post.ts" + run: node "${{ steps.source.outputs.path }}/src/post/index.ts" env: BOT_AGENTS_MANIFEST: ${{ steps.prepare.outputs.manifest }} GITEA_API_URL: ${{ gitea.api_url }} diff --git a/package.json b/package.json index 962022b..d73313c 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.", "type": "module", "imports": { + "#prepare": "./src/prepare/index.ts", + "#run": "./src/run/index.ts", + "#post": "./src/post/index.ts", "#agents": "./src/agents/index.ts", "#processes": "./src/processes/index.ts", "#runtime": "./src/runtime/index.ts", diff --git a/src/post/index.ts b/src/post/index.ts new file mode 100644 index 0000000..dedc16b --- /dev/null +++ b/src/post/index.ts @@ -0,0 +1,22 @@ +// The post phase: posts the agent's result as the reviewer and removes the +// runtime credentials. +import process from "node:process"; + +import { postRuntime } from "./post.ts"; + +export { type Fetcher, type PostContext, postRuntime } from "./post.ts"; + +function required(name: string): string { + const value = process.env[name]; + if (value === undefined || value === "") throw new Error(`${name} is not set`); + return value; +} + +if (import.meta.main) { + await postRuntime(required("BOT_AGENTS_MANIFEST"), { + apiUrl: required("GITEA_API_URL"), + repository: required("GITEA_REPOSITORY"), + issueIndex: required("ISSUE_INDEX"), + eventName: required("EVENT_NAME"), + }); +} diff --git a/src/post.test.ts b/src/post/post.test.ts similarity index 100% rename from src/post.test.ts rename to src/post/post.test.ts diff --git a/src/post.ts b/src/post/post.ts similarity index 94% rename from src/post.ts rename to src/post/post.ts index d86f52d..324b351 100644 --- a/src/post.ts +++ b/src/post/post.ts @@ -1,7 +1,6 @@ import type { Stats } from "node:fs"; import { lstat, readFile, rm } from "node:fs/promises"; import { join } from "node:path"; -import process from "node:process"; import { isNotFound, isObject } from "#common"; import { failureFooter, type FailureReason, type ProfileFailure, withProfile } from "#profiles"; @@ -223,18 +222,3 @@ export async function postRuntime( }); } } - -function required(name: string): string { - const value = process.env[name]; - if (value === undefined || value === "") throw new Error(`${name} is not set`); - return value; -} - -if (import.meta.main) { - await postRuntime(required("BOT_AGENTS_MANIFEST"), { - apiUrl: required("GITEA_API_URL"), - repository: required("GITEA_REPOSITORY"), - issueIndex: required("ISSUE_INDEX"), - eventName: required("EVENT_NAME"), - }); -} diff --git a/src/prepare.ts b/src/prepare/index.ts similarity index 100% rename from src/prepare.ts rename to src/prepare/index.ts diff --git a/src/run.ts b/src/run/index.ts similarity index 99% rename from src/run.ts rename to src/run/index.ts index 3074bec..a4820e4 100644 --- a/src/run.ts +++ b/src/run/index.ts @@ -184,7 +184,7 @@ async function renderPrompt(profile: RuntimeProfile): Promise { REVIEW_PATH: reviewPath, ANCHORS_PATH: anchorsPath, }; - const template = await readFile(new URL("../prompt.md", import.meta.url), "utf8"); + const template = await readFile(new URL("../../prompt.md", import.meta.url), "utf8"); return { prompt: template.replaceAll( /\$\{(\w+)\}/gu, diff --git a/src/runtime/runtime.test.ts b/src/runtime/runtime.test.ts index 4f282e1..08a6d70 100644 --- a/src/runtime/runtime.test.ts +++ b/src/runtime/runtime.test.ts @@ -81,7 +81,7 @@ test.skipIf(!IS_ROOT)("removes prepared credentials when output publication fail ); const result = spawnSync( process.execPath, - [fileURLToPath(new URL("../prepare.ts", import.meta.url))], + [fileURLToPath(new URL("../prepare/index.ts", import.meta.url))], { env: { ACTION_PATH: action, diff --git a/tests/entrypoints.test.ts b/tests/entrypoints.test.ts index 9f7e3a1..8f33d95 100644 --- a/tests/entrypoints.test.ts +++ b/tests/entrypoints.test.ts @@ -4,7 +4,7 @@ import { expect, test } from "vite-plus/test"; import { runCommand } from "#testing"; -// Each phase runs as `node src/.ts` with no build step. Node loads the +// Each phase runs as `node src//index.ts` with no build step. Node loads the // whole import graph, stripping types, before the phase reads its first // variable, so failing on that variable proves every module loads. const PHASES: [phase: string, firstVariable: string][] = [ @@ -14,7 +14,7 @@ const PHASES: [phase: string, firstVariable: string][] = [ ]; test.each(PHASES)("Node loads the %s phase", async (phase, firstVariable) => { - const path = fileURLToPath(new URL(`../src/${phase}.ts`, import.meta.url)); + const path = fileURLToPath(new URL(`../src/${phase}/index.ts`, import.meta.url)); const result = await runCommand(process.execPath, [path], { PATH: process.env["PATH"] ?? "" }); expect(result.success).toBe(false); expect(result.stderr).toContain(`${firstVariable} is not set`); diff --git a/tests/workflow.test.ts b/tests/workflow.test.ts index 363f036..0028e2f 100644 --- a/tests/workflow.test.ts +++ b/tests/workflow.test.ts @@ -86,9 +86,9 @@ test("the source protection script stages immutable action files", async () => { const path = (await readFile(outputFile, "utf8")).trim().slice(5); const files = [ - "src/prepare.ts", - "src/run.ts", - "src/post.ts", + "src/prepare/index.ts", + "src/run/index.ts", + "src/post/index.ts", "src/runtime/index.ts", "src/agents/auth.ts", "package.json", @@ -106,7 +106,7 @@ test("the source protection script stages immutable action files", async () => { }); test("module-relative runtime assets exist", async () => { - const module = new URL("../src/run.ts", import.meta.url); + const module = new URL("../src/run/index.ts", import.meta.url); const source = await readFile(module, "utf8"); const assets = [...source.matchAll(/new URL\("([^"]+)", import\.meta\.url\)/gu)].map( ([, asset = ""]) => new URL(asset, module), @@ -149,7 +149,7 @@ test("CI checks every phase and the root isolation suite", async () => { }); test("uses the standard Skills installer without custom discovery", async () => { - const runner = await readFile("src/run.ts", "utf8"); + const runner = await readFile("src/run/index.ts", "utf8"); assert(runner.includes("skills add obra/superpowers")); const removed = await stat("superpowers.ts").then( () => false, @@ -169,7 +169,7 @@ test("does not install Deno", async () => { }); test("profile preparation does not receive provider credentials", async () => { - const runner = await readFile("src/run.ts", "utf8"); + const runner = await readFile("src/run/index.ts", "utf8"); assert(runner.includes("await prepareProfile(profile);")); expect(runner.includes("prepareProfile(profile, token)")).toBe(false); });