Move the phase entry points into aliased module folders
Check / check (pull_request) Successful in 46s
Check / check (pull_request) Successful in 46s
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/<name>/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/<name>/index.ts` barrel. A phase's
|
||||
`index.ts` is also its entry point, which `action.yml` runs as
|
||||
`node src/<phase>/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 `./`
|
||||
|
||||
+3
-3
@@ -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 }}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
@@ -184,7 +184,7 @@ async function renderPrompt(profile: RuntimeProfile): Promise<RenderedPrompt> {
|
||||
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,
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { expect, test } from "vite-plus/test";
|
||||
|
||||
import { runCommand } from "#testing";
|
||||
|
||||
// Each phase runs as `node src/<phase>.ts` with no build step. Node loads the
|
||||
// Each phase runs as `node src/<phase>/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`);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user