2024-04-07 19:14:44 +08:00
|
|
|
|
import fs from 'node:fs';
|
|
|
|
|
import runScript from '@npmcli/run-script';
|
|
|
|
|
import { Octokit } from '@octokit/rest';
|
|
|
|
|
import AdmZip from 'adm-zip';
|
|
|
|
|
import axios from 'axios';
|
2024-04-08 14:04:08 +08:00
|
|
|
|
import chalk from 'chalk';
|
2024-04-16 16:41:02 +08:00
|
|
|
|
import Spinnies from 'spinnies';
|
2024-07-05 17:35:31 +08:00
|
|
|
|
import dotnev from 'dotenv';
|
2024-04-07 19:14:44 +08:00
|
|
|
|
import checkRepo from './check-repo';
|
|
|
|
|
|
2024-07-05 17:35:31 +08:00
|
|
|
|
dotnev.config({ override: true });
|
|
|
|
|
|
2024-04-07 22:45:53 +08:00
|
|
|
|
const { Notification: Notifier } = require('node-notifier');
|
2024-04-07 19:14:44 +08:00
|
|
|
|
const simpleGit = require('simple-git');
|
|
|
|
|
|
2024-04-16 16:41:02 +08:00
|
|
|
|
const blockStatus = ['failure', 'cancelled', 'timed_out'] as const;
|
|
|
|
|
|
|
|
|
|
const spinner = { interval: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] };
|
|
|
|
|
const spinnies = new Spinnies({ spinner });
|
|
|
|
|
|
2024-11-20 22:26:02 +08:00
|
|
|
|
const IGNORE_ACTIONS = ['Check Virtual Regression Approval', 'issue-remove-inactive'];
|
|
|
|
|
|
2024-04-16 16:41:02 +08:00
|
|
|
|
let spinniesId = 0;
|
|
|
|
|
|
|
|
|
|
// `spinnies` 为按条目进度,需要做简单的封装变成接近 `ora` 的形态
|
|
|
|
|
const showMessage = (
|
|
|
|
|
message: string,
|
|
|
|
|
status?: 'succeed' | 'fail' | 'spinning' | 'non-spinnable' | 'stopped' | true,
|
|
|
|
|
uniqueTitle?: string,
|
|
|
|
|
) => {
|
|
|
|
|
if (!status) {
|
|
|
|
|
spinnies.add(`info-${spinniesId}`, {
|
|
|
|
|
text: message,
|
|
|
|
|
status: 'non-spinnable',
|
|
|
|
|
});
|
|
|
|
|
spinniesId += 1;
|
|
|
|
|
} else {
|
|
|
|
|
const mergedId = uniqueTitle || `msg-${spinniesId}`;
|
|
|
|
|
let mergedMessage = uniqueTitle ? `${uniqueTitle} ${message}` : message;
|
|
|
|
|
|
|
|
|
|
// `spinnies` 对中文支持有 bug,长度会按中文一半计算。我们翻个倍修复一下。
|
|
|
|
|
mergedMessage = `${mergedMessage}${' '.repeat(mergedMessage.length)}`;
|
|
|
|
|
|
|
|
|
|
const existSpinner = spinnies.pick(mergedId);
|
|
|
|
|
if (!existSpinner) {
|
|
|
|
|
spinnies.add(mergedId, {
|
|
|
|
|
text: '',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 'succeed' || status === 'fail' || status === 'stopped') {
|
|
|
|
|
spinnies.update(mergedId, {
|
|
|
|
|
text: mergedMessage,
|
|
|
|
|
status,
|
|
|
|
|
});
|
|
|
|
|
spinniesId += 1;
|
|
|
|
|
} else {
|
|
|
|
|
spinnies.update(mergedId, {
|
|
|
|
|
text: mergedMessage,
|
|
|
|
|
status: status === true ? 'spinning' : status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-07 19:14:44 +08:00
|
|
|
|
process.on('SIGINT', () => {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-22 21:59:12 +08:00
|
|
|
|
const emojify = (status = '') => {
|
2024-04-07 19:14:44 +08:00
|
|
|
|
if (!status) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
const emoji = {
|
|
|
|
|
/* status */
|
|
|
|
|
completed: '✅',
|
|
|
|
|
queued: '🕒',
|
|
|
|
|
in_progress: '⌛',
|
|
|
|
|
/* conclusion */
|
|
|
|
|
success: '✅',
|
|
|
|
|
failure: '❌',
|
|
|
|
|
neutral: '⚪',
|
|
|
|
|
cancelled: '❌',
|
|
|
|
|
skipped: '⏭️',
|
|
|
|
|
timed_out: '⌛',
|
|
|
|
|
action_required: '🔴',
|
|
|
|
|
}[status];
|
|
|
|
|
return `${emoji || ''} ${(status || '').padEnd(15)}`;
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-16 16:41:02 +08:00
|
|
|
|
const toMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2);
|
|
|
|
|
|
|
|
|
|
async function downloadArtifact(msgKey: string, url: string, filepath: string, token?: string) {
|
|
|
|
|
const headers: Record<string, string> = {};
|
|
|
|
|
if (token) {
|
|
|
|
|
headers.Authorization = `token ${token}`;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-07 19:14:44 +08:00
|
|
|
|
const response = await axios.get(url, {
|
2024-04-16 16:41:02 +08:00
|
|
|
|
headers,
|
2024-04-07 19:14:44 +08:00
|
|
|
|
responseType: 'arraybuffer',
|
|
|
|
|
onDownloadProgress: (progressEvent) => {
|
2024-04-16 16:41:02 +08:00
|
|
|
|
const { loaded, total = 0 } = progressEvent;
|
|
|
|
|
|
|
|
|
|
showMessage(
|
|
|
|
|
`下载进度 ${toMB(loaded)}MB/${toMB(total)}MB (${((loaded / total) * 100).toFixed(2)}%)`,
|
|
|
|
|
true,
|
|
|
|
|
msgKey,
|
|
|
|
|
);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
},
|
|
|
|
|
});
|
2024-04-16 16:41:02 +08:00
|
|
|
|
|
2024-04-07 19:14:44 +08:00
|
|
|
|
fs.writeFileSync(filepath, Buffer.from(response.data));
|
2024-04-16 16:41:02 +08:00
|
|
|
|
|
|
|
|
|
return filepath;
|
2024-04-07 19:14:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const runPrePublish = async () => {
|
|
|
|
|
await checkRepo();
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(chalk.black.bgGreenBright('本次发布将跳过本地 CI 检查,远程 CI 通过后方可发布'));
|
2024-04-07 19:14:44 +08:00
|
|
|
|
const git = simpleGit();
|
|
|
|
|
const octokit = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
|
|
|
|
|
const { current: currentBranch } = await git.branch();
|
|
|
|
|
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`正在拉取远程分支 ${currentBranch}`, true);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
await git.pull('origin', currentBranch);
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`成功拉取远程分支 ${currentBranch}`, 'succeed');
|
|
|
|
|
showMessage(`正在推送本地分支 ${currentBranch}`, true);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
await git.push('origin', currentBranch);
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`成功推送远程分支 ${currentBranch}`, 'succeed');
|
|
|
|
|
showMessage(`已经和远程分支保持同步 ${currentBranch}`, 'succeed');
|
2024-04-07 19:14:44 +08:00
|
|
|
|
|
|
|
|
|
const { latest } = await git.log();
|
2024-04-16 16:41:02 +08:00
|
|
|
|
const sha = process.env.TARGET_SHA || latest.hash;
|
|
|
|
|
|
|
|
|
|
showMessage(`找到本地最新 commit:`, 'succeed');
|
|
|
|
|
showMessage(chalk.cyan(` hash: ${sha}`));
|
|
|
|
|
showMessage(chalk.cyan(` date: ${latest.date}`));
|
|
|
|
|
showMessage(chalk.cyan(` message: ${latest.message}`));
|
|
|
|
|
showMessage(chalk.cyan(` author_name: ${latest.author_name}`));
|
|
|
|
|
|
2024-04-07 19:14:44 +08:00
|
|
|
|
const owner = 'ant-design';
|
|
|
|
|
const repo = 'ant-design';
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`开始检查远程分支 ${currentBranch} 的 CI 状态`, true);
|
|
|
|
|
|
|
|
|
|
const failureUrlList: string[] = [];
|
2024-11-20 17:09:25 +08:00
|
|
|
|
let {
|
2024-04-07 19:14:44 +08:00
|
|
|
|
data: { check_runs },
|
|
|
|
|
} = await octokit.checks.listForRef({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
2024-04-16 16:41:02 +08:00
|
|
|
|
ref: sha,
|
2024-04-07 19:14:44 +08:00
|
|
|
|
});
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`远程分支 CI 状态(${check_runs.length}):`, 'succeed');
|
2024-11-20 22:26:02 +08:00
|
|
|
|
check_runs = check_runs.filter((run) =>
|
|
|
|
|
IGNORE_ACTIONS.every((action) => !run.name.includes(action)),
|
2024-11-20 17:09:25 +08:00
|
|
|
|
);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
check_runs.forEach((run) => {
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(` ${run.name.padEnd(36)} ${emojify(run.status)} ${emojify(run.conclusion || '')}`);
|
|
|
|
|
if (blockStatus.some((status) => run.conclusion === status)) {
|
|
|
|
|
failureUrlList.push(run.html_url!);
|
|
|
|
|
}
|
2024-04-07 19:14:44 +08:00
|
|
|
|
});
|
|
|
|
|
const conclusions = check_runs.map((run) => run.conclusion);
|
2024-04-16 16:41:02 +08:00
|
|
|
|
if (blockStatus.some((status) => conclusions.includes(status))) {
|
|
|
|
|
showMessage(chalk.bgRedBright('远程分支 CI 执行异常,无法继续发布,请尝试修复或重试'), 'fail');
|
|
|
|
|
showMessage(` 点此查看状态:https://github.com/${owner}/${repo}/commit/${sha}`);
|
|
|
|
|
|
|
|
|
|
failureUrlList.forEach((url) => {
|
|
|
|
|
showMessage(` - ${url}`);
|
|
|
|
|
});
|
|
|
|
|
|
2024-04-07 19:14:44 +08:00
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2024-04-16 16:41:02 +08:00
|
|
|
|
|
2024-04-07 19:14:44 +08:00
|
|
|
|
const statuses = check_runs.map((run) => run.status);
|
|
|
|
|
if (check_runs.length < 1 || statuses.includes('queued') || statuses.includes('in_progress')) {
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(chalk.bgRedBright('远程分支 CI 还在执行中,请稍候再试'), 'fail');
|
|
|
|
|
showMessage(` 点此查看状态:https://github.com/${owner}/${repo}/commit/${sha}`);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`远程分支 CI 已通过`, 'succeed');
|
2024-04-07 19:14:44 +08:00
|
|
|
|
// clean up
|
|
|
|
|
await runScript({ event: 'clean', path: '.', stdio: 'inherit' });
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`成功清理构建产物目录`, 'succeed');
|
|
|
|
|
|
|
|
|
|
// 从 github artifact 中下载产物
|
|
|
|
|
const downloadArtifactPromise = Promise.resolve().then(async () => {
|
|
|
|
|
showMessage('开始查找远程分支构建产物', true, '[Github]');
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
data: { workflow_runs },
|
|
|
|
|
} = await octokit.rest.actions.listWorkflowRunsForRepo({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
head_sha: sha,
|
|
|
|
|
per_page: 100,
|
|
|
|
|
exclude_pull_requests: true,
|
|
|
|
|
event: 'push',
|
|
|
|
|
status: 'completed',
|
|
|
|
|
conclusion: 'success',
|
|
|
|
|
head_branch: currentBranch,
|
|
|
|
|
});
|
|
|
|
|
const testWorkflowRun = workflow_runs.find((run) => run.name === '✅ test');
|
|
|
|
|
if (!testWorkflowRun) {
|
|
|
|
|
throw new Error('找不到远程构建工作流');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
data: { artifacts },
|
|
|
|
|
} = await octokit.actions.listWorkflowRunArtifacts({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
run_id: testWorkflowRun?.id || 0,
|
|
|
|
|
});
|
|
|
|
|
const artifact = artifacts.find((item) => item.name === 'build artifacts');
|
|
|
|
|
if (!artifact) {
|
|
|
|
|
throw new Error('找不到远程构建产物');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showMessage(`准备从远程分支下载构建产物`, true, '[Github]');
|
|
|
|
|
const { url } = await octokit.rest.actions.downloadArtifact.endpoint({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
artifact_id: artifact.id,
|
|
|
|
|
archive_format: 'zip',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 返回下载后的文件路径
|
|
|
|
|
return downloadArtifact('[Github]', url, 'artifacts.zip', process.env.GITHUB_ACCESS_TOKEN);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
});
|
2024-04-16 16:41:02 +08:00
|
|
|
|
downloadArtifactPromise
|
|
|
|
|
.then(() => {
|
|
|
|
|
showMessage(`成功下载构建产物`, 'succeed', '[Github]');
|
|
|
|
|
})
|
|
|
|
|
.catch((e: Error) => {
|
|
|
|
|
showMessage(chalk.bgRedBright(e.message), 'fail', '[Github]');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 从 OSS 下载产物
|
|
|
|
|
const downloadOSSPromise = Promise.resolve().then(async () => {
|
|
|
|
|
const url = `https://antd-visual-diff.oss-cn-shanghai.aliyuncs.com/${sha}/oss-artifacts.zip`;
|
|
|
|
|
|
|
|
|
|
showMessage(`准备从远程 OSS 下载构建产物`, true, '[OSS]');
|
|
|
|
|
|
|
|
|
|
// 返回下载后的文件路径
|
|
|
|
|
return downloadArtifact('[OSS]', url, 'oss-artifacts.zip');
|
2024-04-07 19:14:44 +08:00
|
|
|
|
});
|
2024-04-16 16:41:02 +08:00
|
|
|
|
downloadOSSPromise
|
|
|
|
|
.then(() => {
|
|
|
|
|
showMessage(`成功下载构建产物`, 'succeed', '[OSS]');
|
|
|
|
|
})
|
|
|
|
|
.catch((e: Error) => {
|
|
|
|
|
showMessage(chalk.bgRedBright(e.message), 'fail', '[OSS]');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 任意一个完成,则完成
|
|
|
|
|
let firstArtifactFile: string;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
firstArtifactFile = await Promise.any([downloadArtifactPromise, downloadOSSPromise]);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showMessage(
|
2024-08-04 12:24:35 +08:00
|
|
|
|
chalk.bgRedBright(`下载失败 ${error},请确认你当前 ${sha.slice(0, 6)} 位于 master 分支中`),
|
2024-04-16 16:41:02 +08:00
|
|
|
|
'fail',
|
|
|
|
|
);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2024-04-16 16:41:02 +08:00
|
|
|
|
|
|
|
|
|
showMessage(`成功从远程分支下载构建产物`, 'succeed');
|
|
|
|
|
|
2024-04-07 19:14:44 +08:00
|
|
|
|
// unzip
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`正在解压构建产物`, true);
|
|
|
|
|
const zip = new AdmZip(firstArtifactFile);
|
2024-04-07 19:14:44 +08:00
|
|
|
|
zip.extractAllTo('./', true);
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`成功解压构建产物`, 'succeed');
|
2024-04-07 19:14:44 +08:00
|
|
|
|
await runScript({ event: 'test:dekko', path: '.', stdio: 'inherit' });
|
|
|
|
|
await runScript({ event: 'test:package-diff', path: '.', stdio: 'inherit' });
|
2024-04-16 16:41:02 +08:00
|
|
|
|
showMessage(`文件检查通过,准备发布!`, 'succeed');
|
2024-04-07 22:45:53 +08:00
|
|
|
|
|
|
|
|
|
new Notifier().notify({
|
|
|
|
|
title: '✅ 准备发布到 npm',
|
|
|
|
|
message: '产物已经准备好了,快回来输入 npm 校验码了!',
|
|
|
|
|
sound: 'Crystal',
|
|
|
|
|
});
|
2024-04-07 19:14:44 +08:00
|
|
|
|
process.exit(0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
runPrePublish();
|