diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9242f3947b..1dbf472ca4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -287,6 +287,7 @@ jobs: NODE_OPTIONS: "--max_old_space_size=4096" CI: 1 + # Artifact build files - uses: actions/upload-artifact@v4 if: github.event_name == 'push' && github.ref == 'refs/heads/master' with: @@ -297,6 +298,17 @@ jobs: es lib + - name: zip builds + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + env: + ALI_OSS_AK_ID: ${{ secrets.ALI_OSS_AK_ID }} + ALI_OSS_AK_SECRET: ${{ secrets.ALI_OSS_AK_SECRET }} + HEAD_SHA: ${{ github.sha }} + run: | + zip -r oss-artifacts.zip dist locale es lib + echo "🤖 Uploading" + node scripts/visual-regression/upload.js ./oss-artifacts.zip --ref=$HEAD_SHA + compiled-module-test: name: module test runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d3f4bd0b50..5800902104 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ components/**/*.jsx /.history *.tmp artifacts.zip +oss-artifacts.zip server/ # Docs templates diff --git a/package.json b/package.json index 559b4e6e03..a2094bc070 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "build": "npm run compile && NODE_OPTIONS='--max-old-space-size=4096' npm run dist", "changelog": "npm run lint:changelog && tsx scripts/print-changelog.ts", "check-commit": "tsx scripts/check-commit.ts", - "clean": "antd-tools run clean && rm -rf es lib coverage locale dist report.html artifacts.zip", + "clean": "antd-tools run clean && rm -rf es lib coverage locale dist report.html artifacts.zip oss-artifacts.zip", "clean:lockfiles": "rm -rf package-lock.json yarn.lock", "precompile": "npm run prestart", "compile": "npm run clean && antd-tools run compile", @@ -221,6 +221,7 @@ "@types/react-highlight-words": "^0.16.7", "@types/react-resizable": "^3.0.7", "@types/semver": "^7.5.8", + "@types/spinnies": "^0.5.3", "@types/tar": "^6.1.12", "@types/throttle-debounce": "^5.0.2", "@types/warning": "^3.0.3", @@ -328,6 +329,7 @@ "sharp": "^0.33.3", "simple-git": "^3.24.0", "size-limit": "^11.1.2", + "spinnies": "^0.5.1", "stylelint": "^16.3.1", "stylelint-config-rational-order": "^0.1.2", "stylelint-config-standard": "^36.0.0", diff --git a/scripts/pre-publish.ts b/scripts/pre-publish.ts index 493c572b4d..503621680b 100644 --- a/scripts/pre-publish.ts +++ b/scripts/pre-publish.ts @@ -1,18 +1,65 @@ -/* eslint-disable camelcase */ +/* eslint-disable camelcase, no-async-promise-executor */ 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'; import chalk from 'chalk'; -import cliProgress from 'cli-progress'; -import ora from 'ora'; +import Spinnies from 'spinnies'; import checkRepo from './check-repo'; const { Notification: Notifier } = require('node-notifier'); const simpleGit = require('simple-git'); +const blockStatus = ['failure', 'cancelled', 'timed_out'] as const; + +const spinner = { interval: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] }; +const spinnies = new Spinnies({ spinner }); + +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, + }); + } + } +}; + process.on('SIGINT', () => { process.exit(1); }); @@ -38,136 +85,193 @@ const emojify = (status: string = '') => { return `${emoji || ''} ${(status || '').padEnd(15)}`; }; -async function downloadArtifact(url: string, filepath: string) { - const bar = new cliProgress.SingleBar( - { - format: ` 下载中 [${chalk.cyan( - '{bar}', - )}] {percentage}% | 预计还剩: {eta}s | {value}/{total}`, - }, - cliProgress.Presets.rect, - ); - bar.start(1, 0); +const toMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2); + +async function downloadArtifact(msgKey: string, url: string, filepath: string, token?: string) { + const headers: Record = {}; + if (token) { + headers.Authorization = `token ${token}`; + } + const response = await axios.get(url, { - headers: { - Authorization: `token ${process.env.GITHUB_ACCESS_TOKEN}`, - }, + headers, responseType: 'arraybuffer', onDownloadProgress: (progressEvent) => { - bar.setTotal(progressEvent.total || 0); - bar.update(progressEvent.loaded); + const { loaded, total = 0 } = progressEvent; + + showMessage( + `下载进度 ${toMB(loaded)}MB/${toMB(total)}MB (${((loaded / total) * 100).toFixed(2)}%)`, + true, + msgKey, + ); }, }); + fs.writeFileSync(filepath, Buffer.from(response.data)); + + return filepath; } const runPrePublish = async () => { await checkRepo(); - const spinner = ora(); - spinner.info(chalk.black.bgGreenBright('本次发布将跳过本地 CI 检查,远程 CI 通过后方可发布')); + showMessage(chalk.black.bgGreenBright('本次发布将跳过本地 CI 检查,远程 CI 通过后方可发布')); const git = simpleGit(); const octokit = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); const { current: currentBranch } = await git.branch(); - spinner.start(`正在拉取远程分支 ${currentBranch}`); + showMessage(`正在拉取远程分支 ${currentBranch}`, true); await git.pull('origin', currentBranch); - spinner.succeed(`成功拉取远程分支 ${currentBranch}`); - spinner.start(`正在推送本地分支 ${currentBranch}`); + showMessage(`成功拉取远程分支 ${currentBranch}`, 'succeed'); + showMessage(`正在推送本地分支 ${currentBranch}`, true); await git.push('origin', currentBranch); - spinner.succeed(`成功推送远程分支 ${currentBranch}`); - spinner.succeed(`已经和远程分支保持同步 ${currentBranch}`); + showMessage(`成功推送远程分支 ${currentBranch}`, 'succeed'); + showMessage(`已经和远程分支保持同步 ${currentBranch}`, 'succeed'); const { latest } = await git.log(); - spinner.succeed(`找到本地最新 commit:`); - spinner.info(chalk.cyan(` hash: ${latest.hash}`)); - spinner.info(chalk.cyan(` date: ${latest.date}`)); - spinner.info(chalk.cyan(` message: ${latest.message}`)); - spinner.info(chalk.cyan(` author_name: ${latest.author_name}`)); + 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}`)); + const owner = 'ant-design'; const repo = 'ant-design'; - spinner.start(`开始检查远程分支 ${currentBranch} 的 CI 状态`); + showMessage(`开始检查远程分支 ${currentBranch} 的 CI 状态`, true); + + const failureUrlList: string[] = []; const { data: { check_runs }, } = await octokit.checks.listForRef({ owner, repo, - ref: latest.hash, + ref: sha, }); - spinner.succeed(`远程分支 CI 状态:`); + showMessage(`远程分支 CI 状态(${check_runs.length}):`, 'succeed'); check_runs.forEach((run) => { - spinner.info( - ` ${run.name.padEnd(36)} ${emojify(run.status)} ${emojify(run.conclusion || '')}`, - ); + showMessage(` ${run.name.padEnd(36)} ${emojify(run.status)} ${emojify(run.conclusion || '')}`); + if (blockStatus.some((status) => run.conclusion === status)) { + failureUrlList.push(run.html_url!); + } }); const conclusions = check_runs.map((run) => run.conclusion); - if ( - conclusions.includes('failure') || - conclusions.includes('cancelled') || - conclusions.includes('timed_out') - ) { - spinner.fail(chalk.bgRedBright('远程分支 CI 执行异常,无法继续发布,请尝试修复或重试')); - spinner.info(` 点此查看状态:https://github.com/${owner}/${repo}/commit/${latest.hash}`); + 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}`); + }); + process.exit(1); } + const statuses = check_runs.map((run) => run.status); if (check_runs.length < 1 || statuses.includes('queued') || statuses.includes('in_progress')) { - spinner.fail(chalk.bgRedBright('远程分支 CI 还在执行中,请稍候再试')); - spinner.info(` 点此查看状态:https://github.com/${owner}/${repo}/commit/${latest.hash}`); + showMessage(chalk.bgRedBright('远程分支 CI 还在执行中,请稍候再试'), 'fail'); + showMessage(` 点此查看状态:https://github.com/${owner}/${repo}/commit/${sha}`); process.exit(1); } - spinner.succeed(`远程分支 CI 已通过`); + showMessage(`远程分支 CI 已通过`, 'succeed'); // clean up await runScript({ event: 'clean', path: '.', stdio: 'inherit' }); - spinner.succeed(`成功清理构建产物目录`); - spinner.start(`开始查找远程分支构建产物`); - const { - data: { workflow_runs }, - } = await octokit.rest.actions.listWorkflowRunsForRepo({ - owner, - repo, - head_sha: latest.hash, - per_page: 100, - exclude_pull_requests: true, - event: 'push', - status: 'completed', - conclusion: 'success', - head_branch: currentBranch, + 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); }); - const testWorkflowRun = workflow_runs.find((run) => run.name === '✅ test'); - if (!testWorkflowRun) { - spinner.fail(chalk.bgRedBright('找不到远程构建工作流')); + 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'); + }); + 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( + chalk.bgRedBright(`下载失败,请确认你当前 ${sha.slice(0, 6)} 位于 master 分支中`), + 'fail', + ); process.exit(1); } - 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) { - spinner.fail(chalk.bgRedBright('找不到远程构建产物')); - process.exit(1); - } - spinner.info(`准备从远程分支下载构建产物`); - const { url } = await octokit.rest.actions.downloadArtifact.endpoint({ - owner, - repo, - artifact_id: artifact.id, - archive_format: 'zip', - }); - await downloadArtifact(url, 'artifacts.zip'); - spinner.info(); - spinner.succeed(`成功从远程分支下载构建产物`); + + showMessage(`成功从远程分支下载构建产物`, 'succeed'); + // unzip - spinner.start(`正在解压构建产物`); - const zip = new AdmZip('artifacts.zip'); + showMessage(`正在解压构建产物`, true); + const zip = new AdmZip(firstArtifactFile); zip.extractAllTo('./', true); - spinner.succeed(`成功解压构建产物`); + showMessage(`成功解压构建产物`, 'succeed'); await runScript({ event: 'test:dekko', path: '.', stdio: 'inherit' }); await runScript({ event: 'test:package-diff', path: '.', stdio: 'inherit' }); - spinner.succeed(`文件检查通过,准备发布!`); + showMessage(`文件检查通过,准备发布!`, 'succeed'); new Notifier().notify({ title: '✅ 准备发布到 npm',