mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-10 14:11:53 +08:00
ci: pr build
This commit is contained in:
parent
cd495fc7a0
commit
95f4a4d37e
121
.github/scripts/lib/comment.ts
vendored
Normal file
121
.github/scripts/lib/comment.ts
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* 构建状态评论模板
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const COMMENT_MARKER = '<!-- napcat-pr-build -->';
|
||||||
|
|
||||||
|
export type BuildStatus = 'success' | 'failure' | 'cancelled' | 'pending' | 'unknown';
|
||||||
|
|
||||||
|
export interface BuildTarget {
|
||||||
|
name: string;
|
||||||
|
status: BuildStatus;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 辅助函数 ==============
|
||||||
|
|
||||||
|
function formatSha (sha: string): string {
|
||||||
|
return sha && sha.length >= 7 ? sha.substring(0, 7) : sha || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeCodeBlock (text: string): string {
|
||||||
|
// 替换 ``` 为转义形式,避免破坏 Markdown 代码块
|
||||||
|
return text.replace(/```/g, '\\`\\`\\`');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 状态图标 ==============
|
||||||
|
|
||||||
|
export function getStatusIcon (status: BuildStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'success':
|
||||||
|
return '✅ 成功';
|
||||||
|
case 'pending':
|
||||||
|
return '⏳ 构建中...';
|
||||||
|
case 'cancelled':
|
||||||
|
return '⚪ 已取消';
|
||||||
|
default:
|
||||||
|
return '❌ 失败';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 构建中评论 ==============
|
||||||
|
|
||||||
|
export function generateBuildingComment (prSha: string, targets: string[]): string {
|
||||||
|
const time = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
COMMENT_MARKER,
|
||||||
|
'## 🔨 构建状态',
|
||||||
|
'',
|
||||||
|
'| 构建目标 | 状态 |',
|
||||||
|
'| :--- | :--- |',
|
||||||
|
...targets.map(name => `| ${name} | ⏳ 构建中... |`),
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
`📝 **提交**: \`${formatSha(prSha)}\``,
|
||||||
|
`🕐 **开始时间**: ${time}`,
|
||||||
|
'',
|
||||||
|
'> 构建进行中,请稍候...',
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 构建结果评论 ==============
|
||||||
|
|
||||||
|
export function generateResultComment (
|
||||||
|
targets: BuildTarget[],
|
||||||
|
prSha: string,
|
||||||
|
runId: string,
|
||||||
|
repository: string
|
||||||
|
): string {
|
||||||
|
const artifactUrl = `https://github.com/${repository}/actions/runs/${runId}/artifacts`;
|
||||||
|
const logUrl = `https://github.com/${repository}/actions/runs/${runId}`;
|
||||||
|
|
||||||
|
const allSuccess = targets.every(t => t.status === 'success');
|
||||||
|
const anyCancelled = targets.some(t => t.status === 'cancelled');
|
||||||
|
|
||||||
|
const headerIcon = allSuccess
|
||||||
|
? '✅ 构建成功'
|
||||||
|
: anyCancelled
|
||||||
|
? '⚪ 构建已取消'
|
||||||
|
: '❌ 构建失败';
|
||||||
|
|
||||||
|
const downloadLink = (status: BuildStatus) =>
|
||||||
|
status === 'success' ? `[📦 下载](${artifactUrl})` : '—';
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
COMMENT_MARKER,
|
||||||
|
`## ${headerIcon}`,
|
||||||
|
'',
|
||||||
|
'| 构建目标 | 状态 | 下载 |',
|
||||||
|
'| :--- | :--- | :--- |',
|
||||||
|
...targets.map(t => `| ${t.name} | ${getStatusIcon(t.status)} | ${downloadLink(t.status)} |`),
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
`📝 **提交**: \`${formatSha(prSha)}\``,
|
||||||
|
`🔗 **构建日志**: [查看详情](${logUrl})`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 添加错误详情
|
||||||
|
const failedTargets = targets.filter(t => t.status === 'failure' && t.error);
|
||||||
|
if (failedTargets.length > 0) {
|
||||||
|
lines.push('', '---', '', '## ⚠️ 错误详情');
|
||||||
|
for (const target of failedTargets) {
|
||||||
|
lines.push('', `### ${target.name} 构建错误`, '```', escapeCodeBlock(target.error!), '```');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加底部提示
|
||||||
|
if (allSuccess) {
|
||||||
|
lines.push('', '> 🎉 所有构建均已成功完成,可点击上方下载链接获取构建产物进行测试。');
|
||||||
|
} else if (anyCancelled) {
|
||||||
|
lines.push('', '> ⚪ 构建已被取消,可能是由于新的提交触发了新的构建。');
|
||||||
|
} else {
|
||||||
|
lines.push('', '> ⚠️ 部分构建失败,请查看上方错误详情或点击构建日志查看完整输出。');
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
175
.github/scripts/lib/github.ts
vendored
Normal file
175
.github/scripts/lib/github.ts
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* GitHub API 工具库
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { appendFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
// ============== 类型定义 ==============
|
||||||
|
|
||||||
|
export interface PullRequest {
|
||||||
|
number: number;
|
||||||
|
state: string;
|
||||||
|
head: {
|
||||||
|
sha: string;
|
||||||
|
ref: string;
|
||||||
|
repo: {
|
||||||
|
full_name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Repository {
|
||||||
|
owner: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== GitHub API Client ==============
|
||||||
|
|
||||||
|
export class GitHubAPI {
|
||||||
|
private token: string;
|
||||||
|
private baseUrl = 'https://api.github.com';
|
||||||
|
|
||||||
|
constructor (token: string) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T> (endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.token}`,
|
||||||
|
Accept: 'application/vnd.github+json',
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPullRequest (owner: string, repo: string, pullNumber: number): Promise<PullRequest> {
|
||||||
|
return this.request<PullRequest>(`/repos/${owner}/${repo}/pulls/${pullNumber}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCollaboratorPermission (owner: string, repo: string, username: string): Promise<string> {
|
||||||
|
const data = await this.request<{ permission: string; }>(
|
||||||
|
`/repos/${owner}/${repo}/collaborators/${username}/permission`
|
||||||
|
);
|
||||||
|
return data.permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRepository (owner: string, repo: string): Promise<Repository> {
|
||||||
|
return this.request(`/repos/${owner}/${repo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkOrgMembership (org: string, username: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.request(`/orgs/${org}/members/${username}`);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createComment (owner: string, repo: string, issueNumber: number, body: string): Promise<void> {
|
||||||
|
await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ body }),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findComment (owner: string, repo: string, issueNumber: number, marker: string): Promise<number | null> {
|
||||||
|
let page = 1;
|
||||||
|
const perPage = 100;
|
||||||
|
|
||||||
|
while (page <= 10) { // 最多检查 1000 条评论
|
||||||
|
const comments = await this.request<Array<{ id: number, body: string; }>>(
|
||||||
|
`/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (comments.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = comments.find(c => c.body.includes(marker));
|
||||||
|
if (found) {
|
||||||
|
return found.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comments.length < perPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateComment (owner: string, repo: string, commentId: number, body: string): Promise<void> {
|
||||||
|
await this.request(`/repos/${owner}/${repo}/issues/comments/${commentId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ body }),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrUpdateComment (
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
issueNumber: number,
|
||||||
|
body: string,
|
||||||
|
marker: string
|
||||||
|
): Promise<void> {
|
||||||
|
const existingId = await this.findComment(owner, repo, issueNumber, marker);
|
||||||
|
if (existingId) {
|
||||||
|
await this.updateComment(owner, repo, existingId, body);
|
||||||
|
console.log(`✓ Updated comment #${existingId}`);
|
||||||
|
} else {
|
||||||
|
await this.createComment(owner, repo, issueNumber, body);
|
||||||
|
console.log('✓ Created new comment');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== Output 工具 ==============
|
||||||
|
|
||||||
|
export function setOutput (name: string, value: string): void {
|
||||||
|
const outputFile = process.env.GITHUB_OUTPUT;
|
||||||
|
if (outputFile) {
|
||||||
|
appendFileSync(outputFile, `${name}=${value}\n`);
|
||||||
|
}
|
||||||
|
console.log(` ${name}=${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMultilineOutput (name: string, value: string): void {
|
||||||
|
const outputFile = process.env.GITHUB_OUTPUT;
|
||||||
|
if (outputFile) {
|
||||||
|
const delimiter = `EOF_${Date.now()}`;
|
||||||
|
appendFileSync(outputFile, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 环境变量工具 ==============
|
||||||
|
|
||||||
|
export function getEnv (name: string, required: true): string;
|
||||||
|
export function getEnv (name: string, required?: false): string | undefined;
|
||||||
|
export function getEnv (name: string, required = false): string | undefined {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (required && !value) {
|
||||||
|
throw new Error(`Environment variable ${name} is required`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRepository (): { owner: string, repo: string; } {
|
||||||
|
const repository = getEnv('GITHUB_REPOSITORY', true);
|
||||||
|
const [owner, repo] = repository.split('/');
|
||||||
|
return { owner, repo };
|
||||||
|
}
|
||||||
36
.github/scripts/pr-build-building.ts
vendored
Normal file
36
.github/scripts/pr-build-building.ts
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* PR Build - 更新构建中状态评论
|
||||||
|
*
|
||||||
|
* 环境变量:
|
||||||
|
* - GITHUB_TOKEN: GitHub API Token
|
||||||
|
* - PR_NUMBER: PR 编号
|
||||||
|
* - PR_SHA: PR 提交 SHA
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
|
||||||
|
import { generateBuildingComment, COMMENT_MARKER } from './lib/comment.ts';
|
||||||
|
|
||||||
|
const BUILD_TARGETS = ['NapCat.Framework', 'NapCat.Shell'];
|
||||||
|
|
||||||
|
async function main (): Promise<void> {
|
||||||
|
console.log('🔨 Updating building status comment\n');
|
||||||
|
|
||||||
|
const token = getEnv('GITHUB_TOKEN', true);
|
||||||
|
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
|
||||||
|
const prSha = getEnv('PR_SHA', true);
|
||||||
|
const { owner, repo } = getRepository();
|
||||||
|
|
||||||
|
console.log(`PR: #${prNumber}`);
|
||||||
|
console.log(`SHA: ${prSha}`);
|
||||||
|
console.log(`Repo: ${owner}/${repo}\n`);
|
||||||
|
|
||||||
|
const github = new GitHubAPI(token);
|
||||||
|
const comment = generateBuildingComment(prSha, BUILD_TARGETS);
|
||||||
|
|
||||||
|
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('❌ Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
205
.github/scripts/pr-build-check.ts
vendored
Normal file
205
.github/scripts/pr-build-check.ts
vendored
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* PR Build Check Script
|
||||||
|
* 检查 PR 构建触发条件和用户权限
|
||||||
|
*
|
||||||
|
* 环境变量:
|
||||||
|
* - GITHUB_TOKEN: GitHub API Token
|
||||||
|
* - GITHUB_EVENT_NAME: 事件名称
|
||||||
|
* - GITHUB_EVENT_PATH: 事件 payload 文件路径
|
||||||
|
* - GITHUB_REPOSITORY: 仓库名称 (owner/repo)
|
||||||
|
* - GITHUB_OUTPUT: 输出文件路径
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { GitHubAPI, getEnv, getRepository, setOutput, PullRequest } from './lib/github.ts';
|
||||||
|
|
||||||
|
// ============== 类型定义 ==============
|
||||||
|
|
||||||
|
interface GitHubPayload {
|
||||||
|
pull_request?: PullRequest;
|
||||||
|
issue?: {
|
||||||
|
number: number;
|
||||||
|
pull_request?: object;
|
||||||
|
};
|
||||||
|
comment?: {
|
||||||
|
body: string;
|
||||||
|
user: { login: string; };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckResult {
|
||||||
|
should_build: boolean;
|
||||||
|
pr_number?: number;
|
||||||
|
pr_sha?: string;
|
||||||
|
pr_head_repo?: string;
|
||||||
|
pr_head_ref?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 权限检查 ==============
|
||||||
|
|
||||||
|
async function checkUserPermission (
|
||||||
|
github: GitHubAPI,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
username: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
// 方法1:检查仓库协作者权限
|
||||||
|
try {
|
||||||
|
const permission = await github.getCollaboratorPermission(owner, repo, username);
|
||||||
|
if (['admin', 'write', 'maintain'].includes(permission)) {
|
||||||
|
console.log(`✓ User ${username} has ${permission} permission`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log(`✗ User ${username} has ${permission} permission (insufficient)`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`✗ Failed to get collaborator permission: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法2:检查组织成员身份
|
||||||
|
try {
|
||||||
|
const repoInfo = await github.getRepository(owner, repo);
|
||||||
|
if (repoInfo.owner.type === 'Organization') {
|
||||||
|
const isMember = await github.checkOrgMembership(owner, username);
|
||||||
|
if (isMember) {
|
||||||
|
console.log(`✓ User ${username} is organization member`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log(`✗ User ${username} is not organization member`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`✗ Failed to check org membership: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 事件处理 ==============
|
||||||
|
|
||||||
|
function handlePullRequestTarget (payload: GitHubPayload): CheckResult {
|
||||||
|
const pr = payload.pull_request;
|
||||||
|
|
||||||
|
if (!pr) {
|
||||||
|
console.log('✗ No pull_request in payload');
|
||||||
|
return { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pr.state !== 'open') {
|
||||||
|
console.log(`✗ PR is not open (state: ${pr.state})`);
|
||||||
|
return { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ PR #${pr.number} is open, triggering build`);
|
||||||
|
return {
|
||||||
|
should_build: true,
|
||||||
|
pr_number: pr.number,
|
||||||
|
pr_sha: pr.head.sha,
|
||||||
|
pr_head_repo: pr.head.repo.full_name,
|
||||||
|
pr_head_ref: pr.head.ref,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIssueComment (
|
||||||
|
payload: GitHubPayload,
|
||||||
|
github: GitHubAPI,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<CheckResult> {
|
||||||
|
const { issue, comment } = payload;
|
||||||
|
|
||||||
|
if (!issue || !comment) {
|
||||||
|
console.log('✗ No issue or comment in payload');
|
||||||
|
return { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是 PR 的评论
|
||||||
|
if (!issue.pull_request) {
|
||||||
|
console.log('✗ Comment is not on a PR');
|
||||||
|
return { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是 /build 命令
|
||||||
|
if (!comment.body.trim().startsWith('/build')) {
|
||||||
|
console.log('✗ Comment is not a /build command');
|
||||||
|
return { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`→ /build command from @${comment.user.login}`);
|
||||||
|
|
||||||
|
// 获取 PR 详情
|
||||||
|
const pr = await github.getPullRequest(owner, repo, issue.number);
|
||||||
|
|
||||||
|
// 检查 PR 状态
|
||||||
|
if (pr.state !== 'open') {
|
||||||
|
console.log(`✗ PR is not open (state: ${pr.state})`);
|
||||||
|
await github.createComment(owner, repo, issue.number, '⚠️ 此 PR 已关闭,无法触发构建。');
|
||||||
|
return { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户权限
|
||||||
|
const username = comment.user.login;
|
||||||
|
const hasPermission = await checkUserPermission(github, owner, repo, username);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
console.log(`✗ User ${username} has no permission`);
|
||||||
|
await github.createComment(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue.number,
|
||||||
|
`⚠️ @${username} 您没有权限使用 \`/build\` 命令,仅仓库协作者或组织成员可使用。`
|
||||||
|
);
|
||||||
|
return { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Build triggered by @${username}`);
|
||||||
|
return {
|
||||||
|
should_build: true,
|
||||||
|
pr_number: issue.number,
|
||||||
|
pr_sha: pr.head.sha,
|
||||||
|
pr_head_repo: pr.head.repo.full_name,
|
||||||
|
pr_head_ref: pr.head.ref,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 主函数 ==============
|
||||||
|
|
||||||
|
async function main (): Promise<void> {
|
||||||
|
console.log('🔍 PR Build Check\n');
|
||||||
|
|
||||||
|
const token = getEnv('GITHUB_TOKEN', true);
|
||||||
|
const eventName = getEnv('GITHUB_EVENT_NAME', true);
|
||||||
|
const eventPath = getEnv('GITHUB_EVENT_PATH', true);
|
||||||
|
const { owner, repo } = getRepository();
|
||||||
|
|
||||||
|
console.log(`Event: ${eventName}`);
|
||||||
|
console.log(`Repository: ${owner}/${repo}\n`);
|
||||||
|
|
||||||
|
const payload = JSON.parse(readFileSync(eventPath, 'utf-8')) as GitHubPayload;
|
||||||
|
const github = new GitHubAPI(token);
|
||||||
|
|
||||||
|
let result: CheckResult;
|
||||||
|
|
||||||
|
switch (eventName) {
|
||||||
|
case 'pull_request_target':
|
||||||
|
result = handlePullRequestTarget(payload);
|
||||||
|
break;
|
||||||
|
case 'issue_comment':
|
||||||
|
result = await handleIssueComment(payload, github, owner, repo);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`✗ Unsupported event: ${eventName}`);
|
||||||
|
result = { should_build: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出结果
|
||||||
|
console.log('\n=== Outputs ===');
|
||||||
|
setOutput('should_build', String(result.should_build));
|
||||||
|
setOutput('pr_number', String(result.pr_number ?? ''));
|
||||||
|
setOutput('pr_sha', result.pr_sha ?? '');
|
||||||
|
setOutput('pr_head_repo', result.pr_head_repo ?? '');
|
||||||
|
setOutput('pr_head_ref', result.pr_head_ref ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('❌ Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
60
.github/scripts/pr-build-result.ts
vendored
Normal file
60
.github/scripts/pr-build-result.ts
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* PR Build - 更新构建结果评论
|
||||||
|
*
|
||||||
|
* 环境变量:
|
||||||
|
* - GITHUB_TOKEN: GitHub API Token
|
||||||
|
* - PR_NUMBER: PR 编号
|
||||||
|
* - PR_SHA: PR 提交 SHA
|
||||||
|
* - RUN_ID: GitHub Actions Run ID
|
||||||
|
* - FRAMEWORK_STATUS: Framework 构建状态
|
||||||
|
* - FRAMEWORK_ERROR: Framework 构建错误信息
|
||||||
|
* - SHELL_STATUS: Shell 构建状态
|
||||||
|
* - SHELL_ERROR: Shell 构建错误信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
|
||||||
|
import { generateResultComment, COMMENT_MARKER, BuildTarget, BuildStatus } from './lib/comment.ts';
|
||||||
|
|
||||||
|
function parseStatus (value: string | undefined): BuildStatus {
|
||||||
|
if (value === 'success' || value === 'failure' || value === 'cancelled') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main (): Promise<void> {
|
||||||
|
console.log('📝 Updating build result comment\n');
|
||||||
|
|
||||||
|
const token = getEnv('GITHUB_TOKEN', true);
|
||||||
|
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
|
||||||
|
const prSha = getEnv('PR_SHA') || 'unknown';
|
||||||
|
const runId = getEnv('RUN_ID', true);
|
||||||
|
const { owner, repo } = getRepository();
|
||||||
|
|
||||||
|
const frameworkStatus = parseStatus(getEnv('FRAMEWORK_STATUS'));
|
||||||
|
const frameworkError = getEnv('FRAMEWORK_ERROR');
|
||||||
|
const shellStatus = parseStatus(getEnv('SHELL_STATUS'));
|
||||||
|
const shellError = getEnv('SHELL_ERROR');
|
||||||
|
|
||||||
|
console.log(`PR: #${prNumber}`);
|
||||||
|
console.log(`SHA: ${prSha}`);
|
||||||
|
console.log(`Run: ${runId}`);
|
||||||
|
console.log(`Framework: ${frameworkStatus}${frameworkError ? ` (${frameworkError})` : ''}`);
|
||||||
|
console.log(`Shell: ${shellStatus}${shellError ? ` (${shellError})` : ''}\n`);
|
||||||
|
|
||||||
|
const targets: BuildTarget[] = [
|
||||||
|
{ name: 'NapCat.Framework', status: frameworkStatus, error: frameworkError },
|
||||||
|
{ name: 'NapCat.Shell', status: shellStatus, error: shellError },
|
||||||
|
];
|
||||||
|
|
||||||
|
const github = new GitHubAPI(token);
|
||||||
|
const repository = `${owner}/${repo}`;
|
||||||
|
const comment = generateResultComment(targets, prSha, runId, repository);
|
||||||
|
|
||||||
|
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('❌ Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
149
.github/scripts/pr-build-run.ts
vendored
Normal file
149
.github/scripts/pr-build-run.ts
vendored
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* PR Build Runner
|
||||||
|
* 执行构建步骤
|
||||||
|
*
|
||||||
|
* 用法: node pr-build-run.ts <target>
|
||||||
|
* target: framework | shell
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { existsSync, renameSync, unlinkSync } from 'node:fs';
|
||||||
|
import { setOutput } from './lib/github.ts';
|
||||||
|
|
||||||
|
type BuildTarget = 'framework' | 'shell';
|
||||||
|
|
||||||
|
interface BuildStep {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 构建步骤 ==============
|
||||||
|
|
||||||
|
function getCommonSteps (): BuildStep[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Install pnpm',
|
||||||
|
command: 'npm i -g pnpm',
|
||||||
|
errorMessage: 'Failed to install pnpm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Install dependencies',
|
||||||
|
command: 'pnpm i',
|
||||||
|
errorMessage: 'Failed to install dependencies',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Type check',
|
||||||
|
command: 'pnpm run typecheck',
|
||||||
|
errorMessage: 'Type check failed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Test',
|
||||||
|
command: 'pnpm test',
|
||||||
|
errorMessage: 'Tests failed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Build WebUI',
|
||||||
|
command: 'pnpm --filter napcat-webui-frontend run build',
|
||||||
|
errorMessage: 'WebUI build failed',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetSteps (target: BuildTarget): BuildStep[] {
|
||||||
|
if (target === 'framework') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Build Framework',
|
||||||
|
command: 'pnpm run build:framework',
|
||||||
|
errorMessage: 'Framework build failed',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Build Shell',
|
||||||
|
command: 'pnpm run build:shell',
|
||||||
|
errorMessage: 'Shell build failed',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 执行器 ==============
|
||||||
|
|
||||||
|
function runStep (step: BuildStep): boolean {
|
||||||
|
console.log(`\n::group::${step.name}`);
|
||||||
|
console.log(`> ${step.command}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(step.command, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
|
||||||
|
});
|
||||||
|
console.log('::endgroup::');
|
||||||
|
console.log(`✓ ${step.name}`);
|
||||||
|
return true;
|
||||||
|
} catch (_error) {
|
||||||
|
console.log('::endgroup::');
|
||||||
|
console.log(`✗ ${step.name}`);
|
||||||
|
setOutput('error', step.errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postBuild (target: BuildTarget): void {
|
||||||
|
const srcDir = target === 'framework'
|
||||||
|
? 'packages/napcat-framework/dist'
|
||||||
|
: 'packages/napcat-shell/dist';
|
||||||
|
const destDir = target === 'framework' ? 'framework-dist' : 'shell-dist';
|
||||||
|
|
||||||
|
console.log(`\n→ Moving ${srcDir} to ${destDir}`);
|
||||||
|
|
||||||
|
if (!existsSync(srcDir)) {
|
||||||
|
throw new Error(`Build output not found: ${srcDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
renameSync(srcDir, destDir);
|
||||||
|
|
||||||
|
// Install production dependencies
|
||||||
|
console.log('→ Installing production dependencies');
|
||||||
|
execSync('npm install --omit=dev', {
|
||||||
|
cwd: destDir,
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove package-lock.json
|
||||||
|
const lockFile = `${destDir}/package-lock.json`;
|
||||||
|
if (existsSync(lockFile)) {
|
||||||
|
unlinkSync(lockFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Build output ready at ${destDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 主函数 ==============
|
||||||
|
|
||||||
|
function main (): void {
|
||||||
|
const target = process.argv[2] as BuildTarget;
|
||||||
|
|
||||||
|
if (!target || !['framework', 'shell'].includes(target)) {
|
||||||
|
console.error('Usage: node pr-build-run.ts <framework|shell>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔨 Building NapCat.${target === 'framework' ? 'Framework' : 'Shell'}\n`);
|
||||||
|
|
||||||
|
const steps = [...getCommonSteps(), ...getTargetSteps(target)];
|
||||||
|
|
||||||
|
for (const step of steps) {
|
||||||
|
if (!runStep(step)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postBuild(target);
|
||||||
|
console.log('\n✅ Build completed successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
252
.github/workflows/pr-build.yml
vendored
Normal file
252
.github/workflows/pr-build.yml
vendored
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# PR 构建工作流
|
||||||
|
# =============================================================================
|
||||||
|
# 功能:
|
||||||
|
# 1. 在 PR 提交时自动构建 Framework 和 Shell 包
|
||||||
|
# 2. 支持通过 /build 命令手动触发构建(仅协作者/组织成员)
|
||||||
|
# 3. 在 PR 中发布构建状态评论,并持续更新(不会重复创建)
|
||||||
|
# 4. 支持 Fork PR 的构建(使用 pull_request_target 获取写权限)
|
||||||
|
#
|
||||||
|
# 安全说明:
|
||||||
|
# - 使用 pull_request_target 事件,在 base 分支上下文运行
|
||||||
|
# - 构建脚本始终从 base 分支 checkout,避免恶意 PR 篡改脚本
|
||||||
|
# - PR 代码单独 checkout 到 workspace 目录
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
name: PR Build
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 触发条件
|
||||||
|
# =============================================================================
|
||||||
|
on:
|
||||||
|
# PR 事件:打开、同步(新推送)、重新打开时触发
|
||||||
|
# 注意:使用 pull_request_target 而非 pull_request,以便对 Fork PR 有写权限
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
# Issue 评论事件:用于响应 /build 命令
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 权限配置
|
||||||
|
# =============================================================================
|
||||||
|
permissions:
|
||||||
|
contents: read # 读取仓库内容
|
||||||
|
pull-requests: write # 写入 PR 评论
|
||||||
|
issues: write # 写入 Issue 评论(/build 命令响应)
|
||||||
|
actions: read # 读取 Actions 信息(获取构建日志链接)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 并发控制
|
||||||
|
# =============================================================================
|
||||||
|
# 同一 PR 的多次构建会取消之前未完成的构建,避免资源浪费
|
||||||
|
concurrency:
|
||||||
|
group: pr-build-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 任务定义
|
||||||
|
# =============================================================================
|
||||||
|
jobs:
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 1: 检查构建条件
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 判断是否应该触发构建:
|
||||||
|
# - pull_request_target 事件:总是触发
|
||||||
|
# - issue_comment 事件:检查是否为 /build 命令,且用户有权限
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_build: ${{ steps.check.outputs.should_build }} # 是否应该构建
|
||||||
|
pr_number: ${{ steps.check.outputs.pr_number }} # PR 编号
|
||||||
|
pr_sha: ${{ steps.check.outputs.pr_sha }} # PR 最新提交 SHA
|
||||||
|
pr_head_repo: ${{ steps.check.outputs.pr_head_repo }} # PR 源仓库(用于 Fork)
|
||||||
|
pr_head_ref: ${{ steps.check.outputs.pr_head_ref }} # PR 源分支
|
||||||
|
steps:
|
||||||
|
# 仅 checkout 脚本目录,加快速度
|
||||||
|
- name: Checkout scripts
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
# 使用 Node.js 24 以支持原生 TypeScript 执行
|
||||||
|
- name: Setup Node.js 24
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
# 执行检查脚本,判断是否触发构建
|
||||||
|
- name: Check trigger condition
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: node --experimental-strip-types .github/scripts/pr-build-check.ts
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 2: 更新评论为"构建中"状态
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 在 PR 中创建或更新评论,显示构建正在进行中
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
update-comment-building:
|
||||||
|
needs: check-build
|
||||||
|
if: needs.check-build.outputs.should_build == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout scripts
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: Setup Node.js 24
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
# 更新 PR 评论,显示构建中状态
|
||||||
|
- name: Update building comment
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
|
||||||
|
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
|
||||||
|
run: node --experimental-strip-types .github/scripts/pr-build-building.ts
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 3: 构建 Framework 包
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 执行 napcat-framework 的构建流程
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
build-framework:
|
||||||
|
needs: [check-build, update-comment-building]
|
||||||
|
if: needs.check-build.outputs.should_build == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
status: ${{ steps.build.outcome }} # 构建结果:success/failure
|
||||||
|
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
|
||||||
|
steps:
|
||||||
|
# 【安全】先从 base 分支 checkout 构建脚本
|
||||||
|
# 这样即使 PR 中修改了脚本,也不会被执行
|
||||||
|
- name: Checkout scripts from base
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
path: _scripts
|
||||||
|
|
||||||
|
# 将 PR 代码 checkout 到单独的 workspace 目录
|
||||||
|
- name: Checkout PR code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ needs.check-build.outputs.pr_head_repo }}
|
||||||
|
ref: ${{ needs.check-build.outputs.pr_sha }}
|
||||||
|
path: workspace
|
||||||
|
|
||||||
|
- name: Setup Node.js 24
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
# 执行构建,使用 base 分支的脚本处理 workspace 中的代码
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
working-directory: workspace
|
||||||
|
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts framework
|
||||||
|
continue-on-error: true # 允许失败,后续更新评论时处理
|
||||||
|
|
||||||
|
# 构建成功时上传产物
|
||||||
|
- name: Upload Artifact
|
||||||
|
if: steps.build.outcome == 'success'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: NapCat.Framework
|
||||||
|
path: workspace/framework-dist
|
||||||
|
retention-days: 7 # 保留 7 天
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 4: 构建 Shell 包
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 执行 napcat-shell 的构建流程(与 Framework 并行执行)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
build-shell:
|
||||||
|
needs: [check-build, update-comment-building]
|
||||||
|
if: needs.check-build.outputs.should_build == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
status: ${{ steps.build.outcome }} # 构建结果:success/failure
|
||||||
|
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
|
||||||
|
steps:
|
||||||
|
# 【安全】先从 base 分支 checkout 构建脚本
|
||||||
|
- name: Checkout scripts from base
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
path: _scripts
|
||||||
|
|
||||||
|
# 将 PR 代码 checkout 到单独的 workspace 目录
|
||||||
|
- name: Checkout PR code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ needs.check-build.outputs.pr_head_repo }}
|
||||||
|
ref: ${{ needs.check-build.outputs.pr_sha }}
|
||||||
|
path: workspace
|
||||||
|
|
||||||
|
- name: Setup Node.js 24
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
# 执行构建
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
working-directory: workspace
|
||||||
|
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts shell
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
# 构建成功时上传产物
|
||||||
|
- name: Upload Artifact
|
||||||
|
if: steps.build.outcome == 'success'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: NapCat.Shell
|
||||||
|
path: workspace/shell-dist
|
||||||
|
retention-days: 7 # 保留 7 天
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 5: 更新评论为构建结果
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 汇总所有构建结果,更新 PR 评论显示最终状态
|
||||||
|
# 使用 always() 确保即使构建失败/取消也会执行
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
update-comment-result:
|
||||||
|
needs: [check-build, update-comment-building, build-framework, build-shell]
|
||||||
|
if: always() && needs.check-build.outputs.should_build == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout scripts
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: Setup Node.js 24
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
# 更新评论,显示构建结果和下载链接
|
||||||
|
- name: Update result comment
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
|
||||||
|
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
# 获取构建状态,如果 job 被跳过则标记为 cancelled
|
||||||
|
FRAMEWORK_STATUS: ${{ needs.build-framework.outputs.status || 'cancelled' }}
|
||||||
|
FRAMEWORK_ERROR: ${{ needs.build-framework.outputs.error }}
|
||||||
|
SHELL_STATUS: ${{ needs.build-shell.outputs.status || 'cancelled' }}
|
||||||
|
SHELL_ERROR: ${{ needs.build-shell.outputs.error }}
|
||||||
|
run: node --experimental-strip-types .github/scripts/pr-build-result.ts
|
||||||
Loading…
Reference in New Issue
Block a user