Skip to content
Merged
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
25 changes: 7 additions & 18 deletions .github/actions/codex-bootstrap-lite/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "Codex Bootstrap (Lite)"
description: "Minimal issue → branch → PR bootstrap with PAT-first auth and optional auto-ready"
description: "Minimal issue → branch → ready-for-review PR bootstrap with PAT-first auth"

inputs:
issue:
Expand All @@ -22,11 +22,11 @@ inputs:
required: false
default: ""
draft:
description: "Whether to create the PR as a draft (true|false)"
description: "Deprecated compatibility input; ignored because automation PRs are always ready"
required: false
default: "false"
auto_ready:
description: "If draft=true and auto_ready=true, immediately mark ready-for-review"
description: "Deprecated compatibility input; ignored because automation PRs are always ready"
required: false
default: "false"
post_codex_comment:
Expand Down Expand Up @@ -80,8 +80,6 @@ runs:
GITHUB_TOKEN: ${{ github.token }}
CODEX_CMD: ${{ inputs.codex_command }}
FORCE_BASE: ${{ inputs.base_branch }}
DRAFT_FLAG: ${{ inputs.draft }}
AUTO_READY: ${{ inputs.auto_ready }}
POST_CODEX: ${{ inputs.post_codex_comment }}
AGENT_KEY: ${{ inputs.agent_key }}
PR_MODE: ${{ inputs.pr_mode }}
Expand Down Expand Up @@ -159,7 +157,6 @@ runs:
'—',
'PR created automatically to engage Codex.'
].join('\n');
const asDraft = /^true$/i.test(process.env.DRAFT_FLAG || 'false');
const prMode = (process.env.PR_MODE || 'create').toLowerCase();

if (prMode === 'invite') {
Expand Down Expand Up @@ -191,10 +188,11 @@ runs:
return;
}

// Create draft or ready PR
// Automation-created PRs are always ready for review. Staging and
// dependency state belong in labels, checks, and the PR body.
let prNum = null;
try {
const { data: pr } = await github.rest.pulls.create({ owner, repo, head: branch, base: baseBranch, draft: asDraft, title: `Codex bootstrap for #${issue_number}`, body });
const { data: pr } = await github.rest.pulls.create({ owner, repo, head: branch, base: baseBranch, draft: false, title: `Codex bootstrap for #${issue_number}`, body });
prNum = pr.number;
} catch (e) {
core.setFailed(`Failed to open PR: ${e.status || '?'} ${e.message}`);
Expand Down Expand Up @@ -246,15 +244,6 @@ runs:
} catch (e) { core.warning(`Instruction comment failed: ${e.message}`); }
}

// Optional: immediately mark ready for review
const auto = /^true$/i.test(process.env.AUTO_READY || 'false');
if (asDraft && auto) {
try { await github.rest.pulls.update({ owner, repo, pull_number: prNum, draft: false }); } catch (e) {
// Some endpoints prefer the separate ready-for-review endpoint
try { await github.request('POST /repos/{owner}/{repo}/pulls/{pull_number}/ready-for-review', { owner, repo, pull_number: prNum }); } catch (e2) { core.warning(`Auto-ready failed: ${e2.status || '?'} ${e2.message}`); }
}
}

// Link back to the issue
try {
const executionSnippet = [
Expand All @@ -272,7 +261,7 @@ runs:
'```'
].join('\n');
const keepaliveStatus = `Keepalive mode: **${keepaliveEnabled ? 'ON' : 'OFF'}**`;
await github.rest.issues.createComment({ owner, repo, issue_number, body: `Opened ${asDraft && !auto ? 'draft ' : ''}PR #${prNum} to engage Codex. Track work there.\n\n${keepaliveStatus}\n\n${executionSnippet}` });
await github.rest.issues.createComment({ owner, repo, issue_number, body: `Opened ready-for-review PR #${prNum} to engage Codex. Track work there.\n\n${keepaliveStatus}\n\n${executionSnippet}` });
} catch {}

core.setOutput('pr', String(prNum));
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/agents_orchestrator_resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ async function resolveOrchestratorParams({ github, context, core, env = process.
merged.bootstrap_issues_label ?? bootstrap.label,
DEFAULTS.bootstrap_issues_label
),
draft_pr: toBoolString(merged.draft_pr, DEFAULTS.draft_pr),
draft_pr: 'false',
dry_run: dryRun,
options_json: finalOptionsJson,
dispatcher_force_issue: dispatcherForceIssue,
Expand Down
79 changes: 55 additions & 24 deletions .github/scripts/maint71_merge_sync_prs.js
Original file line number Diff line number Diff line change
Expand Up @@ -909,10 +909,51 @@ async function run({ github, context, core }) {
}
}

async function holdReadyStableDelivery({ owner, repo, pr }) {
let { data: current } = await withRetry((client) => client.rest.pulls.get({
owner,
repo,
pull_number: pr.number,
}));
if (current.auto_merge) {
await withRetry((client) => client.graphql(
`mutation($id: ID!) {
disablePullRequestAutoMerge(input: {pullRequestId: $id}) {
pullRequest { id autoMergeRequest { enabledAt } }
}
}`,
{ id: current.node_id },
));
}
if (current.draft) {
await withRetry((client) => client.graphql(
`mutation($id: ID!) {
markPullRequestReadyForReview(input: {pullRequestId: $id}) {
pullRequest { id isDraft }
}
}`,
{ id: current.node_id },
));
}
({ data: current } = await withRetry((client) => client.rest.pulls.get({
owner,
repo,
pull_number: pr.number,
})));
if (current.auto_merge) {
throw new Error(`Auto-merge remains enabled for staged delivery PR #${pr.number}.`);
}
if (current.draft) {
throw new Error(`Staged delivery PR #${pr.number} remains draft.`);
}
return current;
}

async function beginStableDeliveryReview({ owner, repo, pr, record, dryRunMode }) {
const reviewStartedAt = record.review_started_at || new Date().toISOString();
if (dryRunMode) return { reviewStartedAt, dryRun: true };
const body = replaceDeliveryRecord(pr.body || '', {
const current = await holdReadyStableDelivery({ owner, repo, pr });
const body = replaceDeliveryRecord(current.body || '', {
delivery_state: 'reviewing',
review_started_at: reviewStartedAt,
sealed_at: '',
Expand All @@ -925,16 +966,6 @@ async function run({ github, context, core }) {
pull_number: pr.number,
body,
}));
if (pr.draft) {
await withRetry((client) => client.graphql(
`mutation($id: ID!) {
markPullRequestReadyForReview(input: {pullRequestId: $id}) {
pullRequest { id isDraft }
}
}`,
{ id: pr.node_id },
));
}
await withRetry((client) => client.rest.issues.addLabels({
owner,
repo,
Expand All @@ -945,30 +976,30 @@ async function run({ github, context, core }) {
}

async function restageStableDelivery({ owner, repo, pr, dryRunMode }) {
const body = replaceDeliveryRecord(pr.body || '', {
if (dryRunMode) {
const body = replaceDeliveryRecord(pr.body || '', {
delivery_state: 'staging',
review_started_at: '',
sealed_at: '',
sealed_head_sha: '',
review_evidence: {},
});
return { body, dryRun: true };
}
const current = await holdReadyStableDelivery({ owner, repo, pr });
const body = replaceDeliveryRecord(current.body || '', {
delivery_state: 'staging',
review_started_at: '',
sealed_at: '',
sealed_head_sha: '',
review_evidence: {},
});
if (dryRunMode) return { body, dryRun: true };
await withRetry((client) => client.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
body,
}));
if (!pr.draft) {
await withRetry((client) => client.graphql(
`mutation($id: ID!) {
convertPullRequestToDraft(input: {pullRequestId: $id}) {
pullRequest { id isDraft }
}
}`,
{ id: pr.node_id },
));
}
await withRetry((client) => client.rest.issues.addLabels({
owner,
repo,
Expand Down Expand Up @@ -2022,7 +2053,7 @@ async function run({ github, context, core }) {
continue;
}

// Stable candidate PRs may advance from draft -> reviewing -> sealed
// Stable candidate PRs may advance from staging -> reviewing -> sealed
// without pre-merge evidence. The evidence artifact authorizes only the
// irreversible merge, so lifecycle progress cannot deadlock behind the
// artifact that the sealed state is responsible for producing.
Expand Down
12 changes: 1 addition & 11 deletions .github/workflows/agents-63-issue-intake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ on:
required: false
type: string
default: "codex"
bridge_draft_pr:
description: "Open Codex bootstrap PRs as draft (true/false)"
required: false
type: boolean
default: false
apply_langchain_formatting:
description: "Auto-format issues with LangChain after creation"
required: false
Expand Down Expand Up @@ -93,7 +88,7 @@ on:
required: false
type: string
bridge_draft_pr:
description: "Open Codex bootstrap PRs as draft (true/false)"
description: "Deprecated compatibility input; ignored because automation PRs are always ready"
required: false
type: string
apply_langchain_formatting:
Expand Down Expand Up @@ -160,7 +155,6 @@ jobs:
issue_number: ${{ steps.normalize.outputs.issue_number }}
post_codex_comment: ${{ steps.normalize.outputs.post_codex_comment }}
bridge_agent: ${{ steps.normalize.outputs.bridge_agent }}
bridge_draft_pr: ${{ steps.normalize.outputs.bridge_draft_pr }}
apply_langchain_formatting: ${{ steps.normalize.outputs.apply_langchain_formatting }}
steps:
- name: Normalize inputs
Expand All @@ -174,7 +168,6 @@ jobs:
INPUT_ISSUE_NUMBER: ${{ inputs.issue_number }}
INPUT_POST_CODEX_COMMENT: ${{ inputs.post_codex_comment }}
INPUT_BRIDGE_AGENT: ${{ inputs.bridge_agent }}
INPUT_BRIDGE_DRAFT_PR: ${{ inputs.bridge_draft_pr }}
INPUT_APPLY_LANGCHAIN_FORMATTING: ${{ inputs.apply_langchain_formatting }}
run: |
# Handle boolean inputs from workflow_dispatch vs workflow_call
Expand All @@ -197,7 +190,6 @@ jobs:
issue_number="${INPUT_ISSUE_NUMBER:-}"
post_codex_input="${INPUT_POST_CODEX_COMMENT:-}"
bridge_agent="${INPUT_BRIDGE_AGENT:-}"
draft_pr=$(normalize_bool "${INPUT_BRIDGE_DRAFT_PR:-}")
apply_formatting=$(normalize_bool "${INPUT_APPLY_LANGCHAIN_FORMATTING:-}")

payload_issue=""
Expand Down Expand Up @@ -253,7 +245,6 @@ jobs:
echo "issue_number=$issue_number"
echo "post_codex_comment=$post_codex"
echo "bridge_agent=$bridge_agent"
echo "bridge_draft_pr=$draft_pr"
echo "apply_langchain_formatting=$apply_formatting"
} >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -1732,7 +1723,6 @@ jobs:
}}
mode: 'invite'
post_agent_comment: ${{ needs.normalize_inputs.outputs.post_codex_comment }}
agent_pr_draft: ${{ needs.normalize_inputs.outputs.bridge_draft_pr }}
secrets:
service_bot_pat: ${{ secrets.SERVICE_BOT_PAT }}
owner_pr_pat: ${{ secrets.OWNER_PR_PAT }}
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/agents-70-orchestrator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ jobs:
keepalive_max_retries: ${{ needs.init.outputs.keepalive_max_retries }}
enable_bootstrap: ${{ needs.init.outputs.enable_bootstrap }}
bootstrap_issues_label: ${{ needs.init.outputs.bootstrap_issues_label }}
draft_pr: ${{ needs.init.outputs.draft_pr }}
verify_issue_valid_assignees: ${{ needs.init.outputs.verify_issue_valid_assignees }}
dry_run: ${{ needs.init.outputs.dry_run }}
options_json: ${{ needs.init.outputs.options_json }}
Expand Down
73 changes: 55 additions & 18 deletions .github/workflows/maint-68-sync-consumer-repos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,56 @@ jobs:
credential_helper="!f() { echo \"username=x-access-token\"; echo \"password=\$GH_TOKEN\"; }; f"
git config credential.helper "$credential_helper"

hold_ready_pr() {
local pr_number="$1"
local expected_head="$2"
local pr_json
local pr_state
local pr_head
local pr_is_draft
local pr_has_auto_merge

pr_json=$(gh pr view "$pr_number" \
--json state,headRefOid,isDraft,autoMergeRequest)
pr_state=$(jq -r .state <<<"$pr_json")
pr_head=$(jq -r .headRefOid <<<"$pr_json")
pr_is_draft=$(jq -r .isDraft <<<"$pr_json")
pr_has_auto_merge=$(jq -r '.autoMergeRequest != null' <<<"$pr_json")
if [ "$pr_state" != "OPEN" ] || [ "$pr_head" != "$expected_head" ]; then
echo "::error::Existing PR #$pr_number changed before refresh; refusing to overwrite it."
return 1
fi

# Maint 71 is the sole merge authority. Disable any inherited
# auto-merge before readiness, body, label, or head mutations.
if [ "$pr_has_auto_merge" = "true" ]; then
if ! gh pr merge "$pr_number" --disable-auto; then
echo "::error::Could not disable auto-merge for PR #$pr_number."
return 1
fi
fi
if [ "$pr_is_draft" = "true" ]; then
if ! gh pr ready "$pr_number"; then
echo "::error::Could not mark legacy draft PR #$pr_number ready for review."
return 1
fi
fi

pr_json=$(gh pr view "$pr_number" \
--json state,headRefOid,isDraft,autoMergeRequest)
pr_state=$(jq -r .state <<<"$pr_json")
pr_head=$(jq -r .headRefOid <<<"$pr_json")
pr_is_draft=$(jq -r .isDraft <<<"$pr_json")
pr_has_auto_merge=$(jq -r '.autoMergeRequest != null' <<<"$pr_json")
if [ "$pr_state" != "OPEN" ] \
|| [ "$pr_head" != "$expected_head" ] \
|| [ "$pr_is_draft" != "false" ] \
|| [ "$pr_has_auto_merge" != "false" ]; then
echo "::error::PR #$pr_number is not safely held ready for review."
return 1
fi
}

# Keep one generated PR per lane current. Both candidate and promoted
# deliveries use stable branches; immutable plan/hash identity lives in
# the delivery marker rather than in a replacement PR branch.
Expand Down Expand Up @@ -1195,21 +1245,13 @@ jobs:
if [ -n "$existing_pr" ] \
&& { [ "$matching_existing" != "true" ] || [ "$migrating_legacy_lifecycle" = "true" ]; }; then
# Revalidate the selected head, then close every automatic merge
# path immediately before mutating it. Draft state is GitHub's
# native hard block; the staging label is enforced by all shared
# merger lanes and by Gate until Maint 71 seals the exact head.
current_pr_json=$(gh pr view "$existing_pr" --json state,headRefOid,isDraft)
current_pr_state=$(jq -r .state <<<"$current_pr_json")
current_pr_head=$(jq -r .headRefOid <<<"$current_pr_json")
if [ "$current_pr_state" != "OPEN" ] || [ "$current_pr_head" != "$existing_head" ]; then
echo "Existing PR #$existing_pr changed before refresh; refusing to overwrite it."
# path immediately before mutating it. The staging label is
# enforced by all shared merger lanes and by Gate until Maint 71
# seals the exact head; PRs remain ready for review throughout.
if ! hold_ready_pr "$existing_pr" "$existing_head"; then
echo "status=existing_pr_changed" >> "$GITHUB_OUTPUT"
exit 1
fi
gh pr merge "$existing_pr" --disable-auto >/dev/null 2>&1 || true
if [ "$(jq -r .isDraft <<<"$current_pr_json")" != "true" ]; then
gh pr ready "$existing_pr" --undo
fi
gh pr edit "$existing_pr" --add-label "sync:delivery-staging"
gh pr edit "$existing_pr" --remove-label "sync:delivery-ready" >/dev/null 2>&1 || true
fi
Expand Down Expand Up @@ -1286,11 +1328,7 @@ jobs:
fi

if [ -n "$existing_pr" ]; then
current_pr_json=$(gh pr view "$existing_pr" --json state,headRefOid)
current_pr_state=$(jq -r .state <<<"$current_pr_json")
current_pr_head=$(jq -r .headRefOid <<<"$current_pr_json")
if [ "$current_pr_state" != "OPEN" ] \
|| [ "$current_pr_head" != "$existing_head" ]; then
if ! hold_ready_pr "$existing_pr" "$existing_head"; then
echo "::error::Existing PR #$existing_pr changed during signed commit preparation."
echo "::error::Refusing to overwrite the changed head."
exit 1
Expand Down Expand Up @@ -1409,7 +1447,6 @@ jobs:
else
pr_url=$(gh pr create \
--head "$branch_name" \
--draft \
--title "chore: sync workflow templates" \
--body "$pr_body" \
--label "sync,automated,sync:delivery-staging")
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/reusable-16-agents.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ on:
default: 'agent:codex'
type: string
draft_pr:
description: 'Open bootstrap PRs as draft (true/false)'
description: 'Deprecated compatibility input; ignored because automation PRs are always ready'
required: false
default: 'false'
type: string
Expand Down Expand Up @@ -969,7 +969,6 @@ jobs:
with:
issue: ${{ fromJson(steps.ready.outputs.issue_numbers_json)[0] }}
service_bot_pat: ${{ secrets.service_bot_pat || '' }}
draft: ${{ inputs.draft_pr }}

watchdog:
if: inputs.enable_watchdog == 'true'
Expand Down
Loading
Loading