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.
117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
/*
|
||
* Centralised access to environment variables used by the Codex GitHub
|
||
* Action.
|
||
*
|
||
* To enable proper unit-testing we avoid reading from `process.env` at module
|
||
* initialisation time. Instead a `EnvContext` object is created (usually from
|
||
* the real `process.env`) and passed around explicitly or – where that is not
|
||
* yet practical – imported as the shared `defaultContext` singleton. Tests can
|
||
* create their own context backed by a stubbed map of variables without having
|
||
* to mutate global state.
|
||
*/
|
||
|
||
import { fail } from "./fail";
|
||
import * as github from "@actions/github";
|
||
|
||
export interface EnvContext {
|
||
/**
|
||
* Return the value for a given environment variable or terminate the action
|
||
* via `fail` if it is missing / empty.
|
||
*/
|
||
get(name: string): string;
|
||
|
||
/**
|
||
* Attempt to read an environment variable. Returns the value when present;
|
||
* otherwise returns undefined (does not call `fail`).
|
||
*/
|
||
tryGet(name: string): string | undefined;
|
||
|
||
/**
|
||
* Attempt to read an environment variable. Returns non-empty string value or
|
||
* null if unset or empty string.
|
||
*/
|
||
tryGetNonEmpty(name: string): string | null;
|
||
|
||
/**
|
||
* Return a memoised Octokit instance authenticated via the token resolved
|
||
* from the provided argument (when defined) or the environment variables
|
||
* `GITHUB_TOKEN`/`GH_TOKEN`.
|
||
*
|
||
* Subsequent calls return the same cached instance to avoid spawning
|
||
* multiple REST clients within a single action run.
|
||
*/
|
||
getOctokit(token?: string): ReturnType<typeof github.getOctokit>;
|
||
}
|
||
|
||
/** Internal helper – *not* exported. */
|
||
function _getRequiredEnv(
|
||
name: string,
|
||
env: Record<string, string | undefined>,
|
||
): string | undefined {
|
||
const value = env[name];
|
||
|
||
// Avoid leaking secrets into logs while still logging non-secret variables.
|
||
if (name.endsWith("KEY") || name.endsWith("TOKEN")) {
|
||
if (value) {
|
||
console.log(`value for ${name} was found`);
|
||
}
|
||
} else {
|
||
console.log(`${name}=${value}`);
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
/** Create a context backed by the supplied environment map (defaults to `process.env`). */
|
||
export function createEnvContext(
|
||
env: Record<string, string | undefined> = process.env,
|
||
): EnvContext {
|
||
// Lazily instantiated Octokit client – shared across this context.
|
||
let cachedOctokit: ReturnType<typeof github.getOctokit> | null = null;
|
||
|
||
return {
|
||
get(name: string): string {
|
||
const value = _getRequiredEnv(name, env);
|
||
if (value == null) {
|
||
fail(`Missing required environment variable: ${name}`);
|
||
}
|
||
return value;
|
||
},
|
||
|
||
tryGet(name: string): string | undefined {
|
||
return _getRequiredEnv(name, env);
|
||
},
|
||
|
||
tryGetNonEmpty(name: string): string | null {
|
||
const value = _getRequiredEnv(name, env);
|
||
return value == null || value === "" ? null : value;
|
||
},
|
||
|
||
getOctokit(token?: string) {
|
||
if (cachedOctokit) {
|
||
return cachedOctokit;
|
||
}
|
||
|
||
// Determine the token to authenticate with.
|
||
const githubToken = token ?? env["GITHUB_TOKEN"] ?? env["GH_TOKEN"];
|
||
|
||
if (!githubToken) {
|
||
fail(
|
||
"Unable to locate a GitHub token. `github_token` should have been set on the action.",
|
||
);
|
||
}
|
||
|
||
cachedOctokit = github.getOctokit(githubToken!);
|
||
return cachedOctokit;
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Shared context built from the actual `process.env`. Production code that is
|
||
* not yet refactored to receive a context explicitly may import and use this
|
||
* singleton. Tests should avoid the singleton and instead pass their own
|
||
* context to the functions they exercise.
|
||
*/
|
||
export const defaultContext: EnvContext = createEnvContext();
|