mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-08-12 08:50:38 +00:00
ci: test ci with the workflow file
This commit is contained in:
parent
fc9f41839c
commit
5b780addba
57
.github/workflows/issue-manage.yml
vendored
57
.github/workflows/issue-manage.yml
vendored
@ -6,8 +6,6 @@ permissions:
|
|||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, labeled]
|
types: [opened, labeled]
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
welcome-new-issues:
|
welcome-new-issues:
|
||||||
@ -28,61 +26,6 @@ jobs:
|
|||||||
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
||||||
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
||||||
|
|
||||||
triage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
steps:
|
|
||||||
- name: Check account age
|
|
||||||
id: check
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const user = await github.rest.users.getByUsername({
|
|
||||||
username: context.payload.issue.user.login
|
|
||||||
});
|
|
||||||
const created = new Date(user.data.created_at);
|
|
||||||
const days = (Date.now() - created) / (1000 * 60 * 60 * 24);
|
|
||||||
return days >= 30;
|
|
||||||
result-encoding: string
|
|
||||||
|
|
||||||
- name: Generate bot token
|
|
||||||
id: token
|
|
||||||
uses: actions/create-github-app-token@v1
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.APP_ID }}
|
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
||||||
owner: ${{ github.repository_owner }}
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
token: ${{ steps.token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Configure git identity
|
|
||||||
run: |
|
|
||||||
git config --global user.name "takanashi-hoshino-agent[bot]"
|
|
||||||
git config --global user.email "takanashi-hoshino-agent[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Run opencode
|
|
||||||
uses: anomalyco/opencode/github@latest
|
|
||||||
env:
|
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
|
|
||||||
with:
|
|
||||||
model: opencode/deepseek-v4-flash-free
|
|
||||||
use_github_token: true
|
|
||||||
prompt: |
|
|
||||||
Review this issue. If there's a clear fix or relevant docs:
|
|
||||||
- Provide documentation links
|
|
||||||
- Add error handling guidance for code examples
|
|
||||||
Otherwise, do not comment.
|
|
||||||
${{ secrets.OPENCODE_PROMPT }}
|
|
||||||
|
|
||||||
stale-issues:
|
stale-issues:
|
||||||
if: github.event.action == 'opened'
|
if: github.event.action == 'opened'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
59
.github/workflows/opencode-auto.yml
vendored
Normal file
59
.github/workflows/opencode-auto.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
name: OpenCode Automation
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: auto-${{ github.event.issue.number || github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automate:
|
||||||
|
# 排除机器人自己的触发,防止自我循环
|
||||||
|
if: |
|
||||||
|
github.actor != 'takanashi-hoshino-agent[bot]' &&
|
||||||
|
github.actor != 'github-actions[bot]' &&
|
||||||
|
github.actor != 'opencode-agent[bot]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Generate bot token
|
||||||
|
id: token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: npm i -g opencode-ai@latest
|
||||||
|
|
||||||
|
- name: Run automation
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
|
MODEL: opencode/deepseek-v4-flash-free
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
ISSUE_PROMPT: ${{ secrets.OPENCODE_PROMPT }}
|
||||||
|
PR_PROMPT: |
|
||||||
|
${{ secrets.OPENCODE_PROMPT }}
|
||||||
|
${{ secrets.PR_REVIEW_PROMPT }}
|
||||||
|
run: node scripts/opencode-automation.mjs
|
||||||
11
.github/workflows/opencode.yml
vendored
11
.github/workflows/opencode.yml
vendored
@ -14,10 +14,13 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
bot:
|
bot:
|
||||||
if: |
|
if: |
|
||||||
contains(github.event.comment.body, ' /oc') ||
|
(contains(github.event.comment.body, ' /oc') ||
|
||||||
startsWith(github.event.comment.body, '/oc') ||
|
startsWith(github.event.comment.body, '/oc') ||
|
||||||
contains(github.event.comment.body, ' /opencode') ||
|
contains(github.event.comment.body, ' /opencode') ||
|
||||||
startsWith(github.event.comment.body, '/opencode')
|
startsWith(github.event.comment.body, '/opencode')) &&
|
||||||
|
github.actor != 'opencode-agent[bot]' &&
|
||||||
|
github.actor != 'takanashi-hoshino-agent[bot]' &&
|
||||||
|
github.actor != 'github-actions[bot]'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
50
.github/workflows/reviewer.yml
vendored
50
.github/workflows/reviewer.yml
vendored
@ -1,50 +0,0 @@
|
|||||||
name: PR Reviewer
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
review:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
steps:
|
|
||||||
- name: Generate bot token
|
|
||||||
id: token
|
|
||||||
uses: actions/create-github-app-token@v1
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.APP_ID }}
|
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
||||||
owner: ${{ github.repository_owner }}
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
token: ${{ steps.token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Configure git identity
|
|
||||||
run: |
|
|
||||||
git config --global user.name "takanashi-hoshino-agent[bot]"
|
|
||||||
git config --global user.email "takanashi-hoshino-agent[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Run opencode
|
|
||||||
uses: anomalyco/opencode/github@latest
|
|
||||||
env:
|
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
|
|
||||||
with:
|
|
||||||
model: opencode/deepseek-v4-flash-free
|
|
||||||
use_github_token: true
|
|
||||||
prompt: |
|
|
||||||
${{ secrets.PR_REVIEW_PROMPT }}
|
|
||||||
${{ secrets.OPENCODE_PROMPT }}
|
|
||||||
204
scripts/opencode-automation.mjs
Normal file
204
scripts/opencode-automation.mjs
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
const eventName = process.env.GITHUB_EVENT_NAME
|
||||||
|
const eventPath = process.env.GITHUB_EVENT_PATH
|
||||||
|
const token = process.env.GITHUB_TOKEN
|
||||||
|
const repo = process.env.GITHUB_REPOSITORY
|
||||||
|
const model = process.env.MODEL || 'opencode/deepseek-v4-flash-free'
|
||||||
|
const botLogin = process.env.BOT_LOGIN || 'takanashi-hoshino-agent[bot]'
|
||||||
|
|
||||||
|
if (!eventPath || !token || !repo) {
|
||||||
|
console.error(
|
||||||
|
'Missing required env: GITHUB_EVENT_PATH / GITHUB_TOKEN / GITHUB_REPOSITORY',
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
if (!process.env.OPENCODE_API_KEY) {
|
||||||
|
console.error('Missing OPENCODE_API_KEY')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = JSON.parse(readFileSync(eventPath, 'utf8'))
|
||||||
|
const [owner, repoName] = repo.split('/')
|
||||||
|
const api = `https://api.github.com/repos/${owner}/${repoName}`
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
Accept: 'application/vnd.github+json',
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
|
'User-Agent': 'opencode-automation',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(path) {
|
||||||
|
const res = await fetch(`${api}${path}`, { headers })
|
||||||
|
if (!res.ok) throw new Error(`GET ${path}: HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(path, body) {
|
||||||
|
const res = await fetch(`${api}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
if (!res.ok) throw new Error(`POST ${path}: HTTP ${res.status} ${text}`)
|
||||||
|
return text ? JSON.parse(text) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 无头模式跑 opencode,返回模型输出的文本 */
|
||||||
|
function runOpencode(prompt, contextFile) {
|
||||||
|
const args = ['run', '--auto', '-m', model, '--format', 'default', prompt]
|
||||||
|
if (contextFile) args.push('-f', contextFile)
|
||||||
|
console.log(
|
||||||
|
`Running: opencode run --auto -m ${model} (prompt ${prompt.length} chars${contextFile ? ', context attached' : ''})`,
|
||||||
|
)
|
||||||
|
const res = spawnSync('opencode', args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
maxBuffer: 128 * 1024 * 1024,
|
||||||
|
})
|
||||||
|
if (res.error) throw res.error
|
||||||
|
if (res.status !== 0) {
|
||||||
|
if (res.stderr) console.error('opencode stderr:\n' + res.stderr)
|
||||||
|
throw new Error(`opencode exited with code ${res.status}`)
|
||||||
|
}
|
||||||
|
return (res.stdout || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 该 issue/PR 是否已有 bot 评论(用于去重,避免 synchronize 重复评论) */
|
||||||
|
async function hasBotComment(target) {
|
||||||
|
const comments = await get(`/issues/${target}/comments?per_page=100`)
|
||||||
|
return comments.some((c) => c.user?.login === botLogin)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function comment(target, body) {
|
||||||
|
if (!body) {
|
||||||
|
console.log('No output from agent, skip comment')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (body.length > 60000) body = body.slice(0, 60000) + '\n\n_...(truncated)_'
|
||||||
|
await post(`/issues/${target}/comments`, { body })
|
||||||
|
console.log(`Commented on #${target}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIssue(issue) {
|
||||||
|
const n = issue.number
|
||||||
|
console.log(`Handling issue #${n}`)
|
||||||
|
if (await hasBotComment(n)) {
|
||||||
|
console.log(`Already handled by bot, skip`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = await get(`/issues/${n}/comments?per_page=100`)
|
||||||
|
const thread = comments
|
||||||
|
.filter((c) => c.user?.login !== botLogin)
|
||||||
|
.map(
|
||||||
|
(c) =>
|
||||||
|
`- **${c.user?.login}** (${c.created_at}):\n ${(c.body || '').replace(/\n/g, '\n ')}`,
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
const ctx = [
|
||||||
|
`# Issue #${n}: ${issue.title}`,
|
||||||
|
``,
|
||||||
|
`- State: ${issue.state}`,
|
||||||
|
`- Author: ${issue.user?.login}`,
|
||||||
|
`- Created: ${issue.created_at}`,
|
||||||
|
``,
|
||||||
|
`## Body`,
|
||||||
|
``,
|
||||||
|
issue.body || '(empty)',
|
||||||
|
...(thread ? [`\n## Comments\n\n${thread}`] : []),
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const ctxFile = join(tmpdir(), 'opencode-issue-context.md')
|
||||||
|
writeFileSync(ctxFile, ctx)
|
||||||
|
|
||||||
|
const prompt =
|
||||||
|
process.env.ISSUE_PROMPT ||
|
||||||
|
[
|
||||||
|
`You are a maintainer bot for the ${repo} repository.`,
|
||||||
|
`Read the attached file for the issue context.`,
|
||||||
|
`If the issue has a clear fix or points to relevant docs, reply with documentation links and/or`,
|
||||||
|
`error-handling guidance for code examples. If nothing actionable, reply with exactly "NO_ACTION".`,
|
||||||
|
`Do NOT modify any files, do NOT commit, do NOT push. Output only the comment text.`,
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const output = runOpencode(prompt, ctxFile)
|
||||||
|
if (output === 'NO_ACTION' || output.startsWith('NO_ACTION')) {
|
||||||
|
console.log('Agent decided no action needed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await comment(n, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePullRequest(pr) {
|
||||||
|
const n = pr.number
|
||||||
|
console.log(`Handling PR #${n}`)
|
||||||
|
if (await hasBotComment(n)) {
|
||||||
|
console.log(`Already handled by bot, skip`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = []
|
||||||
|
try {
|
||||||
|
files = await get(`/pulls/${n}/files?per_page=100`)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to fetch changed files: ${e.message}`)
|
||||||
|
}
|
||||||
|
const fileList = files
|
||||||
|
.map((f) => `- ${f.status} ${f.filename} (+${f.additions}/-${f.deletions})`)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
const ctx = [
|
||||||
|
`# PR #${n}: ${pr.title}`,
|
||||||
|
``,
|
||||||
|
`- State: ${pr.state}`,
|
||||||
|
`- Author: ${pr.user?.login}`,
|
||||||
|
`- Base: ${pr.base?.ref} <- Head: ${pr.head?.ref}`,
|
||||||
|
`- Created: ${pr.created_at}`,
|
||||||
|
`- Stats: +${pr.additions}/-${pr.deletions}, ${pr.changed_files} files`,
|
||||||
|
``,
|
||||||
|
`## Body`,
|
||||||
|
``,
|
||||||
|
pr.body || '(empty)',
|
||||||
|
...(fileList ? [`\n## Changed files\n\n${fileList}`] : []),
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const ctxFile = join(tmpdir(), 'opencode-pr-context.md')
|
||||||
|
writeFileSync(ctxFile, ctx)
|
||||||
|
|
||||||
|
const prompt =
|
||||||
|
process.env.PR_PROMPT ||
|
||||||
|
[
|
||||||
|
`You are a maintainer bot for the ${repo} repository.`,
|
||||||
|
`Read the attached file for the PR context. The PR branch is already checked out.`,
|
||||||
|
`Run \`git diff HEAD^1 HEAD\` (or \`git diff origin/${pr.base?.ref}...HEAD\`) to see the changes.`,
|
||||||
|
`Review the code changes: point out bugs, risks and improvements, and suggest fixes for obvious issues.`,
|
||||||
|
`If the PR looks good, say so concisely.`,
|
||||||
|
`Do NOT modify any files, do NOT commit, do NOT push. Output only the review comment text.`,
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const output = runOpencode(prompt, ctxFile)
|
||||||
|
await comment(n, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`Event: ${eventName} on ${repo}`)
|
||||||
|
if (eventName === 'issues') {
|
||||||
|
await handleIssue(event.issue)
|
||||||
|
} else if (eventName === 'pull_request') {
|
||||||
|
await handlePullRequest(event.pull_request)
|
||||||
|
} else {
|
||||||
|
console.log(`Unhandled event type: ${eventName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(`Automation failed: ${e.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user