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]]).
|
([[lint-overrides-policy]]).
|
||||||
|
|
||||||
Size and complexity limits match Deno, which had none here, so every Oxlint
|
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
|
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
|
awaits in loops became recursion or a `reduce` chain (`no-await-in-loop`) to
|
||||||
stay sequential. Add no other opt-out without the user.
|
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
|
removed the only runtime dependency (`@std/yaml`); keep it free of runtime
|
||||||
dependencies (`@eslint/core` and the other devDependencies serve only the
|
dependencies (`@eslint/core` and the other devDependencies serve only the
|
||||||
tooling).
|
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
|
`engines` (user decision 2026-10-18): the `node:24-bookworm` job image
|
||||||
decides the version ([[bot-agents-runtime-no-build]]).
|
decides the version ([[bot-agents-runtime-no-build]]).
|
||||||
- **Published lint plugins** (2026-10-18, after they reached npm):
|
- **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.
|
properties, no decorators) and relative `.ts` imports.
|
||||||
|
|
||||||
- **Image:** the steps assume the `node:24-bookworm` job container. Node must
|
- **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`
|
pin (`package.json` has no `engines`); `check.yml` prints `node --version`
|
||||||
to confirm the cached image's minor.
|
to confirm the cached image's minor.
|
||||||
- **Root-owned source copy:** `scripts/protect-source.sh` copies the action
|
- **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
|
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
|
credential cleanup. They need root and are written as
|
||||||
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `#testing`), so they show
|
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `#testing`), so they show
|
||||||
as skipped instead of silently passing.
|
as skipped instead of silently passing.
|
||||||
|
|
||||||
- As a normal user `vp test` passes 61 and skips 5. In CI (self-hosted
|
- As a normal user `vp test` passes 61 and skips 5. In CI (self-hosted
|
||||||
runner, root) all 66 run.
|
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
|
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
|
container with the repo mounted) before pushing; a green non-root run
|
||||||
proves nothing about those paths.
|
proves nothing about those paths.
|
||||||
- The migration also ran an end-to-end smoke as root with fake
|
- The migration also ran an end-to-end smoke as root with fake
|
||||||
`codex`/`claude`/`npx` binaries and a fake Gitea through
|
`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]]).
|
for changes to the phase flow ([[bot-agents-runtime-no-build]]).
|
||||||
- `src/testing/index.ts` holds the shared test helpers (user IDs, temp dirs,
|
- `src/testing/index.ts` holds the shared test helpers (user IDs, temp dirs,
|
||||||
`runCommand`) behind the `#testing` alias. Every test imports it as
|
`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
|
- Every `package.json` `imports` alias points only at the `index.ts` of a
|
||||||
direct subfolder of `src/`, e.g. `"#helpers": "./src/helpers/index.ts"`.
|
direct subfolder of `src/`, e.g. `"#helpers": "./src/helpers/index.ts"`.
|
||||||
No alias to a single file, a deeper folder, or anything outside `src/`.
|
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.
|
`#common`, and `#testing`, in that layer order.
|
||||||
- Code outside `src/` (`scripts/`, `tests/`, `vite.config.ts`) gets no alias
|
- Code outside `src/` (`scripts/`, `tests/`, `vite.config.ts`) gets no alias
|
||||||
of its own and imports its own files by `./` relative paths. It reaches
|
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
|
`engines` or a `.node-version`, local `vp` uses its default Node
|
||||||
(`vp env default`). bot-agents has no Containerfile or setup step: the
|
(`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
|
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
|
- **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
|
runtime branches. Oxlint's `RuleTester` refuses to run under Bun anyway, and
|
||||||
`vp` brings its own Node, the way uv brings its own Python.
|
`vp` brings its own Node, the way uv brings its own Python.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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: node --version
|
||||||
|
|
||||||
- run: npm ci
|
- 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
|
(`import/no-relative-parent-imports`); runtime file URLs such as
|
||||||
`new URL("../prompt.md", import.meta.url)` are paths, not imports, and stay.
|
`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
|
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
|
Claude Code and OpenCode specifics), `#processes` (child processes and the
|
||||||
isolated profile users), `#runtime` (the prepared runtime and its
|
isolated profile users), `#runtime` (the prepared runtime and its
|
||||||
credentials), `#profiles`, `#common` (error and JSON checks), and `#testing`
|
credentials), `#profiles`, `#common` (error and JSON checks), and `#testing`
|
||||||
(test helpers), each a `src/<name>/index.ts` barrel. The phase entry points
|
(test helpers), each with a `src/<name>/index.ts` barrel. A phase's
|
||||||
`src/prepare.ts`, `src/run.ts`, and `src/post.ts` stay at the top of `src/`,
|
`index.ts` is also its entry point, which `action.yml` runs as
|
||||||
unowned, because `action.yml` runs them by path. Tests that use the test
|
`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
|
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/`
|
this repo, aliases point only at a direct subfolder of `src/`
|
||||||
(`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses `./`
|
(`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses `./`
|
||||||
|
|||||||
+3
-3
@@ -35,7 +35,7 @@ runs:
|
|||||||
- name: Prepare credentials
|
- name: Prepare credentials
|
||||||
id: prepare
|
id: prepare
|
||||||
shell: bash
|
shell: bash
|
||||||
run: node "${{ steps.source.outputs.path }}/src/prepare.ts"
|
run: node "${{ steps.source.outputs.path }}/src/prepare/index.ts"
|
||||||
env:
|
env:
|
||||||
ACTION_PATH: ${{ steps.source.outputs.path }}
|
ACTION_PATH: ${{ steps.source.outputs.path }}
|
||||||
BOT_AGENTS_MASTER_KEY: ${{ inputs.master-key }}
|
BOT_AGENTS_MASTER_KEY: ${{ inputs.master-key }}
|
||||||
@@ -43,7 +43,7 @@ runs:
|
|||||||
- name: Run bot
|
- name: Run bot
|
||||||
id: agent
|
id: agent
|
||||||
shell: bash
|
shell: bash
|
||||||
run: node "${{ steps.source.outputs.path }}/src/run.ts"
|
run: node "${{ steps.source.outputs.path }}/src/run/index.ts"
|
||||||
env:
|
env:
|
||||||
BOT_AGENTS_MANIFEST: ${{ steps.prepare.outputs.manifest }}
|
BOT_AGENTS_MANIFEST: ${{ steps.prepare.outputs.manifest }}
|
||||||
GITEA_TOKEN: ${{ gitea.token }}
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
@@ -56,7 +56,7 @@ runs:
|
|||||||
id: post
|
id: post
|
||||||
if: always() && steps.prepare.outputs.manifest != ''
|
if: always() && steps.prepare.outputs.manifest != ''
|
||||||
shell: bash
|
shell: bash
|
||||||
run: node "${{ steps.source.outputs.path }}/src/post.ts"
|
run: node "${{ steps.source.outputs.path }}/src/post/index.ts"
|
||||||
env:
|
env:
|
||||||
BOT_AGENTS_MANIFEST: ${{ steps.prepare.outputs.manifest }}
|
BOT_AGENTS_MANIFEST: ${{ steps.prepare.outputs.manifest }}
|
||||||
GITEA_API_URL: ${{ gitea.api_url }}
|
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.",
|
"description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"#prepare": "./src/prepare/index.ts",
|
||||||
|
"#run": "./src/run/index.ts",
|
||||||
|
"#post": "./src/post/index.ts",
|
||||||
"#agents": "./src/agents/index.ts",
|
"#agents": "./src/agents/index.ts",
|
||||||
"#processes": "./src/processes/index.ts",
|
"#processes": "./src/processes/index.ts",
|
||||||
"#runtime": "./src/runtime/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 type { Stats } from "node:fs";
|
||||||
import { lstat, readFile, rm } from "node:fs/promises";
|
import { lstat, readFile, rm } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import process from "node:process";
|
|
||||||
|
|
||||||
import { isNotFound, isObject } from "#common";
|
import { isNotFound, isObject } from "#common";
|
||||||
import { failureFooter, type FailureReason, type ProfileFailure, withProfile } from "#profiles";
|
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,
|
REVIEW_PATH: reviewPath,
|
||||||
ANCHORS_PATH: anchorsPath,
|
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 {
|
return {
|
||||||
prompt: template.replaceAll(
|
prompt: template.replaceAll(
|
||||||
/\$\{(\w+)\}/gu,
|
/\$\{(\w+)\}/gu,
|
||||||
@@ -81,7 +81,7 @@ test.skipIf(!IS_ROOT)("removes prepared credentials when output publication fail
|
|||||||
);
|
);
|
||||||
const result = spawnSync(
|
const result = spawnSync(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
[fileURLToPath(new URL("../prepare.ts", import.meta.url))],
|
[fileURLToPath(new URL("../prepare/index.ts", import.meta.url))],
|
||||||
{
|
{
|
||||||
env: {
|
env: {
|
||||||
ACTION_PATH: action,
|
ACTION_PATH: action,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { expect, test } from "vite-plus/test";
|
|||||||
|
|
||||||
import { runCommand } from "#testing";
|
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
|
// whole import graph, stripping types, before the phase reads its first
|
||||||
// variable, so failing on that variable proves every module loads.
|
// variable, so failing on that variable proves every module loads.
|
||||||
const PHASES: [phase: string, firstVariable: string][] = [
|
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) => {
|
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"] ?? "" });
|
const result = await runCommand(process.execPath, [path], { PATH: process.env["PATH"] ?? "" });
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.stderr).toContain(`${firstVariable} is not set`);
|
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 path = (await readFile(outputFile, "utf8")).trim().slice(5);
|
||||||
const files = [
|
const files = [
|
||||||
"src/prepare.ts",
|
"src/prepare/index.ts",
|
||||||
"src/run.ts",
|
"src/run/index.ts",
|
||||||
"src/post.ts",
|
"src/post/index.ts",
|
||||||
"src/runtime/index.ts",
|
"src/runtime/index.ts",
|
||||||
"src/agents/auth.ts",
|
"src/agents/auth.ts",
|
||||||
"package.json",
|
"package.json",
|
||||||
@@ -106,7 +106,7 @@ test("the source protection script stages immutable action files", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("module-relative runtime assets exist", 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 source = await readFile(module, "utf8");
|
||||||
const assets = [...source.matchAll(/new URL\("([^"]+)", import\.meta\.url\)/gu)].map(
|
const assets = [...source.matchAll(/new URL\("([^"]+)", import\.meta\.url\)/gu)].map(
|
||||||
([, asset = ""]) => new URL(asset, module),
|
([, 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 () => {
|
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"));
|
assert(runner.includes("skills add obra/superpowers"));
|
||||||
const removed = await stat("superpowers.ts").then(
|
const removed = await stat("superpowers.ts").then(
|
||||||
() => false,
|
() => false,
|
||||||
@@ -169,7 +169,7 @@ test("does not install Deno", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("profile preparation does not receive provider credentials", 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);"));
|
assert(runner.includes("await prepareProfile(profile);"));
|
||||||
expect(runner.includes("prepareProfile(profile, token)")).toBe(false);
|
expect(runner.includes("prepareProfile(profile, token)")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user