mirror of
https://github.com/JackChen-me/open-multi-agent.git
synced 2026-10-22 11:02:59 -05:00
feat: activate maintainer bot via GitHub Actions
This commit is contained in:
1 parent
2f1198f4f1
commit
098371cf0c
38 files changed
+4678
-39
No files matched your search
@@ -18,10 +18,40 @@ Steps to reproduce the behavior:
|
||||
2. Call `runTeam(...)` with '...'
|
||||
3. See error
|
||||
|
||||
## Current behavior
|
||||
|
||||
Describe the observed behavior or the deterministic failing-test procedure.
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear description of what you expected to happen.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- State each objectively verifiable result as a separate bullet.
|
||||
- Include the focused test or command outcome when one is known.
|
||||
|
||||
## Target paths
|
||||
|
||||
- `path/to/exact-file.test.ts`
|
||||
|
||||
Use normalized repository-relative files or directories. Do not use globs. Issue
|
||||
text never grants edit authority by itself: only a write-authorized maintainer's
|
||||
later `agent-ready` label can authorize this exact revision, and trusted policy
|
||||
may narrow or reject the scope.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- List code, behavior, public APIs, dependencies, or workspaces that must not change.
|
||||
|
||||
## Open decisions
|
||||
|
||||
None.
|
||||
|
||||
## Blockers
|
||||
|
||||
None.
|
||||
|
||||
## Error output
|
||||
|
||||
```
|
||||
|
||||
@@ -21,6 +21,43 @@ assignees: ''
|
||||
|
||||
A clear description of the problem or limitation you're experiencing.
|
||||
|
||||
## Current behavior
|
||||
|
||||
Describe what the repository does today and why that is insufficient.
|
||||
|
||||
## Expected behavior
|
||||
|
||||
Describe the bounded behavior that should exist after this issue is complete.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- State each objectively verifiable result as a separate bullet.
|
||||
- Include the focused test or command outcome when one is known.
|
||||
|
||||
## Target paths
|
||||
|
||||
- `path/to/exact-file.ts`
|
||||
|
||||
Use normalized repository-relative files or directories. Do not use globs. Issue
|
||||
text never grants edit authority by itself: only a write-authorized maintainer's
|
||||
later `agent-ready` label can authorize this exact revision, and trusted policy
|
||||
may narrow or reject the scope.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- List code, behavior, public APIs, dependencies, or workspaces that must not change.
|
||||
|
||||
## Open decisions
|
||||
|
||||
None.
|
||||
|
||||
<!-- If a product or architecture decision remains open, describe it here. The
|
||||
Maintainer Bot will route the issue to manual handling. -->
|
||||
|
||||
## Blockers
|
||||
|
||||
None.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
Describe what you'd like to happen.
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
name: OMA Maintainer Bot
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: oma-maintainer-${{ github.repository }}-issue-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
if: >-
|
||||
github.repository == 'open-multi-agent/open-multi-agent' &&
|
||||
github.event.label.name == 'agent-ready'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Publish immediate STARTED status and pin the default branch
|
||||
id: started
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const marker = 'oma-maintainer-bot-status:v1'
|
||||
const botId = 41898282
|
||||
const repository = context.repo.owner + '/' + context.repo.repo
|
||||
const issueNumber = context.issue.number
|
||||
const runUrl = `${context.serverUrl}/${repository}/actions/runs/${context.runId}`
|
||||
const claimId = `${context.runId}.${process.env.GITHUB_RUN_ATTEMPT}`
|
||||
const repo = await github.rest.repos.get(context.repo)
|
||||
const issue = await github.rest.issues.get({
|
||||
...context.repo,
|
||||
issue_number: issueNumber,
|
||||
})
|
||||
const eventIssue = context.payload.issue
|
||||
const labelNames = labels => [...new Set(labels.map(label => typeof label === 'string' ? label : label.name))].sort()
|
||||
const eventSnapshotMatched = issue.data.number === eventIssue.number &&
|
||||
issue.data.title === eventIssue.title &&
|
||||
(issue.data.body ?? '') === (eventIssue.body ?? '') &&
|
||||
issue.data.state === eventIssue.state &&
|
||||
issue.data.updated_at === eventIssue.updated_at &&
|
||||
issue.data.user?.login === eventIssue.user?.login &&
|
||||
JSON.stringify(labelNames(issue.data.labels)) === JSON.stringify(labelNames(eventIssue.labels))
|
||||
const branch = await github.rest.repos.getBranch({
|
||||
...context.repo,
|
||||
branch: repo.data.default_branch,
|
||||
})
|
||||
const baseSha = branch.data.commit.sha
|
||||
const metadata = {
|
||||
version: 1,
|
||||
repository,
|
||||
issueNumber,
|
||||
status: 'STARTED',
|
||||
claimId,
|
||||
actionsRunId: Number(context.runId),
|
||||
runUrl,
|
||||
baseSha,
|
||||
issueRevision: null,
|
||||
runKey: null,
|
||||
branch: null,
|
||||
pullRequestUrl: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
claims: [],
|
||||
}
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
...context.repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
const trusted = comments.filter(comment =>
|
||||
comment.user?.id === botId &&
|
||||
comment.user?.login === 'github-actions[bot]' &&
|
||||
comment.user?.type === 'Bot' &&
|
||||
comment.body?.includes(marker))
|
||||
if (trusted.length > 1) throw new Error('Multiple trusted Maintainer Bot status comments exist.')
|
||||
let commentId = trusted[0]?.id
|
||||
if (commentId === undefined) {
|
||||
const body = `<!-- ${marker} ${JSON.stringify(metadata)} -->\n## OMA Maintainer Bot — STARTED\n\n- Actions run: [${context.runId}](${runUrl})\n- Base SHA: \`${baseSha}\`\n- Issue revision: \`pending\`\n\nThe label event was received. Deterministic authorization, revision, scope, duplicate, and readiness checks are starting.\n\n_This bot creates Draft PRs only. It never approves, merges, closes, releases, publishes, tags, or deploys._\n`
|
||||
const created = await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
})
|
||||
commentId = created.data.id
|
||||
} else {
|
||||
const matches = [...trusted[0].body.matchAll(new RegExp(`<!--\\s*${marker}\\s+(\\{[^\\n]*\\})\\s*-->`, 'g'))]
|
||||
if (matches.length !== 1) throw new Error('Trusted Maintainer Bot comment has invalid machine metadata.')
|
||||
const previous = JSON.parse(matches[0][1])
|
||||
if (previous.repository !== repository || previous.issueNumber !== issueNumber) {
|
||||
throw new Error('Trusted Maintainer Bot comment belongs to a different issue.')
|
||||
}
|
||||
const body = `${matches[0][0]}\n## OMA Maintainer Bot — STARTED\n\n- Actions run: [${context.runId}](${runUrl})\n- Base SHA: \`${baseSha}\`\n- Issue revision: \`pending\`\n- Previous durable status: \`${previous.status}\`\n\nThe new label event was received. The prior machine claim remains authoritative until deterministic duplicate, stale, permission, revision, and readiness checks finish.\n\n_This bot creates Draft PRs only. It never approves, merges, closes, releases, publishes, tags, or deploys._\n`
|
||||
await github.rest.issues.updateComment({
|
||||
...context.repo,
|
||||
comment_id: commentId,
|
||||
body,
|
||||
})
|
||||
}
|
||||
core.setOutput('base_sha', baseSha)
|
||||
core.setOutput('claim_id', claimId)
|
||||
core.setOutput('comment_id', String(commentId))
|
||||
core.setOutput('run_url', runUrl)
|
||||
core.setOutput('event_snapshot_matched', String(eventSnapshotMatched))
|
||||
|
||||
- name: Check out the trusted default-branch commit
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
ref: ${{ steps.started.outputs.base_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up pinned Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.23.1
|
||||
cache: npm
|
||||
|
||||
- name: Verify pinned npm version
|
||||
run: test "$(npm --version)" = "10.9.8"
|
||||
|
||||
- name: Install locked dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build the deterministic host and OMA engine
|
||||
id: build
|
||||
run: npm run build -w @open-multi-agent/maintainer-host
|
||||
|
||||
- name: Re-fetch, authorize, and claim the candidate run
|
||||
id: prepare
|
||||
env:
|
||||
MAINTAINER_BOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OMA_CLAIM_ID: ${{ steps.started.outputs.claim_id }}
|
||||
OMA_RUN_URL: ${{ steps.started.outputs.run_url }}
|
||||
OMA_BASE_SHA: ${{ steps.started.outputs.base_sha }}
|
||||
OMA_EVENT_SNAPSHOT_MATCHED: ${{ steps.started.outputs.event_snapshot_matched }}
|
||||
OMA_PR_CREATION_ATTESTED: ${{ vars.OMA_MAINTAINER_BOT_PR_CREATION_ENABLED }}
|
||||
run: >-
|
||||
node packages/maintainer-host/dist/cli.js prepare
|
||||
--event "$GITHUB_EVENT_PATH"
|
||||
--policy packages/maintainer-host/config/production-policy.json
|
||||
--repo "$GITHUB_WORKSPACE"
|
||||
--event-id "${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}"
|
||||
--claim-id "$OMA_CLAIM_ID"
|
||||
--actions-run-id "$GITHUB_RUN_ID"
|
||||
--run-url "$OMA_RUN_URL"
|
||||
--base-sha "$OMA_BASE_SHA"
|
||||
--event-snapshot-matched "$OMA_EVENT_SNAPSHOT_MATCHED"
|
||||
--pr-creation-attested "$OMA_PR_CREATION_ATTESTED"
|
||||
--activation-out "$RUNNER_TEMP/oma-maintainer/activation.json"
|
||||
|
||||
- name: Run OMA and DeepSeek in a credential-isolated child process
|
||||
id: engine
|
||||
if: steps.prepare.outcome == 'success'
|
||||
continue-on-error: true
|
||||
env:
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
||||
run: >-
|
||||
node packages/maintainer-host/dist/cli.js run-engine
|
||||
--activation "$RUNNER_TEMP/oma-maintainer/activation.json"
|
||||
--result-out "$RUNNER_TEMP/oma-maintainer/engine-result.json"
|
||||
--state-dir "$RUNNER_TEMP/oma-maintainer/state"
|
||||
--artifact-dir "$RUNNER_TEMP/oma-maintainer/artifacts"
|
||||
--maintainer-bot-cli packages/maintainer-bot/dist/cli.js
|
||||
--repo "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Revalidate and create at most one Draft PR
|
||||
id: finalize
|
||||
if: always() && steps.build.outcome == 'success' && steps.prepare.outcome == 'success'
|
||||
env:
|
||||
MAINTAINER_BOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OMA_PR_CREATION_ATTESTED: ${{ vars.OMA_MAINTAINER_BOT_PR_CREATION_ENABLED }}
|
||||
run: >-
|
||||
node packages/maintainer-host/dist/cli.js finalize
|
||||
--activation "$RUNNER_TEMP/oma-maintainer/activation.json"
|
||||
--engine-result "$RUNNER_TEMP/oma-maintainer/engine-result.json"
|
||||
--event "$GITHUB_EVENT_PATH"
|
||||
--policy packages/maintainer-host/config/production-policy.json
|
||||
--state-dir "$RUNNER_TEMP/oma-maintainer/state"
|
||||
--artifact-dir "$RUNNER_TEMP/oma-maintainer/artifacts"
|
||||
--repo "$GITHUB_WORKSPACE"
|
||||
--pr-creation-attested "$OMA_PR_CREATION_ATTESTED"
|
||||
--final-out "$RUNNER_TEMP/oma-maintainer/final.json"
|
||||
|
||||
- name: Publish infrastructure failure fallback
|
||||
if: always() && steps.finalize.outcome != 'success'
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
env:
|
||||
OMA_BASE_SHA: ${{ steps.started.outputs.base_sha }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const marker = 'oma-maintainer-bot-status:v1'
|
||||
const repository = context.repo.owner + '/' + context.repo.repo
|
||||
const issueNumber = context.issue.number
|
||||
const claimId = `${context.runId}.${process.env.GITHUB_RUN_ATTEMPT}`
|
||||
const runUrl = `${context.serverUrl}/${repository}/actions/runs/${context.runId}`
|
||||
const baseSha = process.env.OMA_BASE_SHA || context.sha
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
...context.repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
const trusted = comments.filter(comment =>
|
||||
comment.user?.id === 41898282 &&
|
||||
comment.user?.login === 'github-actions[bot]' &&
|
||||
comment.user?.type === 'Bot' &&
|
||||
comment.body?.includes(marker))
|
||||
if (trusted.length > 1) throw new Error('Multiple trusted Maintainer Bot status comments exist.')
|
||||
const current = trusted[0]
|
||||
let previous = null
|
||||
if (current !== undefined) {
|
||||
const matches = [...current.body.matchAll(new RegExp(`<!--\\s*${marker}\\s+(\\{[^\\n]*\\})\\s*-->`, 'g'))]
|
||||
if (matches.length === 1) {
|
||||
try { previous = JSON.parse(matches[0][1]) } catch { previous = null }
|
||||
}
|
||||
}
|
||||
const sameClaim = previous?.claimId === claimId && previous?.runKey
|
||||
const runningClaim = sameClaim && (previous.status === 'STARTED' || previous.status === 'RUNNING')
|
||||
const terminalClaim = sameClaim && !runningClaim
|
||||
const status = runningClaim ? 'NEEDS_HUMAN' : terminalClaim ? previous.status : 'FAILED'
|
||||
const metadata = {
|
||||
version: 1,
|
||||
repository,
|
||||
issueNumber,
|
||||
status,
|
||||
claimId,
|
||||
actionsRunId: Number(context.runId),
|
||||
runUrl,
|
||||
baseSha: sameClaim ? previous.baseSha : baseSha,
|
||||
issueRevision: sameClaim ? previous.issueRevision : null,
|
||||
runKey: sameClaim ? previous.runKey : null,
|
||||
branch: sameClaim ? previous.branch : null,
|
||||
pullRequestUrl: sameClaim ? previous.pullRequestUrl : null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
const claims = new Map()
|
||||
const add = value => {
|
||||
if (!value || !value.claimId) return
|
||||
const claim = {
|
||||
status: value.status,
|
||||
claimId: value.claimId,
|
||||
actionsRunId: value.actionsRunId,
|
||||
runUrl: value.runUrl,
|
||||
baseSha: value.baseSha,
|
||||
issueRevision: value.issueRevision ?? null,
|
||||
runKey: value.runKey ?? null,
|
||||
branch: value.branch ?? null,
|
||||
pullRequestUrl: value.pullRequestUrl ?? null,
|
||||
updatedAt: value.updatedAt,
|
||||
}
|
||||
claims.set(claim.runKey ?? `pending:${claim.claimId}`, claim)
|
||||
}
|
||||
if (Array.isArray(previous?.claims)) previous.claims.forEach(add)
|
||||
add(previous)
|
||||
if (metadata.runKey) claims.delete(`pending:${metadata.claimId}`)
|
||||
add(metadata)
|
||||
metadata.claims = [...claims.values()]
|
||||
if (metadata.claims.length > 64) throw new Error('Durable claim ledger reached safe capacity.')
|
||||
const revision = metadata.issueRevision === null ? '`not resolved`' : `\`${metadata.issueRevision}\``
|
||||
const detail = runningClaim
|
||||
? 'The control layer stopped after a durable RUNNING claim. Automatic model resume is forbidden; inspect this run and reauthorize a fresh revision or base.'
|
||||
: terminalClaim
|
||||
? `The control layer had already published terminal state ${previous.status}; this fallback preserved that authoritative terminal result.`
|
||||
: 'The control layer failed before it established a durable RUNNING claim. Inspect the linked run; no Draft PR was authorized by this fallback.'
|
||||
const body = `<!-- ${marker} ${JSON.stringify(metadata)} -->\n## OMA Maintainer Bot — ${status}\n\n- Actions run: [${context.runId}](${runUrl})\n- Base SHA: \`${metadata.baseSha}\`\n- Issue revision: ${revision}\n\n${detail}\n\n_This bot creates Draft PRs only. It never approves, merges, closes, releases, publishes, tags, or deploys._\n`
|
||||
if (current !== undefined) {
|
||||
await github.rest.issues.updateComment({ ...context.repo, comment_id: current.id, body })
|
||||
} else {
|
||||
await github.rest.issues.createComment({ ...context.repo, issue_number: issueNumber, body })
|
||||
}
|
||||
await core.summary
|
||||
.addHeading(`OMA Maintainer Bot — ${status}`)
|
||||
.addLink(`Actions run ${context.runId}`, runUrl)
|
||||
.addRaw(`\n${detail}\n`)
|
||||
.write()
|
||||
|
||||
- name: Mark validated FAILED terminal as an unsuccessful run
|
||||
if: always() && steps.finalize.outcome == 'success' && steps.finalize.outputs.terminal_status == 'FAILED'
|
||||
run: exit 1
|
||||
Reference in new issue
Block a user