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.5 KiB
TypeScript
57 lines
1.5 KiB
TypeScript
import type { Config, LabelConfig } from "./config";
|
|
|
|
import { getDefaultConfig } from "./default-label-config";
|
|
import { readFileSync, readdirSync, statSync } from "fs";
|
|
import * as path from "path";
|
|
|
|
/**
|
|
* Build an in-memory configuration object by scanning the repository for
|
|
* Markdown templates located in `.github/codex/labels`.
|
|
*
|
|
* Each `*.md` file in that directory represents a label that can trigger the
|
|
* Codex GitHub Action. The filename **without** the extension is interpreted
|
|
* as the label name, e.g. `codex-review.md` ➜ `codex-review`.
|
|
*
|
|
* For every such label we derive the corresponding `doneLabel` by appending
|
|
* the suffix `-completed`.
|
|
*/
|
|
export function loadConfig(workspace: string): Config {
|
|
const labelsDir = path.join(workspace, ".github", "codex", "labels");
|
|
|
|
let entries: string[];
|
|
try {
|
|
entries = readdirSync(labelsDir);
|
|
} catch {
|
|
// If the directory is missing, return the default configuration.
|
|
return getDefaultConfig();
|
|
}
|
|
|
|
const labels: Record<string, LabelConfig> = {};
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.endsWith(".md")) {
|
|
continue;
|
|
}
|
|
|
|
const fullPath = path.join(labelsDir, entry);
|
|
|
|
if (!statSync(fullPath).isFile()) {
|
|
continue;
|
|
}
|
|
|
|
const labelName = entry.slice(0, -3); // trim ".md"
|
|
|
|
labels[labelName] = new FileLabelConfig(fullPath);
|
|
}
|
|
|
|
return { labels };
|
|
}
|
|
|
|
class FileLabelConfig implements LabelConfig {
|
|
constructor(private readonly promptPath: string) {}
|
|
|
|
getPromptTemplate(): string {
|
|
return readFileSync(this.promptPath, "utf8");
|
|
}
|
|
}
|