Move the action from Deno to Node.js and Vite+ (#20)
## Summary
Moves `commons/bot-agents` off Deno. The action's TypeScript now runs directly on the job image's Node.js (`node src/<phase>.ts`, using Node's built-in type stripping), with no build step and still no runtime dependency. Repository checks move to Vite+ (`vp check`, `vp test`).
## What changed
- **Runtime.** `action.yml` runs `prepare.ts`, `run.ts`, and `post.ts` with `node` instead of `deno run -A`. `package.json` (`"type": "module"`, `engines.node >=24.2` for `import.meta.main`) is now copied into the root-owned source copy by `scripts/protect-source.sh`.
- **Profiles.** `profiles.yml` → `profiles.json`, with the same content (encrypted values unchanged), parsed with `JSON.parse`. This drops the only runtime dependency (`@std/yaml`).
- **Deno APIs → Node.**
- `src/command.ts` wraps `child_process.spawn` with the old `Deno.Command` shapes: `output()`/`spawn()`, `clearEnv`, `uid`/`gid`, and Deno's stdio defaults (stdin inherited; stdout/stderr piped for `output()`, inherited for `spawn()`). A signal exit maps to `128 + signo`. Status settles on process exit.
- `src/errors.ts` maps `NotFound` to `ENOENT`/`ESRCH`.
- fs, path, stream, and readline calls are mapped one for one. `makeTempFile` becomes an exclusive `wx` 0600 create.
- **Size refactors.** These are needed for Oxlint's default limits. There are no behavior changes and no overrides for source files.
- `run.ts` is split into `run-context.ts`, `agents.ts`, and `agent-{claude,codex,opencode}.ts`.
- `post.ts` is split into `post.ts` and `post-artifacts.ts`.
- `parseCredentials`, `readOutcome`, and `postRuntime` are broken into helpers.
- Loops that awaited sequentially became recursion or a `reduce` chain, so they stay sequential. `/proc` status reads became `Promise.all`.
- `src/json.ts` adds type guards in place of `as` casts.
- **Tests.** The 58 `Deno.test` cases were converted to Vitest with the codemod, and its TODOs were finished by hand.
- Unit tests are `src/*.test.ts`; the repository-level workflow test is `tests/workflow.test.ts`.
- Root-only tests are now `test.skipIf(!IS_ROOT)`, so they show as skipped instead of silently passing.
- New `tests/entrypoints.test.ts` runs each phase under real Node to prove its whole import graph loads.
- **Config.** The only config files are `package.json`, `tsconfig.json`, and `vite.config.ts`, plus `package-lock.json`.
- Lint uses Oxlint correctness/suspicious/pedantic/perf at error, with default limits and type-aware checks.
- `scripts/project-lint.ts` holds the rule list. It maps Deno's `recommended` rules that fall outside those categories, adds the template's `id-match`, `no-disable-comments`, and `no-non-ts-imports`, and maps Deno's `no-window`/`no-process-global`/`no-node-globals` to `no-restricted-globals` (so `process`/`Buffer` come from `node:` imports).
- **CI.** `.gitea/workflows/check.yml` runs `node --version`, `npm ci`, `npx vp check`, and `npx vp test` in the default runner image.
- **Formatting.** Oxfmt formatting is a separate first commit, so the second commit shows only the semantic diff.
- **AGENTS.md** is updated for the new toolchain.
## Deviations / risks
- **Live `@main` switch.** Six consumer repos use `commons/bot-agents@main`, so merging switches all of them at once. Suggested before merging: tag the current `main` (e.g. `deno-final`) as a rollback target, and canary this branch from one consumer (`uses: …/commons/bot-agents@node-migration`) on a throwaway PR.
- **Node version.** `post.ts` relies on `import.meta.main`, which needs Node ≥ 24.2. DANNYEC's cached `node:24-bookworm` minor is unknown (`forcePull=false`), and the CI job now prints `node --version` to confirm it.
- **Deno stays in the global install.** `deno` remains in the action's global npm install because agents still verify Deno-based consumers (memona, memona-home). Remove it once those repos move.
- **`prefer-readonly-parameter-types` is off.** This pedantic rule is turned off, as in the explicit-types template and the app repos, because Node/web types (`Response`, `Stats`, `RequestInit`) can never satisfy it. `scripts/**` has one narrow override (`no-unsafe-enum-comparison`) for AST node `type` comparisons.
- **Stricter validation of malformed input.** For valid input, behavior is unchanged. A few validations are stricter where the old code would have used a malformed value:
- Gitea `/user` and comment responses are checked.
- The runtime manifest and the outcome's profile type are checked against known values.
- Some type-check errors are now `TypeError` (same messages).
- **@cunarist lint plugins.** "CI fails until @cunarist/eslint-plugin-* are published" does **not** apply here. This repo's Deno setup enabled no plugin, so it depends on none of `@cunarist/eslint-plugin-*`, and CI can pass as is.
## Verification
- `vp check`: formatting, lint (0 warnings), and type check all pass.
- `vp test` as a normal user: 56 passed, 5 root-only skipped.
- `vp test` as root in `node:24-bookworm` (Node v24.22.0, the CI image): all 61 passed, including the UID isolation, process cleanup, and `prepare.ts` cleanup tests.
- End-to-end smoke as root in `node:24-bookworm`, with fake `codex`/`claude`/`npx` binaries and a fake Gitea, through `protect-source.sh` → `prepare.ts` → `run.ts` → `post.ts`:
- Codex hit "usage limit" and fell through to Claude.
- Agents ran as UIDs 20000/20001 without the master key.
- The Codex login was persisted to `/root/.codex/auth.json` (0600).
- The PR review with an inline comment was posted with the reviewer token.
- Runtime credentials were removed and the git author was configured.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #20
Reviewed-by: bot <noreply@capsulizers.com>
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #20.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: migrate-deno-repo
|
||||
description: Checklist for moving a Deno repo (or a leftover Deno part of one) to Node 22+ + Vite+ with npm, same behavior, full lint parity, one PR
|
||||
---
|
||||
|
||||
Template: `cunarist/eslint-plugin-explicit-types` on `main`. Read its
|
||||
`AGENTS.md`, `vite.config.ts`, `scripts/project-lint.ts`, `package.json`, and
|
||||
CI before starting, and copy their shape. Never ask the user mid-way when
|
||||
they're away: decide by the memories, record the choice in the PR.
|
||||
|
||||
Branch `node-migration` (apps) or `initial-port` (new plugin repos); one PR
|
||||
with everything.
|
||||
|
||||
**Inventory first**
|
||||
|
||||
- [ ] List every `deno.json` task, import-map entry, lint rule (tags,
|
||||
includes, `@hugoalh`, custom plugins), test, `Deno.*` call, JSON/CSS/SVG
|
||||
import, and every place the output is consumed (Rust, HTML,
|
||||
Containerfile, CI).
|
||||
|
||||
**Toolchain**
|
||||
|
||||
- [ ] `package.json` in the required key order, `engines.node ">=22"`, no
|
||||
`packageManager`, no `peerDependencies`; `npm install`; commit
|
||||
`package-lock.json`.
|
||||
- [ ] `tsconfig.json` from the template; `vite.config.ts` with `fmt:
|
||||
{ sortPackageJson: false }` and the category-based `lint` block.
|
||||
- [ ] Delete `deno.json`, `deno.lock`, and Deno-only files. `mod.ts` →
|
||||
`index.ts`; `deno.json` imports → `package.json` `imports` (`#x/*`
|
||||
patterns for trailing-slash mappings); `jsr:`/`npm:` → npm packages.
|
||||
|
||||
**Code**
|
||||
|
||||
- [ ] Only `.ts` files; no non-`.ts` imports: CSS via `<link>`, JSON and SVG
|
||||
as `.ts` modules, icons in memona's style.
|
||||
- [ ] `Deno.*` → `node:*` / `process`. Generated JS written as `.ts`, built,
|
||||
gitignored.
|
||||
- [ ] Decorators stay standard with `accessor`; add `scripts/decorators.ts`
|
||||
if the repo has any. Lit repos: `scripts/lit-check.ts` + `npm run
|
||||
lit-check`.
|
||||
|
||||
**Lint**
|
||||
|
||||
- [ ] `scripts/project-lint.ts` copied; old local Deno lint rules ported into
|
||||
it; parity mapping for every former rule; `id-match`; no disable
|
||||
comments; no source `overrides`; refactor size-rule hits.
|
||||
|
||||
**Tests**
|
||||
|
||||
- [ ] `Deno.test` → Vitest (codemod, then finish TODOs); unit tests next to
|
||||
code, integration in `tests/`; no case dropped.
|
||||
|
||||
**Build, ship, CI**
|
||||
|
||||
- [ ] `vp build` in app mode with the same output paths, or `vp pack` for
|
||||
packages. Containerfile node stage on `node:24-alpine`. CI with
|
||||
`setup-vp`: `vp check`, build, `vp test` (+ lit-check). Packages:
|
||||
release-triggered `publish.yml`.
|
||||
- [ ] `AGENTS.md` with the damem header, `.agents/` notes from the damem kit,
|
||||
README updated (Node.js 22+, no Deno instructions). `damem doctor`
|
||||
clean.
|
||||
|
||||
**Finish**
|
||||
|
||||
- [ ] `vp check`, `vp test`, build all pass locally; the app runs.
|
||||
- [ ] Commit in plain imperative sentences; open the PR (summary, changes,
|
||||
parity mapping, deviations/risks, "CI fails until @cunarist/eslint-plugin-*
|
||||
are published" if relevant, verification, footer line).
|
||||
- [ ] Run `review-cycle`. Never merge, publish, or archive.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: review-cycle
|
||||
description: After opening a PR, run review rounds until fully approved; @bot on Capsulizers Gitea repos, an adversarial subagent review posted as a PR comment elsewhere; then hand to the user, never merge
|
||||
---
|
||||
|
||||
Every PR gets reviewed before the user sees it. Repeat rounds until the
|
||||
review is a full approval (cap: 6 rounds; if still not approved, stop and
|
||||
report what remains). Never approve to finish early, and never merge.
|
||||
|
||||
## Capsulizers repos (git.capsulizers.com) with `.gitea/workflows/agents.yml`
|
||||
|
||||
1. Comment `@bot` on the PR through the Gitea API, as the user's own account
|
||||
(credentials from the user's environment, sourced in a subshell, never
|
||||
printed).
|
||||
2. Wait for the review from the `bot` account (poll about every 2 minutes,
|
||||
up to about 30 minutes per round).
|
||||
3. For each point: fix valid ones with commits on the PR branch; reply
|
||||
briefly with reasons to ones that are wrong.
|
||||
4. Re-run `vp check`, the build, and `vp test`, push, and comment `@bot`
|
||||
again. Done when the bot approves.
|
||||
|
||||
## Other repos (GitHub, or Gitea without the bot)
|
||||
|
||||
1. Have a separate subagent review the **full PR diff** adversarially:
|
||||
correctness, behavior changes, missed Deno APIs, broken build paths, lint
|
||||
parity, and violations of the repo's `.agents/memory` rules.
|
||||
2. Post the review as a PR comment headed `Automated review (round N)`,
|
||||
ending with exactly one verdict line: `VERDICT: APPROVED` or
|
||||
`VERDICT: CHANGES REQUESTED`.
|
||||
3. Address every point with commits, re-run checks, push, and reply to the
|
||||
review comment summarizing the fixes.
|
||||
4. Next round reviews from scratch, not just the fixes. Done when a round's
|
||||
verdict is `APPROVED`.
|
||||
|
||||
## After approval
|
||||
|
||||
- Ask the user to review (or, when the user is away, list the PR and any open
|
||||
questions in the report and the questions file). Don't wait idle.
|
||||
- The user merges (squash). Don't merge even after approval unless the user
|
||||
explicitly allowed it for this PR.
|
||||
Reference in New Issue
Block a user