Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .github/workflows/codex-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: codex autopmerge

on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, labeled]

jobs:
codex-automerge:
name: Enable or apply Codex auto-merge
runs-on: ubuntu-latest
if: >-
github.event.pull_request.draft == false &&
contains(github.event.pull_request.labels.*.name, 'automerge') &&
(github.event.sender.login == 'chatgpt-codex-connector' || startsWith(github.event.pull_request.head.ref, 'codex/'))
permissions:
contents: write
pull-requests: write
checks: read
actions: read
steps:
- name: Evaluate file changes and apply merge
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const denylist = [
'**/*wrangler*.{json,toml}',
'**/.github/workflows/**',
'**/src/**/stripe*',
'**/src/**/payments*',
'**/src/**/auth*',
'**/src/**/sessions*',
'**/src/**/worker/**/env*',
'**/routing/**',
'**/redirect',
'**/canonical',
'**/migrations/**',
'**/*.sql',
];

const { owner, repo } = context.repo;
const pull_number = context.payload.pull_request.number;

function globToRegex(glob) {
let out = String(glob)
.replace(/[.+^${}()|[\]\]/g, '\\$&')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fix invalid regex literal in glob escaping

The globToRegex helper currently contains an invalid JavaScript regex (/[.+^${}()|[\]\]/g), which causes a SyntaxError when actions/github-script evaluates the step, so this workflow job fails before it can evaluate file changes or enable auto-merge for any PR.

Useful? React with 👍 / 👎.

.replace(/\{([^}]+)\}/g, (_m, alts) => `(?:${alts.split(',').join('|')})`)
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
Comment on lines +48 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve double-star glob behavior in denylist matching

The replacement order in globToRegex converts ** to .* and then immediately rewrites that * into [^/]*, so patterns like **/.github/workflows/** and **/*.sql no longer match repo-root paths (for example .github/workflows/x.yml or schema.sql), allowing denylisted files to bypass the safety gate and be auto-merged when labeled.

Useful? React with 👍 / 👎.

.replace(/\?/g, '.');
return new RegExp(`^${out}$`);
}

function isDenied(path) {
return denylist.some((pattern) => globToRegex(pattern).test(path));
}

const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number,
per_page: 100,
});

const denied = files.filter((f) => isDenied(f.filename));
if (denied.length) {
console.log(`Codex safe-merge skipped: denied file changes detected. files=${denied.map((f) => f.filename).join(', ')}`);
return;
}

let pr = await github.rest.pulls.get({ owner, repo, pull_number }).then((res) => res.data);

try {
await github.rest.pulls.enablePullRequestAutoMerge({
owner,
repo,
pull_number,
merge_method: 'squash',
});
console.log('Enabled GitHub PR auto-merge (squash).');
return;
} catch (err) {
const code = err.status || err.statusCode || 0;
console.log(`Auto-merge unavailable (${code}): ${err.message}`);

if (![422, 405, 403].includes(code)) {
throw err;
}
}

pr = await github.rest.pulls.get({ owner, repo, pull_number }).then((res) => res.data);
if (!pr.mergeable) {
console.log('PR is not mergeable, skipping fallback merge.');
return;
}
if (pr.mergeable_state !== 'clean') {
console.log(`Branch protections/check state not clean (${pr.mergeable_state}), skipping fallback merge.`);
return;
}

try {
await github.rest.pulls.merge({
owner,
repo,
pull_number,
merge_method: 'squash',
});
console.log('Fallback merge via API completed.');
} catch (err) {
const message = err.message || String(err);
console.log(`Fallback merge failed: ${message}`);
}
38 changes: 38 additions & 0 deletions docs/codex-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Codex PR Workflow

## Labels
- `codex-ready`: reviewed by a human (optional).
- `automerge`: safe to automerge (docs-only changes, UI copy, styling, non-sensitive bugfixes).

## Auto-merge policy
- Label `automerge` to trigger this workflow.
- PR must be ready for review (not draft).
- PR author must be `chatgpt-codex-connector` or branch must start with `codex/`.

### Never auto-merge
The workflow exits without automerging when files match any denylist pattern:
- `**/*wrangler*.{json,toml}`
- `**/.github/workflows/**`
- `**/src/**/stripe*`
- `**/src/**/payments*`
- `**/src/**/auth*`
- `**/src/**/sessions*`
- `**/src/**/worker/**/env*`
- `**/routing/**`
- `**/redirect`
- `**/canonical`
- `**/migrations/**`
- `**/*.sql`

### Merge behavior
- `pull_request_target` workflow scans changed files on `opened`, `synchronize`, `reopened`, `ready_for_review`, and `labeled` events.
- If no denylist matches, it enables GitHub auto-merge (`squash`) for the PR.
- If repository auto-merge is disabled, it falls back to API merge only when mergeability is clean.

## PR title format
Use `[codex]` prefix so these PRs are searchable across the portfolio:

- `[codex] add safe automerge workflow`

## Scope
This file documents workflow behavior only. The workflow does not make runtime code changes.