This is a first cut at a GitHub Action that lets you define prompt
templates in `.md` files under `.github/codex/labels` that will run
Codex with the associated prompt when the label is added to a GitHub
pull request.
For example, this PR includes these files:
```
.github/codex/labels/codex-attempt.md
.github/codex/labels/codex-code-review.md
.github/codex/labels/codex-investigate-issue.md
```
And the new `.github/workflows/codex.yml` workflow declares the
following triggers:
```yaml
on:
issues:
types: [opened, labeled]
pull_request:
branches: [main]
types: [labeled]
```
as well as the following expression to gate the action:
```
jobs:
codex:
if: |
(github.event_name == 'issues' && (
(github.event.action == 'labeled' && (github.event.label.name == 'codex-attempt' || github.event.label.name == 'codex-investigate-issue'))
)) ||
(github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'codex-code-review')
```
Note the "actor" who added the label must have write access to the repo
for the action to take effect.
After adding a label, the action will "ack" the request by replacing the
original label (e.g., `codex-review`) with an `-in-progress` suffix
(e.g., `codex-review-in-progress`). When it is finished, it will swap
the `-in-progress` label with a `-completed` one (e.g.,
`codex-review-completed`).
Users of the action are responsible for providing an `OPENAI_API_KEY`
and making it available as a secret to the action.
57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
import { fail } from "./fail";
|
|
import { EnvContext } from "./env-context";
|
|
import { tmpdir } from "os";
|
|
import { join } from "node:path";
|
|
import { readFile, mkdtemp } from "fs/promises";
|
|
import { resolveWorkspacePath } from "./github-workspace";
|
|
|
|
/**
|
|
* Runs the Codex CLI with the provided prompt and returns the output written
|
|
* to the "last message" file.
|
|
*/
|
|
export async function runCodex(
|
|
prompt: string,
|
|
ctx: EnvContext,
|
|
): Promise<string> {
|
|
const OPENAI_API_KEY = ctx.get("OPENAI_API_KEY");
|
|
|
|
const tempDirPath = await mkdtemp(join(tmpdir(), "codex-"));
|
|
const lastMessageOutput = join(tempDirPath, "codex-prompt.md");
|
|
|
|
const args = ["/usr/local/bin/codex-exec"];
|
|
|
|
const inputCodexArgs = ctx.tryGet("INPUT_CODEX_ARGS")?.trim();
|
|
if (inputCodexArgs) {
|
|
args.push(...inputCodexArgs.split(/\s+/));
|
|
}
|
|
|
|
args.push("--output-last-message", lastMessageOutput, prompt);
|
|
|
|
const env: Record<string, string> = { ...process.env, OPENAI_API_KEY };
|
|
const INPUT_CODEX_HOME = ctx.tryGet("INPUT_CODEX_HOME");
|
|
if (INPUT_CODEX_HOME) {
|
|
env.CODEX_HOME = resolveWorkspacePath(INPUT_CODEX_HOME, ctx);
|
|
}
|
|
|
|
console.log(`Running Codex: ${JSON.stringify(args)}`);
|
|
const result = Bun.spawnSync(args, {
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
|
|
if (!result.success) {
|
|
fail(`Codex failed: see above for details.`);
|
|
}
|
|
|
|
// Read the output generated by Codex.
|
|
let lastMessage: string;
|
|
try {
|
|
lastMessage = await readFile(lastMessageOutput, "utf8");
|
|
} catch (err) {
|
|
fail(`Failed to read Codex output at '${lastMessageOutput}': ${err}`);
|
|
}
|
|
|
|
return lastMessage;
|
|
}
|