diff --git a/packages/upgrade-deps/dist/index.mjs b/packages/upgrade-deps/dist/index.mjs index 71bf29c..58fc5fc 100644 --- a/packages/upgrade-deps/dist/index.mjs +++ b/packages/upgrade-deps/dist/index.mjs @@ -1,5 +1,4 @@ import { createRequire } from "node:module"; -import * as path from "node:path"; import * as os$1 from "os"; import os, { EOL } from "os"; import * as fs from "fs"; @@ -9,6 +8,7 @@ import * as events from "events"; import { StringDecoder } from "string_decoder"; import * as child from "child_process"; import { setTimeout as setTimeout$1 } from "timers"; +import * as path from "node:path"; //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; @@ -16641,6 +16641,15 @@ function getBooleanInput(name, options) { throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\nSupport boolean input list: \`true | True | TRUE | false | False | FALSE\``); } /** +* Sets the action status to failed. +* When the action exits it will be with an exit code of 1 +* @param message add error issue message +*/ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +/** * Adds an error issue * @param message error issue message. Errors will be converted to string via toString() * @param properties optional properties to add to the annotation. @@ -19856,9 +19865,10 @@ var GitHelper = class { ], { cwd: this.repoPath }); } async commit(message) { + await exec("git", ["add", "-A"], { cwd: this.repoPath }); await exec("git", [ "commit", - "-am", + "-m", message, "--no-verify" ], { cwd: this.repoPath }); @@ -19934,9 +19944,9 @@ var GithubHelper = class { pull_number: prNumber }); return data; - } catch (error$4) { - error(`获取PR数据失败: ${error$4}`); - throw error$4; + } catch (error$3) { + error(`获取PR数据失败: ${error$3}`); + throw error$3; } } async getIssueData(issueNumber) { @@ -19946,9 +19956,9 @@ var GithubHelper = class { issue_number: issueNumber }); return data; - } catch (error$6) { - error(`获取Issue数据失败: ${error$6}`); - throw error$6; + } catch (error$5) { + error(`获取Issue数据失败: ${error$5}`); + throw error$5; } } async getIssueList(params) { @@ -19958,9 +19968,9 @@ var GithubHelper = class { ...this.defaultRepoParams }); return data.filter((item) => !item?.pull_request); - } catch (error$5) { - error(`获取Issue列表失败: ${error$5}`); - throw error$5; + } catch (error$4) { + error(`获取Issue列表失败: ${error$4}`); + throw error$4; } } async closeIssue(issueNumber) { @@ -19971,9 +19981,9 @@ var GithubHelper = class { issue_number: issueNumber, state: "closed" }); - } catch (error$7) { - error(`关闭Issue失败: ${error$7}`); - throw error$7; + } catch (error$6) { + error(`关闭Issue失败: ${error$6}`); + throw error$6; } } async createPR(title, head, body, base = "develop") { @@ -19992,9 +20002,9 @@ var GithubHelper = class { body }); return data; - } catch (error$2) { - error(`创建PR失败: ${error$2}`); - throw error$2; + } catch (error$1) { + error(`创建PR失败: ${error$1}`); + throw error$1; } } async addComment(issueNumber, body) { @@ -20009,9 +20019,9 @@ var GithubHelper = class { body }); return data; - } catch (error$3) { - error(`添加评论失败: ${error$3}`); - throw error$3; + } catch (error$2) { + error(`添加评论失败: ${error$2}`); + throw error$2; } } async addLabels(issueNumber, labels) { @@ -20026,9 +20036,9 @@ var GithubHelper = class { labels }); return data; - } catch (error$8) { - error(`添加标签失败: ${error$8}`); - throw error$8; + } catch (error$7) { + error(`添加标签失败: ${error$7}`); + throw error$7; } } }; @@ -20045,11 +20055,14 @@ const PACKAGE_MANAGER_COMMANDS = { }, npm: { cmd: "npm", - args: ["update"] + args: ["install"] } }; +function slugify(value) { + return value.replace(/@/g, "").replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, ""); +} function getBranchName(deps) { - return `chore/deps/upgrade-${deps.map((d) => `${d.name.replace(/@/g, "").replace(/\//g, "-")}-${d.version}`).join("-")}`; + return `chore/deps/upgrade-${deps.map((d) => `${slugify(d.name)}-${slugify(d.version)}`).join("-")}`; } function getPrTitle(deps) { return `chore: upgrade ${deps.map((d) => `${d.name} to ${d.version}`).join(", ")}`; @@ -20058,34 +20071,42 @@ function getRepoPath(repo, targetDir) { const base = `./${repo}`; return targetDir ? path.join(base, targetDir) : base; } +function parseDependencyName(spec) { + const value = spec.trim(); + if (!value) throw new Error("Empty dependency name"); + if ((value.startsWith("@") ? value.indexOf("@", value.indexOf("/") + 1) : value.lastIndexOf("@")) > 0) throw new Error(`Dependency versions are not supported: ${spec}. Please pass package names only.`); + return value; +} +function parseDependencyInputs(inputs) { + const deps = inputs.flatMap((input) => input.split(/\s+/)).map((item) => item.trim()).filter(Boolean).map(parseDependencyName); + if (!deps.length) throw new Error("Missing deps input"); + return deps; +} +function validatePackageManager(packageManager) { + if (packageManager in PACKAGE_MANAGER_COMMANDS) return packageManager; + throw new Error(`Unsupported package-manager "${packageManager}". Supported values: npm, yarn, pnpm.`); +} async function fetchPackageVersion(pkg) { try { const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`); - if (!response.ok) { - error(`Failed to get ${pkg} info from npm registry, status code: ${response.status}`); - return null; - } + if (!response.ok) throw new Error(`status code: ${response.status}`); const { version } = await response.json(); - if (!version) { - error(`No version found for ${pkg}`); - return null; - } + if (!version) throw new Error("no version found"); info(`Latest version of ${pkg} is ${version}`); return { name: pkg, version }; - } catch (error$1) { - error(`Error fetching ${pkg}: ${error$1}`); - return null; + } catch (error) { + throw new Error(`Failed to get ${pkg} info from npm registry: ${error instanceof Error ? error.message : String(error)}`); } } -async function getPkgLatestVersions(pkgNames) { - return (await Promise.all(pkgNames.map(fetchPackageVersion))).filter((r) => r !== null); +async function resolveDependencyInfos(deps) { + return Promise.all(deps.map(fetchPackageVersion)); } async function updatePackageDependencies(packageManager, deps, repo, targetDir) { const repoPath = getRepoPath(repo, targetDir); - const { cmd, args } = PACKAGE_MANAGER_COMMANDS[packageManager] ?? PACKAGE_MANAGER_COMMANDS.npm; + const { cmd, args } = PACKAGE_MANAGER_COMMANDS[packageManager]; await exec(cmd, [...args, ...deps], { cwd: repoPath }); } async function createDepsPr(title, branchName, baseBranch, context) { @@ -20097,18 +20118,17 @@ async function createDepsPr(title, branchName, baseBranch, context) { }).createPR(title, branchName, title, baseBranch); } async function updateDependencies(context) { - const packageManager = getInput("package-manager") || "npm"; + const packageManager = validatePackageManager(getInput("package-manager") || "npm"); const targetDir = getInput("target-dir") || ""; const customTitle = getInput("title") || ""; - const deps = getMultilineInput("deps", { + const deps = parseDependencyInputs(getMultilineInput("deps", { required: true, trimWhitespace: true - }); + })); info(`deps: ${JSON.stringify(deps)}`); info(`target-dir: ${targetDir || "default (repo root)"}`); if (customTitle) info(`custom-title: ${customTitle}`); - if (!deps.length) throw new Error("Missing deps input"); - const depInfos = await getPkgLatestVersions(deps); + const depInfos = await resolveDependencyInfos(deps); info(`depInfos: ${JSON.stringify(depInfos)}`); if (packageManager !== "npm") await exec("corepack", ["enable"]); const gitHelper = new GitHelper({ @@ -20153,6 +20173,8 @@ async function main() { } //#endregion //#region index.ts -main(); +main().catch((error) => { + setFailed(`upgrade-deps failed: ${error instanceof Error ? error.message : String(error)}`); +}); //#endregion export {}; diff --git a/packages/upgrade-deps/index.ts b/packages/upgrade-deps/index.ts index ad78d52..b691a8c 100644 --- a/packages/upgrade-deps/index.ts +++ b/packages/upgrade-deps/index.ts @@ -1,3 +1,6 @@ +import * as core from '@actions/core' import { main } from './main' -main() +main().catch((error) => { + core.setFailed(`upgrade-deps failed: ${error instanceof Error ? error.message : String(error)}`) +}) diff --git a/packages/upgrade-deps/main.test.ts b/packages/upgrade-deps/main.test.ts new file mode 100644 index 0000000..d9b44d8 --- /dev/null +++ b/packages/upgrade-deps/main.test.ts @@ -0,0 +1,111 @@ +import * as exec from '@actions/exec' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + fetchPackageVersion, + getBranchName, + getPrTitle, + parseDependencyInputs, + resolveDependencyInfos, + updatePackageDependencies, + validatePackageManager, +} from './main' + +vi.mock('@actions/core', () => ({ + endGroup: vi.fn(), + error: vi.fn(), + getBooleanInput: vi.fn(), + getInput: vi.fn(), + getMultilineInput: vi.fn(), + info: vi.fn(), + setFailed: vi.fn(), + startGroup: vi.fn(), +})) + +vi.mock('@actions/exec', () => ({ + exec: vi.fn(), + getExecOutput: vi.fn(), +})) + +vi.mock('@actions/github', () => ({ + context: { + eventName: 'workflow_dispatch', + repo: { + owner: 'Tencent', + repo: 'tdesign-vue-next', + }, + }, + getOctokit: vi.fn(), +})) + +describe('升级依赖', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('fetch', vi.fn()) + }) + + it('解析空格和换行分隔的依赖输入', () => { + expect(parseDependencyInputs([ + '@tdesign/site-components @tdesign/theme-generator', + 'vite', + ])).toEqual([ + '@tdesign/site-components', + '@tdesign/theme-generator', + 'vite', + ]) + }) + + it('拒绝带版本号的依赖输入', () => { + expect(() => parseDependencyInputs(['vite@7.0.0'])).toThrow('Dependency versions are not supported') + expect(() => parseDependencyInputs(['@tdesign/site-components@0.19.1'])).toThrow('Dependency versions are not supported') + }) + + it('拒绝空依赖输入', () => { + expect(() => parseDependencyInputs(['', ' '])).toThrow('Missing deps input') + }) + + it('拒绝不支持的 package-manager', () => { + expect(validatePackageManager('pnpm')).toBe('pnpm') + expect(() => validatePackageManager('bun')).toThrow('Unsupported package-manager "bun"') + }) + + it('全部依赖都查询 npm latest', async () => { + vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ version: '0.19.1' }), { status: 200 })) + vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ version: '7.0.0' }), { status: 200 })) + + await expect(resolveDependencyInfos([ + '@tdesign/site-components', + 'vite', + ])).resolves.toEqual([ + { name: '@tdesign/site-components', version: '0.19.1' }, + { name: 'vite', version: '7.0.0' }, + ]) + + expect(fetch).toHaveBeenCalledWith('https://registry.npmjs.org/@tdesign/site-components/latest') + }) + + it('npm registry 查询失败时中止流程', async () => { + vi.mocked(fetch).mockResolvedValueOnce(new Response('', { status: 404 })) + + await expect(fetchPackageVersion('@tdesign/missing')).rejects.toThrow( + 'Failed to get @tdesign/missing info from npm registry: status code: 404', + ) + }) + + it('按包管理器执行升级命令', async () => { + await updatePackageDependencies('npm', ['vite'], 'tdesign-vue-next', '') + expect(exec.exec).toHaveBeenLastCalledWith('npm', ['install', 'vite'], { cwd: './tdesign-vue-next' }) + + await updatePackageDependencies('pnpm', ['@tdesign/site-components'], 'tdesign-vue-next', 'site') + expect(exec.exec).toHaveBeenLastCalledWith('pnpm', ['up', '--latest', '@tdesign/site-components'], { cwd: 'tdesign-vue-next/site' }) + }) + + it('生成分支名和默认 PR 标题', () => { + const deps = [ + { name: '@tdesign/site-components', version: '0.19.1' }, + { name: 'vite', version: '^7.0.0' }, + ] + + expect(getBranchName(deps)).toBe('chore/deps/upgrade-tdesign-site-components-0.19.1-vite-7.0.0') + expect(getPrTitle(deps)).toBe('chore: upgrade @tdesign/site-components to 0.19.1, vite to ^7.0.0') + }) +}) diff --git a/packages/upgrade-deps/main.ts b/packages/upgrade-deps/main.ts index 9b11375..b0943ed 100644 --- a/packages/upgrade-deps/main.ts +++ b/packages/upgrade-deps/main.ts @@ -12,61 +12,98 @@ interface TriggerContext { trigger: string } -interface DependencyInfo { +export interface DependencyInfo { name: string version: string } -const PACKAGE_MANAGER_COMMANDS: Record = { +const PACKAGE_MANAGER_COMMANDS = { pnpm: { cmd: 'pnpm', args: ['up', '--latest'] }, yarn: { cmd: 'yarn', args: ['upgrade', '--latest'] }, - npm: { cmd: 'npm', args: ['update'] }, + npm: { cmd: 'npm', args: ['install'] }, +} as const + +type PackageManager = keyof typeof PACKAGE_MANAGER_COMMANDS + +function slugify(value: string): string { + return value.replace(/@/g, '').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '') } -function getBranchName(deps: DependencyInfo[]): string { - const depsSlug = deps.map(d => `${d.name.replace(/@/g, '').replace(/\//g, '-')}-${d.version}`).join('-') +export function getBranchName(deps: DependencyInfo[]): string { + const depsSlug = deps.map(d => `${slugify(d.name)}-${slugify(d.version)}`).join('-') return `chore/deps/upgrade-${depsSlug}` } -function getPrTitle(deps: DependencyInfo[]): string { +export function getPrTitle(deps: DependencyInfo[]): string { const depList = deps.map(d => `${d.name} to ${d.version}`).join(', ') return `chore: upgrade ${depList}` } -function getRepoPath(repo: string, targetDir: string): string { +export function getRepoPath(repo: string, targetDir: string): string { const base = `./${repo}` return targetDir ? path.join(base, targetDir) : base } -async function fetchPackageVersion(pkg: string): Promise { +export function parseDependencyName(spec: string): string { + const value = spec.trim() + if (!value) + throw new Error('Empty dependency name') + + const versionSeparator = value.startsWith('@') + ? value.indexOf('@', value.indexOf('/') + 1) + : value.lastIndexOf('@') + + if (versionSeparator > 0) + throw new Error(`Dependency versions are not supported: ${spec}. Please pass package names only.`) + + return value +} + +export function parseDependencyInputs(inputs: string[]): string[] { + const deps = inputs + .flatMap(input => input.split(/\s+/)) + .map(item => item.trim()) + .filter(Boolean) + .map(parseDependencyName) + + if (!deps.length) + throw new Error('Missing deps input') + + return deps +} + +export function validatePackageManager(packageManager: string): PackageManager { + if (packageManager in PACKAGE_MANAGER_COMMANDS) + return packageManager as PackageManager + + throw new Error(`Unsupported package-manager "${packageManager}". Supported values: npm, yarn, pnpm.`) +} + +export async function fetchPackageVersion(pkg: string): Promise { try { const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`) if (!response.ok) { - core.error(`Failed to get ${pkg} info from npm registry, status code: ${response.status}`) - return null + throw new Error(`status code: ${response.status}`) } const { version } = await response.json() as { version?: string } if (!version) { - core.error(`No version found for ${pkg}`) - return null + throw new Error('no version found') } core.info(`Latest version of ${pkg} is ${version}`) return { name: pkg, version } } catch (error) { - core.error(`Error fetching ${pkg}: ${error}`) - return null + throw new Error(`Failed to get ${pkg} info from npm registry: ${error instanceof Error ? error.message : String(error)}`) } } -async function getPkgLatestVersions(pkgNames: string[]): Promise { - const results = await Promise.all(pkgNames.map(fetchPackageVersion)) - return results.filter((r): r is DependencyInfo => r !== null) +export async function resolveDependencyInfos(deps: string[]): Promise { + return Promise.all(deps.map(fetchPackageVersion)) } -async function updatePackageDependencies(packageManager: string, deps: string[], repo: string, targetDir: string): Promise { +export async function updatePackageDependencies(packageManager: PackageManager, deps: string[], repo: string, targetDir: string): Promise { const repoPath = getRepoPath(repo, targetDir) - const { cmd, args } = PACKAGE_MANAGER_COMMANDS[packageManager] ?? PACKAGE_MANAGER_COMMANDS.npm + const { cmd, args } = PACKAGE_MANAGER_COMMANDS[packageManager] await exec.exec(cmd, [...args, ...deps], { cwd: repoPath }) } @@ -86,10 +123,10 @@ async function createDepsPr( } export async function updateDependencies(context: TriggerContext): Promise { - const packageManager = core.getInput('package-manager') || 'npm' + const packageManager = validatePackageManager(core.getInput('package-manager') || 'npm') const targetDir = core.getInput('target-dir') || '' const customTitle = core.getInput('title') || '' - const deps = core.getMultilineInput('deps', { required: true, trimWhitespace: true }) + const deps = parseDependencyInputs(core.getMultilineInput('deps', { required: true, trimWhitespace: true })) core.info(`deps: ${JSON.stringify(deps)}`) core.info(`target-dir: ${targetDir || 'default (repo root)'}`) @@ -97,11 +134,7 @@ export async function updateDependencies(context: TriggerContext): Promise core.info(`custom-title: ${customTitle}`) } - if (!deps.length) { - throw new Error('Missing deps input') - } - - const depInfos = await getPkgLatestVersions(deps) + const depInfos = await resolveDependencyInfos(deps) core.info(`depInfos: ${JSON.stringify(depInfos)}`) if (packageManager !== 'npm') { diff --git a/packages/upgrade-deps/package.json b/packages/upgrade-deps/package.json index 135d011..fe31ba1 100644 --- a/packages/upgrade-deps/package.json +++ b/packages/upgrade-deps/package.json @@ -1,7 +1,7 @@ { "name": "upgrade-deps", "type": "module", - "private": "true", + "private": true, "main": "index.ts", "scripts": { "build": "tsdown" diff --git a/packages/utils/git-helper.ts b/packages/utils/git-helper.ts index 75a3720..998c29c 100644 --- a/packages/utils/git-helper.ts +++ b/packages/utils/git-helper.ts @@ -92,7 +92,8 @@ export class GitHelper { } async commit(message: string): Promise { - await exec('git', ['commit', '-am', message, '--no-verify'], { cwd: this.repoPath }) + await exec('git', ['add', '-A'], { cwd: this.repoPath }) + await exec('git', ['commit', '-m', message, '--no-verify'], { cwd: this.repoPath }) } async push(branch: string, forkOwner?: string): Promise {