checkpoint work: run codex multiple times (#11)
This commit is contained in:
@@ -31,24 +31,24 @@ Provide either `prompt` or `prompt-file`; the other may be left empty. The actio
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Required | Description | Default |
|
||||
| -------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
|
||||
| `openai-api-key` | Yes | Secret used to authenticate the helper proxy with OpenAI. Store it in `secrets` and never hardcode it. | — |
|
||||
| `prompt` | Conditionally | Inline prompt text. Provide this or `prompt-file`. | `""` |
|
||||
| `prompt-file` | Conditionally | Path (relative to the repository root) of a file that contains the prompt. Provide this or `prompt`. | `""` |
|
||||
| `output-file` | No | File where the final Codex message is written. Leave empty to skip writing a file. | `""` |
|
||||
| `working-directory` | No | Directory passed to `codex exec --cd`. Defaults to the repository root. | `""` |
|
||||
| `sandbox` | No | Sandbox mode for Codex. One of `workspace-write` (default), `read-only` or `danger-full-access`. | `""` |
|
||||
| `codex-version` | No | Version of `@openai/codex` to install. | `0.42.0-alpha.3` |
|
||||
| `codex-args` | No | Extra arguments forwarded to `codex exec`. Accepts JSON arrays (`["--flag", "value"]`) or shell-style strings. | `""` |
|
||||
| `output-schema` | No | Inline schema contents written to a temp file and passed to `codex exec --output-schema`. Mutually exclusive with `output-schema-file`. | `""` |
|
||||
| `output-schema-file` | No | Schema file forwarded to `codex exec --output-schema`. Leave empty to skip passing the option. | `""` |
|
||||
| `model` | No | Model the agent should use. Leave empty to let Codex pick its default. | `""` |
|
||||
| `codex-home` | No | Directory to use as the Codex CLI home (config/cache). Uses the CLI default when empty. | `""` |
|
||||
| `safety-strategy` | No | Controls how the action restricts Codex privileges. See [Safety strategy](#safety-strategy). | `drop-sudo` |
|
||||
| `codex-user` | No | Username to run Codex as when `safety-strategy` is `unprivileged-user`. | `""` |
|
||||
| `require-repo-write` | No | Whether to require the triggering actor to have write access to the repository before running. | "true" |
|
||||
| `allow-bots` | No | Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check. | "false" |
|
||||
| Name | Description | Default |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
|
||||
| `openai-api-key` | Secret used to start the Responses API proxy. Required when starting the proxy (key-only or key+prompt). Store it in `secrets`. | `""` |
|
||||
| `prompt` | Inline prompt text. Provide this or `prompt-file`. | `""` |
|
||||
| `prompt-file` | Path (relative to the repository root) of a file that contains the prompt. Provide this or `prompt`. | `""` |
|
||||
| `output-file` | File where the final Codex message is written. Leave empty to skip writing a file. | `""` |
|
||||
| `working-directory` | Directory passed to `codex exec --cd`. Defaults to the repository root. | `""` |
|
||||
| `sandbox` | Sandbox mode for Codex. One of `workspace-write` (default), `read-only` or `danger-full-access`. | `""` |
|
||||
| `codex-version` | Version of `@openai/codex` to install. | `0.42.0-alpha.3` |
|
||||
| `codex-args` | Extra arguments forwarded to `codex exec`. Accepts JSON arrays (`["--flag", "value"]`) or shell-style strings. | `""` |
|
||||
| `output-schema` | Inline schema contents written to a temp file and passed to `codex exec --output-schema`. Mutually exclusive with `output-schema-file`. | `""` |
|
||||
| `output-schema-file` | Schema file forwarded to `codex exec --output-schema`. Leave empty to skip passing the option. | `""` |
|
||||
| `model` | Model the agent should use. Leave empty to let Codex pick its default. | `""` |
|
||||
| `codex-home` | Directory to use as the Codex CLI home (config/cache). Uses the CLI default when empty. | `""` |
|
||||
| `safety-strategy` | Controls how the action restricts Codex privileges. See [Safety strategy](#safety-strategy). | `drop-sudo` |
|
||||
| `codex-user` | Username to run Codex as when `safety-strategy` is `unprivileged-user`. | `""` |
|
||||
| `require-repo-write` | Whether to require the triggering actor to have write access to the repository before running. | "true" |
|
||||
| `allow-bots` | Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check. | "false" |
|
||||
|
||||
## Safety Strategy
|
||||
|
||||
@@ -86,7 +86,8 @@ Replace `steps.codex` with the `id` assigned to your action step.
|
||||
- Run this action after `actions/checkout@v5` so Codex has access to your repository contents.
|
||||
- If you want Codex to have access to a narrow set of privileged functionality, consider running a local MCP server that can perform these actions and configure Codex to use it.
|
||||
- If you need more control over the CLI invocation, pass flags through `codex-args` or create a `config.toml` in `codex-home`.
|
||||
- Once `openai/codex-action` is run once with `openai-api-key`, you can also call `codex` from subsequent scripts in your job. (You can omit `prompt` and `prompt-file` from the action in this case.)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [Apache License 2.0](./LICENSE)
|
||||
This project is licensed under the [Apache License 2.0](./LICENSE).
|
||||
|
||||
+38
-17
@@ -16,7 +16,8 @@ inputs:
|
||||
default: ""
|
||||
openai-api-key:
|
||||
description: "OpenAI API key used by Codex."
|
||||
required: true
|
||||
required: false
|
||||
default: ""
|
||||
working-directory:
|
||||
description: "Working directory that Codex should use. Defaults to the repository root."
|
||||
required: false
|
||||
@@ -127,36 +128,62 @@ runs:
|
||||
shell: bash
|
||||
run: npm install -g "@openai/codex-responses-api-proxy@${{ inputs['codex-version'] }}"
|
||||
|
||||
- name: Resolve Codex home
|
||||
id: resolve_home
|
||||
shell: bash
|
||||
run: |
|
||||
node "${{ github.action_path }}/dist/main.js" resolve-codex-home "${{ inputs['codex-home'] }}"
|
||||
|
||||
- name: Determine server info path
|
||||
id: derive_server_info
|
||||
shell: bash
|
||||
run: |
|
||||
server_info_file="${{ steps.resolve_home.outputs.codex-home }}/${{ github.run_id }}.json"
|
||||
echo "server_info_file=$server_info_file" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Start Responses API proxy
|
||||
id: start_proxy
|
||||
if: ${{ inputs['openai-api-key'] != '' }}
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ inputs['openai-api-key'] }}
|
||||
shell: bash
|
||||
run: |
|
||||
tmpfile=$(mktemp)
|
||||
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
|
||||
if [ -s "$server_info_file" ]; then
|
||||
echo "Responses API proxy already appears to be running (found $server_info_file)."
|
||||
exit 0
|
||||
fi
|
||||
(
|
||||
printenv OPENAI_API_KEY | codex-responses-api-proxy --http-shutdown --server-info "$tmpfile"
|
||||
printenv OPENAI_API_KEY | codex-responses-api-proxy --http-shutdown --server-info "$server_info_file"
|
||||
) &
|
||||
for _ in {1..10}; do
|
||||
if [ -s "$tmpfile" ]; then
|
||||
if [ -s "$server_info_file" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ ! -s "$tmpfile" ]; then
|
||||
if [ ! -s "$server_info_file" ]; then
|
||||
echo "responses-api-proxy did not write server info" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "server_info_file=$tmpfile" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# This step has an output named `port`.
|
||||
- name: Read server info
|
||||
id: read_server_info
|
||||
if: ${{ inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '' }}
|
||||
shell: bash
|
||||
run: node "${{ github.action_path }}/dist/main.js" read-server-info "${{ steps.start_proxy.outputs.server_info_file }}"
|
||||
run: node "${{ github.action_path }}/dist/main.js" read-server-info "${{ steps.derive_server_info.outputs.server_info_file }}"
|
||||
|
||||
- name: Write Codex proxy config
|
||||
if: ${{ inputs['openai-api-key'] != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
node "${{ github.action_path }}/dist/main.js" write-proxy-config \
|
||||
--codex-home "${{ steps.resolve_home.outputs.codex-home }}" \
|
||||
--port "${{ steps.read_server_info.outputs.port }}"
|
||||
|
||||
- name: Drop sudo privilege, if appropriate
|
||||
if: ${{ inputs['safety-strategy'] == 'drop-sudo' }}
|
||||
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
case "${RUNNER_OS}" in
|
||||
@@ -173,7 +200,7 @@ runs:
|
||||
esac
|
||||
|
||||
- name: Verify sudo privilege removed
|
||||
if: ${{ inputs['safety-strategy'] == 'drop-sudo' }}
|
||||
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
if sudo -n true 2>/dev/null; then
|
||||
@@ -184,11 +211,12 @@ runs:
|
||||
|
||||
- name: Run codex exec
|
||||
id: run_codex
|
||||
if: ${{ inputs.prompt != '' || inputs['prompt-file'] != '' }}
|
||||
env:
|
||||
CODEX_PROMPT: ${{ inputs.prompt }}
|
||||
CODEX_PROMPT_FILE: ${{ inputs['prompt-file'] }}
|
||||
CODEX_OUTPUT_FILE: ${{ inputs['output-file'] }}
|
||||
CODEX_HOME: ${{ inputs['codex-home'] }}
|
||||
CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
|
||||
CODEX_WORKING_DIRECTORY: ${{ inputs['working-directory'] || github.workspace }}
|
||||
CODEX_SANDBOX: ${{ inputs.sandbox }}
|
||||
CODEX_ARGS: ${{ inputs['codex-args'] }}
|
||||
@@ -206,7 +234,6 @@ runs:
|
||||
--output-file "$CODEX_OUTPUT_FILE" \
|
||||
--codex-home "$CODEX_HOME" \
|
||||
--cd "$CODEX_WORKING_DIRECTORY" \
|
||||
--proxy-port "${{ steps.read_server_info.outputs.port }}" \
|
||||
--extra-args "$CODEX_ARGS" \
|
||||
--output-schema "$CODEX_OUTPUT_SCHEMA" \
|
||||
--output-schema-file "$CODEX_OUTPUT_SCHEMA_FILE" \
|
||||
@@ -214,9 +241,3 @@ runs:
|
||||
--model "$CODEX_MODEL" \
|
||||
--safety-strategy "$CODEX_SAFETY_STRATEGY" \
|
||||
--codex-user "$CODEX_USER"
|
||||
|
||||
- name: Shutdown codex webserver
|
||||
env:
|
||||
CODEX_SERVER_PORT: ${{ steps.read_server_info.outputs.port }}
|
||||
shell: bash
|
||||
run: curl --fail --silent --show-error "http://127.0.0.1:${CODEX_SERVER_PORT}/shutdown"
|
||||
|
||||
Vendored
+178
-114
File diff suppressed because one or more lines are too long
+64
-8
@@ -1,4 +1,7 @@
|
||||
import { Command, Option } from "commander";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import pkg from "../package.json" assert { type: "json" };
|
||||
|
||||
import { readServerInfo } from "./readServerInfo";
|
||||
@@ -12,6 +15,7 @@ import {
|
||||
import { dropSudo } from "./dropSudo";
|
||||
import { ensureActorHasWriteAccess } from "./checkActorPermissions";
|
||||
import parseArgsStringToArgv from "string-argv";
|
||||
import { writeProxyConfig } from "./writeProxyConfig";
|
||||
|
||||
export async function main() {
|
||||
const program = new Command();
|
||||
@@ -29,6 +33,37 @@ export async function main() {
|
||||
await readServerInfo(serverInfoFile);
|
||||
});
|
||||
|
||||
program
|
||||
.command("resolve-codex-home")
|
||||
.description(
|
||||
"Resolve the Codex home directory with precedence: input, env, default (~/.codex)"
|
||||
)
|
||||
.argument(
|
||||
"[inputCodexHome]",
|
||||
"Optional codex-home input value (may be empty)"
|
||||
)
|
||||
.action(async (inputCodexHome?: string) => {
|
||||
const resolved = await resolveCodexHome(
|
||||
emptyAsNull(inputCodexHome ?? "")
|
||||
);
|
||||
// Ensure directory exists for downstream steps that will write files here.
|
||||
await fs.mkdir(resolved, { recursive: true });
|
||||
const { setOutput } = await import("@actions/core");
|
||||
setOutput("codex-home", resolved);
|
||||
console.log(`Resolved Codex home: ${resolved}`);
|
||||
});
|
||||
|
||||
program
|
||||
.command("write-proxy-config")
|
||||
.description(
|
||||
"Write the OpenAI Proxy model provider config into CODEX_HOME/config.toml"
|
||||
)
|
||||
.requiredOption("--codex-home <DIRECTORY>", "Path to Codex home directory")
|
||||
.requiredOption("--port <port>", "Proxy server port", parseIntStrict)
|
||||
.action(async (options: { codexHome: string; port: number }) => {
|
||||
await writeProxyConfig(options.codexHome, options.port);
|
||||
});
|
||||
|
||||
program
|
||||
.command("drop-sudo")
|
||||
.description("Drops sudo privileges for the configured user.")
|
||||
@@ -62,11 +97,6 @@ export async function main() {
|
||||
"Path to the Codex CLI home directory (where config files are stored)."
|
||||
)
|
||||
.requiredOption("--cd <DIRECTORY>", "Working directory for Codex")
|
||||
.requiredOption(
|
||||
"--proxy-port <port>",
|
||||
"Port of the Responses API Proxy",
|
||||
parseIntStrict
|
||||
)
|
||||
.requiredOption(
|
||||
"--extra-args <args>",
|
||||
"Additional args to pass through to `codex exec` as JSON array or shell string.",
|
||||
@@ -103,7 +133,6 @@ export async function main() {
|
||||
promptFile: string;
|
||||
codexHome: string;
|
||||
cd: string;
|
||||
proxyPort: number;
|
||||
extraArgs: Array<string>;
|
||||
outputFile: string;
|
||||
outputSchemaFile: string;
|
||||
@@ -119,7 +148,6 @@ export async function main() {
|
||||
outputFile,
|
||||
codexHome,
|
||||
cd,
|
||||
proxyPort,
|
||||
extraArgs,
|
||||
outputSchema,
|
||||
outputSchemaFile,
|
||||
@@ -131,6 +159,12 @@ export async function main() {
|
||||
|
||||
const normalizedPrompt = emptyAsNull(prompt);
|
||||
const normalizedPromptFile = emptyAsNull(promptFile);
|
||||
if (normalizedPrompt != null && normalizedPromptFile != null) {
|
||||
throw new Error(
|
||||
"Only one of `prompt` or `prompt-file` may be specified."
|
||||
);
|
||||
}
|
||||
|
||||
let promptSource: PromptSource;
|
||||
if (normalizedPrompt != null) {
|
||||
promptSource = { type: "inline", content: normalizedPrompt };
|
||||
@@ -173,7 +207,6 @@ export async function main() {
|
||||
prompt: promptSource,
|
||||
codexHome: emptyAsNull(codexHome),
|
||||
cd,
|
||||
proxyPort,
|
||||
extraArgs,
|
||||
explicitOutputFile: emptyAsNull(outputFile),
|
||||
outputSchema: outputSchemaSource,
|
||||
@@ -279,3 +312,26 @@ function parseBoolean(value: string): boolean {
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
async function resolveCodexHome(
|
||||
inputCodexHome: string | null
|
||||
): Promise<string> {
|
||||
if (inputCodexHome != null) {
|
||||
return expandTilde(inputCodexHome);
|
||||
}
|
||||
const envHome = emptyAsNull(process.env.CODEX_HOME ?? "");
|
||||
if (envHome != null) {
|
||||
return envHome;
|
||||
}
|
||||
return path.join(os.homedir(), ".codex");
|
||||
}
|
||||
|
||||
function expandTilde(p: string): string {
|
||||
if (p === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
if (p.startsWith("~/") || p.startsWith("~\\")) {
|
||||
return path.join(os.homedir(), p.slice(2));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ export async function runCodexExec({
|
||||
prompt,
|
||||
codexHome,
|
||||
cd,
|
||||
proxyPort,
|
||||
extraArgs,
|
||||
explicitOutputFile,
|
||||
outputSchema,
|
||||
@@ -50,7 +49,6 @@ export async function runCodexExec({
|
||||
prompt: PromptSource;
|
||||
codexHome: string | null;
|
||||
cd: string;
|
||||
proxyPort: number;
|
||||
extraArgs: Array<string>;
|
||||
explicitOutputFile: string | null;
|
||||
outputSchema: OutputSchemaSource | null;
|
||||
@@ -94,20 +92,12 @@ export async function runCodexExec({
|
||||
command.push("sudo", "-u", codexUser, "--");
|
||||
}
|
||||
|
||||
const providerBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
|
||||
const providerId = "openai-proxy";
|
||||
const providerConfig = `model_providers.${providerId}={ name = "OpenAI Proxy", base_url = "${providerBaseUrl}", wire_api = "responses" }`;
|
||||
|
||||
command.push(
|
||||
"codex",
|
||||
"exec",
|
||||
"--skip-git-repo-check",
|
||||
"--cd",
|
||||
cd,
|
||||
"--config",
|
||||
providerConfig,
|
||||
"--config",
|
||||
`model_provider="${providerId}"`,
|
||||
"--output-last-message",
|
||||
outputFile.file
|
||||
);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
const MODEL_PROVIDER = "openai-proxy";
|
||||
|
||||
export async function writeProxyConfig(
|
||||
codexHome: string,
|
||||
port: number
|
||||
): Promise<void> {
|
||||
const configPath = path.join(codexHome, "config.toml");
|
||||
|
||||
let existing = "";
|
||||
try {
|
||||
existing = await fs.readFile(configPath, "utf8");
|
||||
} catch {
|
||||
existing = "";
|
||||
}
|
||||
|
||||
const header = `# Added by codex-action.
|
||||
model_provider = "${MODEL_PROVIDER}"
|
||||
|
||||
|
||||
`;
|
||||
const table = `
|
||||
|
||||
# Added by codex-action.
|
||||
[model_providers.${MODEL_PROVIDER}]
|
||||
name = "OpenAI Proxy"
|
||||
base_url = "http://127.0.0.1:${port}/v1"
|
||||
wire_api = "responses"
|
||||
`;
|
||||
|
||||
// Prepend model_provider at the very top.
|
||||
let output = `${header}${existing}${table}`;
|
||||
|
||||
await fs.mkdir(codexHome, { recursive: true });
|
||||
await fs.writeFile(configPath, output, "utf8");
|
||||
}
|
||||
Reference in New Issue
Block a user