mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
Merge pull request #81 from morluto/ci/conventional-release-commits
ci: enforce conventional pull request titles
This commit is contained in:
10 files changed
+239
-9
No files matched your search
@@ -7,6 +7,22 @@ on:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
pr-title:
|
||||
name: Conventional PR title
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate title for Release Please
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
run: >-
|
||||
node -e 'const title = process.env.PR_TITLE ?? "";
|
||||
const conventional = /^(?:feat|fix|docs|refactor|perf|test|build|ci|chore|revert)(?:\([a-z0-9._/-]+\))?!?: .+/u;
|
||||
if (!conventional.test(title)) {
|
||||
console.error("PR title must use Conventional Commits, for example: feat: add historical source import");
|
||||
process.exit(1);
|
||||
}'
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
@@ -98,6 +114,23 @@ jobs:
|
||||
path: test-results.xml
|
||||
retention-days: 30
|
||||
|
||||
package-e2e:
|
||||
name: Package E2E (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-14]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run rebuild:native
|
||||
- run: npm run verify:package
|
||||
|
||||
docs:
|
||||
name: Docs Generation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Real Hopper E2E
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
first_target:
|
||||
description: Absolute path to first test binary on the runner
|
||||
required: true
|
||||
type: string
|
||||
second_target:
|
||||
description: Absolute path to distinct second test binary on the runner
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Installed package against real Hopper
|
||||
runs-on: [self-hosted, macOS, hopper]
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run rebuild:native
|
||||
- name: Install packed CLI in isolation
|
||||
shell: bash
|
||||
run: |
|
||||
tarball="$(npm pack --silent)"
|
||||
prefix="${RUNNER_TEMP}/rea-prefix"
|
||||
npm install --global --prefix "${prefix}" "./${tarball}"
|
||||
echo "REA_VERIFY_SERVER_COMMAND=${prefix}/bin/rea" >> "${GITHUB_ENV}"
|
||||
echo 'REA_VERIFY_SERVER_ARGS_JSON=["mcp"]' >> "${GITHUB_ENV}"
|
||||
- name: Verify two real binaries
|
||||
env:
|
||||
HOPPER_TARGET_PATH: ${{ inputs.first_target }}
|
||||
HOPPER_SECOND_TARGET_PATH: ${{ inputs.second_target }}
|
||||
run: npm run verify:hopper
|
||||
@@ -43,3 +43,19 @@ jobs:
|
||||
|
||||
- name: Publish
|
||||
run: npm publish --access public
|
||||
|
||||
- name: Verify published CLI from npm
|
||||
shell: bash
|
||||
run: |
|
||||
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
if npm view "rea-agents@${version}" version --json >/dev/null 2>&1; then
|
||||
npx --yes --package "rea-agents@${version}" rea --help
|
||||
npx --yes --package "rea-agents@${version}" rea --llms
|
||||
npm run verify:published -- "${version}"
|
||||
exit 0
|
||||
fi
|
||||
if [ "${attempt}" -lt 6 ]; then sleep 10; fi
|
||||
done
|
||||
echo "Published rea-agents@${version} was not available from npm" >&2
|
||||
exit 1
|
||||
@@ -11,7 +11,7 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec
|
||||
- `src/cli.ts`: one-shot CLI adapter for setup, diagnostics, analysis, and decompilation.
|
||||
- `src/config.ts`: Zod-validated parsing of environment configuration into `AppConfig`.
|
||||
- `src/domain/`: pure, side-effect-free modules. `errors.ts` (tagged error algebra), `result.ts` (`Result`/`ok`/`err`), `hopperValues.ts` (boundary parsers for Hopper JSON), `symbolAnalysis.ts` (Swift/ObjC name parsing).
|
||||
- `src/contracts/`: caller-visible schemas for 31 direct, 8 enhanced, and 3 session tools; `enhancedInputs.ts` owns enhanced input parsing.
|
||||
- `src/contracts/`: caller-visible schemas for 33 direct, 10 enhanced, 5 native, 2 artifact, and 18 session tools; `enhancedInputs.ts` owns enhanced input parsing.
|
||||
- `src/hopper/`: Hopper launch and Unix-socket protocol mechanics. `BridgeLauncher.ts` spawns the Hopper app with the in-process bridge, `HopperClient.ts` correlates request/response over the socket with timeouts and cancellation, `protocol.ts` frames bridge messages.
|
||||
- `bridge/hopper_bridge.py`: runs inside Hopper and adapts declared operations to Hopper's public Python API. Hopper's bundled MCP server is not used.
|
||||
- `src/application/`: shared CLI/MCP session composition, setup and diagnostics, and the 8 enhanced workflows.
|
||||
@@ -34,7 +34,7 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec
|
||||
- `npm run jscpd`: detect duplicate code blocks.
|
||||
- `npm run scan:todos`: scan for TODO, FIXME, and HACK markers.
|
||||
- `npm run verify:hopper`: build and run the real-Hopper verifier with two distinct binaries.
|
||||
- `npm run verify:package`: pack and test the CLI, setup transaction, skill, and 42-tool target-free MCP server in an isolated environment.
|
||||
- `npm run verify:package`: pack and test the CLI, setup transaction, skill, and 68-tool target-free MCP server in an isolated environment.
|
||||
- `npm run docs:generate`: generate API documentation from JSDoc comments into `docs/api/` using TypeDoc.
|
||||
- `npm run config:print -- /path/to/binary`: print an MCP server config with absolute paths.
|
||||
- `HOPPER_TARGET_PATH=/path/to/binary npm start`: launch Hopper and run the built stdio MCP server.
|
||||
@@ -54,8 +54,8 @@ Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use the fake launcher/socket; MCP tests connect with the beta.3 client. Preserve the 31 direct, 8 enhanced, and 3 session tool inventory (42 total). Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper claims cannot be replaced by mocks — use `npm run verify:hopper` with two distinct binaries for end-to-end verification.
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use the fake launcher/socket; MCP tests connect with the beta.3 client. Preserve the 33 direct, 10 enhanced, 5 native, 2 artifact, and 18 session tool inventory (68 total). Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper claims cannot be replaced by mocks — use `npm run verify:hopper` with two distinct binaries for end-to-end verification.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Use concise imperative subjects, optionally scoped, such as `Add Hopper timeout classification`. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper verification was performed. Never commit binaries, Hopper documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
|
||||
Use Conventional Commit subjects because Release Please derives versions and changelogs from them. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper verification was performed. Never commit binaries, Hopper documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
|
||||
@@ -69,6 +69,7 @@
|
||||
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
|
||||
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
|
||||
"verify:package": "npm run build && node scripts/verify-package.mjs",
|
||||
"verify:published": "node scripts/verify-published-package.mjs",
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
"docs:generate": "typedoc",
|
||||
"prepack": "npm run build",
|
||||
|
||||
@@ -96,7 +96,10 @@ try {
|
||||
HOME: home,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
||||
HOPPER_LAUNCHER_PATH: hopper,
|
||||
HOPPER_LAUNCHER_PATH: process.execPath,
|
||||
HOPPER_LOADER_ARGS_JSON: JSON.stringify([
|
||||
join(root, "tests/fixtures/fakeLauncher.mjs"),
|
||||
]),
|
||||
REA_NPX_LOG: npxLog,
|
||||
REA_EVIDENCE_ROOTS_JSON: JSON.stringify([evidenceRoot]),
|
||||
REA_REFERENCE_ROOTS_JSON: JSON.stringify([referenceRoot]),
|
||||
@@ -142,6 +145,16 @@ try {
|
||||
artifactInventory.normalized_result?.manifest?.root_format !== "zip"
|
||||
)
|
||||
throw new Error("packaged artifact inventory CLI failed");
|
||||
const overview = json(
|
||||
await run(cli, ["analyze", process.execPath, "--json"], environment),
|
||||
);
|
||||
if (
|
||||
overview.operation !== "binary_overview" ||
|
||||
overview.normalized_result?.procedure_count < 1
|
||||
)
|
||||
throw new Error(
|
||||
`packaged Hopper-backed analyze CLI failed: ${JSON.stringify(overview)}`,
|
||||
);
|
||||
const referenceImport = json(
|
||||
await run(
|
||||
cli,
|
||||
@@ -292,6 +305,27 @@ try {
|
||||
});
|
||||
if (result.isError !== true)
|
||||
throw new Error("packaged target-free MCP omitted no-target error");
|
||||
const opened = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path: process.execPath },
|
||||
});
|
||||
if (opened.isError === true)
|
||||
throw new Error("packaged MCP could not open a binary");
|
||||
const overviewResult = await client.callTool({
|
||||
name: "binary_overview",
|
||||
arguments: {},
|
||||
});
|
||||
if (
|
||||
overviewResult.isError === true ||
|
||||
json(text(overviewResult)).normalized_result?.procedure_count < 1
|
||||
)
|
||||
throw new Error("packaged MCP analysis workflow failed");
|
||||
const closed = await client.callTool({
|
||||
name: "close_binary",
|
||||
arguments: {},
|
||||
});
|
||||
if (closed.isError === true)
|
||||
throw new Error("packaged MCP could not close its binary");
|
||||
const mcpBundlePath = join(evidenceRoot, "mcp.json");
|
||||
const mcpExport = await client.callTool({
|
||||
name: "export_evidence_bundle",
|
||||
@@ -313,7 +347,7 @@ try {
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ cli: true, artifactCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents mcp", doctor: "platform-appropriate", setup: process.platform === "darwin" ? "idempotent" : "unsupported-host-rejected", clients: process.platform === "darwin" ? 2 : 0, backupReadback: process.platform === "darwin", failureRecovery: process.platform === "darwin", skill: process.platform === "darwin", mcpTools: expectedToolCount, evidenceMcp: true, targetFree: true })}\n`,
|
||||
`${JSON.stringify({ cli: true, analysisCli: true, artifactCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents mcp", doctor: "platform-appropriate", setup: process.platform === "darwin" ? "idempotent" : "unsupported-host-rejected", clients: process.platform === "darwin" ? 2 : 0, backupReadback: process.platform === "darwin", failureRecovery: process.platform === "darwin", skill: process.platform === "darwin", mcpTools: expectedToolCount, evidenceMcp: true, targetFree: true, targetLifecycle: true })}\n`,
|
||||
);
|
||||
} finally {
|
||||
if (tarball) await rm(join(root, tarball), { force: true });
|
||||
@@ -326,3 +360,9 @@ async function run(command, args, env) {
|
||||
function json(text) {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
function text(result) {
|
||||
const value = result.content?.find((item) => item.type === "text")?.text;
|
||||
if (typeof value !== "string") throw new Error("MCP result omitted text");
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
const version = process.argv[2];
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? ""))
|
||||
throw new Error("Usage: node scripts/verify-published-package.mjs <version>");
|
||||
|
||||
const serverEnvironment = { ...process.env };
|
||||
delete serverEnvironment.HOPPER_TARGET_PATH;
|
||||
const transport = new StdioClientTransport({
|
||||
command: "npx",
|
||||
args: ["--yes", "--package", `rea-agents@${version}`, "rea", "mcp"],
|
||||
env: serverEnvironment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "published-package-canary", version: "1" });
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
if (stderr.length < 16_384) stderr += chunk.toString("utf8");
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const tools = await client.listTools();
|
||||
if (tools.tools.length !== 68)
|
||||
throw new Error(
|
||||
`Published MCP exposed ${String(tools.tools.length)} tools`,
|
||||
);
|
||||
const status = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
if (status.isError === true)
|
||||
throw new Error("Published target-free MCP session check failed");
|
||||
} catch (cause) {
|
||||
throw new Error(`Published MCP canary failed: ${stderr}`, { cause });
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ package: "rea-agents", version, mcpTools: 68 })}\n`,
|
||||
);
|
||||
@@ -15,6 +15,17 @@ import {
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const timeout = 180_000;
|
||||
|
||||
const parseServerArgs = (encoded) => {
|
||||
const parsed = JSON.parse(encoded);
|
||||
if (
|
||||
!Array.isArray(parsed) ||
|
||||
parsed.some((value) => typeof value !== "string")
|
||||
)
|
||||
throw new Error("REA_VERIFY_SERVER_ARGS_JSON must encode string arguments");
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const sessionsBefore = new Set(
|
||||
(await readdir("/tmp")).filter((name) => name.startsWith("rea-")),
|
||||
);
|
||||
@@ -245,10 +256,17 @@ if (targetA === targetB)
|
||||
const serverEnvironment = { ...process.env };
|
||||
delete serverEnvironment.HOPPER_TARGET_PATH;
|
||||
delete serverEnvironment.HOPPER_SECOND_TARGET_PATH;
|
||||
delete serverEnvironment.REA_VERIFY_SERVER_COMMAND;
|
||||
delete serverEnvironment.REA_VERIFY_SERVER_ARGS_JSON;
|
||||
|
||||
const serverCommand = process.env.REA_VERIFY_SERVER_COMMAND ?? process.execPath;
|
||||
const serverArgs = parseServerArgs(
|
||||
process.env.REA_VERIFY_SERVER_ARGS_JSON ?? '["dist/main.js"]',
|
||||
);
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: ["dist/main.js"],
|
||||
command: serverCommand,
|
||||
args: serverArgs,
|
||||
cwd: process.cwd(),
|
||||
env: serverEnvironment,
|
||||
stderr: "pipe",
|
||||
|
||||
@@ -294,7 +294,7 @@ describe("artifact graph provider", () => {
|
||||
offset: 500,
|
||||
next_offset: null,
|
||||
});
|
||||
});
|
||||
}, 15_000);
|
||||
|
||||
it("uses bounded native lipo metadata for universal slice ranges", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-slices-"));
|
||||
|
||||
Vendored
+35
@@ -1,6 +1,39 @@
|
||||
import { createServer } from "node:net";
|
||||
|
||||
const [socketPath, token, runId] = process.argv.slice(2);
|
||||
const enhancedFixtureResult = (method) => {
|
||||
switch (method) {
|
||||
case "list_segments":
|
||||
return [
|
||||
{
|
||||
name: "__TEXT",
|
||||
start: "0x1000",
|
||||
end: "0x2000",
|
||||
readable: true,
|
||||
writable: false,
|
||||
executable: true,
|
||||
},
|
||||
];
|
||||
case "list_documents":
|
||||
return ["fixture"];
|
||||
case "list_procedures":
|
||||
return {
|
||||
items: [{ address: "0x1000", value: "fixture" }],
|
||||
offset: 0,
|
||||
limit: 500,
|
||||
total: 1,
|
||||
next_offset: null,
|
||||
has_more: false,
|
||||
};
|
||||
case "list_strings":
|
||||
return ["fixture"];
|
||||
case "procedure_pseudo_code":
|
||||
return "return 0;";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const server = createServer((socket) => {
|
||||
socket.on("error", () => undefined);
|
||||
socket.setEncoding("utf8");
|
||||
@@ -50,6 +83,8 @@ const server = createServer((socket) => {
|
||||
});
|
||||
} else if (request.method === "current_document") {
|
||||
send({ id: request.id, result: "fixture" });
|
||||
} else if (enhancedFixtureResult(request.method) !== undefined) {
|
||||
send({ id: request.id, result: enhancedFixtureResult(request.method) });
|
||||
} else if (request.method === "resolve_containing_procedure") {
|
||||
send({
|
||||
id: request.id,
|
||||
|
||||
Reference in new issue
Block a user