Skip to content

Commit da41ad1

Browse files
authored
fix(upgrade-deps): 修复依赖升级流程多项缺陷 (#72)
- 修复 git commit 未暂存新文件的问题,改为先 git add -A 再 commit - 修复 npm 更新命令为 install 替代 update - 修复 npm registry 查询失败时静默跳过的问题,改为抛出错误中止流程 - 增加依赖输入和 package-manager 的严格校验 - 捕获主流程异常并调用 core.setFailed 标记 Action 失败
1 parent 18f8936 commit da41ad1

6 files changed

Lines changed: 244 additions & 74 deletions

File tree

packages/upgrade-deps/dist/index.mjs

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createRequire } from "node:module";
2-
import * as path from "node:path";
32
import * as os$1 from "os";
43
import os, { EOL } from "os";
54
import * as fs from "fs";
@@ -9,6 +8,7 @@ import * as events from "events";
98
import { StringDecoder } from "string_decoder";
109
import * as child from "child_process";
1110
import { setTimeout as setTimeout$1 } from "timers";
11+
import * as path from "node:path";
1212
//#region \0rolldown/runtime.js
1313
var __create = Object.create;
1414
var __defProp = Object.defineProperty;
@@ -16641,6 +16641,15 @@ function getBooleanInput(name, options) {
1664116641
throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\nSupport boolean input list: \`true | True | TRUE | false | False | FALSE\``);
1664216642
}
1664316643
/**
16644+
* Sets the action status to failed.
16645+
* When the action exits it will be with an exit code of 1
16646+
* @param message add error issue message
16647+
*/
16648+
function setFailed(message) {
16649+
process.exitCode = ExitCode.Failure;
16650+
error(message);
16651+
}
16652+
/**
1664416653
* Adds an error issue
1664516654
* @param message error issue message. Errors will be converted to string via toString()
1664616655
* @param properties optional properties to add to the annotation.
@@ -19856,9 +19865,10 @@ var GitHelper = class {
1985619865
], { cwd: this.repoPath });
1985719866
}
1985819867
async commit(message) {
19868+
await exec("git", ["add", "-A"], { cwd: this.repoPath });
1985919869
await exec("git", [
1986019870
"commit",
19861-
"-am",
19871+
"-m",
1986219872
message,
1986319873
"--no-verify"
1986419874
], { cwd: this.repoPath });
@@ -19934,9 +19944,9 @@ var GithubHelper = class {
1993419944
pull_number: prNumber
1993519945
});
1993619946
return data;
19937-
} catch (error$4) {
19938-
error(`获取PR数据失败: ${error$4}`);
19939-
throw error$4;
19947+
} catch (error$3) {
19948+
error(`获取PR数据失败: ${error$3}`);
19949+
throw error$3;
1994019950
}
1994119951
}
1994219952
async getIssueData(issueNumber) {
@@ -19946,9 +19956,9 @@ var GithubHelper = class {
1994619956
issue_number: issueNumber
1994719957
});
1994819958
return data;
19949-
} catch (error$6) {
19950-
error(`获取Issue数据失败: ${error$6}`);
19951-
throw error$6;
19959+
} catch (error$5) {
19960+
error(`获取Issue数据失败: ${error$5}`);
19961+
throw error$5;
1995219962
}
1995319963
}
1995419964
async getIssueList(params) {
@@ -19958,9 +19968,9 @@ var GithubHelper = class {
1995819968
...this.defaultRepoParams
1995919969
});
1996019970
return data.filter((item) => !item?.pull_request);
19961-
} catch (error$5) {
19962-
error(`获取Issue列表失败: ${error$5}`);
19963-
throw error$5;
19971+
} catch (error$4) {
19972+
error(`获取Issue列表失败: ${error$4}`);
19973+
throw error$4;
1996419974
}
1996519975
}
1996619976
async closeIssue(issueNumber) {
@@ -19971,9 +19981,9 @@ var GithubHelper = class {
1997119981
issue_number: issueNumber,
1997219982
state: "closed"
1997319983
});
19974-
} catch (error$7) {
19975-
error(`关闭Issue失败: ${error$7}`);
19976-
throw error$7;
19984+
} catch (error$6) {
19985+
error(`关闭Issue失败: ${error$6}`);
19986+
throw error$6;
1997719987
}
1997819988
}
1997919989
async createPR(title, head, body, base = "develop") {
@@ -19992,9 +20002,9 @@ var GithubHelper = class {
1999220002
body
1999320003
});
1999420004
return data;
19995-
} catch (error$2) {
19996-
error(`创建PR失败: ${error$2}`);
19997-
throw error$2;
20005+
} catch (error$1) {
20006+
error(`创建PR失败: ${error$1}`);
20007+
throw error$1;
1999820008
}
1999920009
}
2000020010
async addComment(issueNumber, body) {
@@ -20009,9 +20019,9 @@ var GithubHelper = class {
2000920019
body
2001020020
});
2001120021
return data;
20012-
} catch (error$3) {
20013-
error(`添加评论失败: ${error$3}`);
20014-
throw error$3;
20022+
} catch (error$2) {
20023+
error(`添加评论失败: ${error$2}`);
20024+
throw error$2;
2001520025
}
2001620026
}
2001720027
async addLabels(issueNumber, labels) {
@@ -20026,9 +20036,9 @@ var GithubHelper = class {
2002620036
labels
2002720037
});
2002820038
return data;
20029-
} catch (error$8) {
20030-
error(`添加标签失败: ${error$8}`);
20031-
throw error$8;
20039+
} catch (error$7) {
20040+
error(`添加标签失败: ${error$7}`);
20041+
throw error$7;
2003220042
}
2003320043
}
2003420044
};
@@ -20045,11 +20055,14 @@ const PACKAGE_MANAGER_COMMANDS = {
2004520055
},
2004620056
npm: {
2004720057
cmd: "npm",
20048-
args: ["update"]
20058+
args: ["install"]
2004920059
}
2005020060
};
20061+
function slugify(value) {
20062+
return value.replace(/@/g, "").replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
20063+
}
2005120064
function getBranchName(deps) {
20052-
return `chore/deps/upgrade-${deps.map((d) => `${d.name.replace(/@/g, "").replace(/\//g, "-")}-${d.version}`).join("-")}`;
20065+
return `chore/deps/upgrade-${deps.map((d) => `${slugify(d.name)}-${slugify(d.version)}`).join("-")}`;
2005320066
}
2005420067
function getPrTitle(deps) {
2005520068
return `chore: upgrade ${deps.map((d) => `${d.name} to ${d.version}`).join(", ")}`;
@@ -20058,34 +20071,42 @@ function getRepoPath(repo, targetDir) {
2005820071
const base = `./${repo}`;
2005920072
return targetDir ? path.join(base, targetDir) : base;
2006020073
}
20074+
function parseDependencyName(spec) {
20075+
const value = spec.trim();
20076+
if (!value) throw new Error("Empty dependency name");
20077+
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.`);
20078+
return value;
20079+
}
20080+
function parseDependencyInputs(inputs) {
20081+
const deps = inputs.flatMap((input) => input.split(/\s+/)).map((item) => item.trim()).filter(Boolean).map(parseDependencyName);
20082+
if (!deps.length) throw new Error("Missing deps input");
20083+
return deps;
20084+
}
20085+
function validatePackageManager(packageManager) {
20086+
if (packageManager in PACKAGE_MANAGER_COMMANDS) return packageManager;
20087+
throw new Error(`Unsupported package-manager "${packageManager}". Supported values: npm, yarn, pnpm.`);
20088+
}
2006120089
async function fetchPackageVersion(pkg) {
2006220090
try {
2006320091
const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`);
20064-
if (!response.ok) {
20065-
error(`Failed to get ${pkg} info from npm registry, status code: ${response.status}`);
20066-
return null;
20067-
}
20092+
if (!response.ok) throw new Error(`status code: ${response.status}`);
2006820093
const { version } = await response.json();
20069-
if (!version) {
20070-
error(`No version found for ${pkg}`);
20071-
return null;
20072-
}
20094+
if (!version) throw new Error("no version found");
2007320095
info(`Latest version of ${pkg} is ${version}`);
2007420096
return {
2007520097
name: pkg,
2007620098
version
2007720099
};
20078-
} catch (error$1) {
20079-
error(`Error fetching ${pkg}: ${error$1}`);
20080-
return null;
20100+
} catch (error) {
20101+
throw new Error(`Failed to get ${pkg} info from npm registry: ${error instanceof Error ? error.message : String(error)}`);
2008120102
}
2008220103
}
20083-
async function getPkgLatestVersions(pkgNames) {
20084-
return (await Promise.all(pkgNames.map(fetchPackageVersion))).filter((r) => r !== null);
20104+
async function resolveDependencyInfos(deps) {
20105+
return Promise.all(deps.map(fetchPackageVersion));
2008520106
}
2008620107
async function updatePackageDependencies(packageManager, deps, repo, targetDir) {
2008720108
const repoPath = getRepoPath(repo, targetDir);
20088-
const { cmd, args } = PACKAGE_MANAGER_COMMANDS[packageManager] ?? PACKAGE_MANAGER_COMMANDS.npm;
20109+
const { cmd, args } = PACKAGE_MANAGER_COMMANDS[packageManager];
2008920110
await exec(cmd, [...args, ...deps], { cwd: repoPath });
2009020111
}
2009120112
async function createDepsPr(title, branchName, baseBranch, context) {
@@ -20097,18 +20118,17 @@ async function createDepsPr(title, branchName, baseBranch, context) {
2009720118
}).createPR(title, branchName, title, baseBranch);
2009820119
}
2009920120
async function updateDependencies(context) {
20100-
const packageManager = getInput("package-manager") || "npm";
20121+
const packageManager = validatePackageManager(getInput("package-manager") || "npm");
2010120122
const targetDir = getInput("target-dir") || "";
2010220123
const customTitle = getInput("title") || "";
20103-
const deps = getMultilineInput("deps", {
20124+
const deps = parseDependencyInputs(getMultilineInput("deps", {
2010420125
required: true,
2010520126
trimWhitespace: true
20106-
});
20127+
}));
2010720128
info(`deps: ${JSON.stringify(deps)}`);
2010820129
info(`target-dir: ${targetDir || "default (repo root)"}`);
2010920130
if (customTitle) info(`custom-title: ${customTitle}`);
20110-
if (!deps.length) throw new Error("Missing deps input");
20111-
const depInfos = await getPkgLatestVersions(deps);
20131+
const depInfos = await resolveDependencyInfos(deps);
2011220132
info(`depInfos: ${JSON.stringify(depInfos)}`);
2011320133
if (packageManager !== "npm") await exec("corepack", ["enable"]);
2011420134
const gitHelper = new GitHelper({
@@ -20153,6 +20173,8 @@ async function main() {
2015320173
}
2015420174
//#endregion
2015520175
//#region index.ts
20156-
main();
20176+
main().catch((error) => {
20177+
setFailed(`upgrade-deps failed: ${error instanceof Error ? error.message : String(error)}`);
20178+
});
2015720179
//#endregion
2015820180
export {};

packages/upgrade-deps/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import * as core from '@actions/core'
12
import { main } from './main'
23

3-
main()
4+
main().catch((error) => {
5+
core.setFailed(`upgrade-deps failed: ${error instanceof Error ? error.message : String(error)}`)
6+
})

packages/upgrade-deps/main.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as exec from '@actions/exec'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import {
4+
fetchPackageVersion,
5+
getBranchName,
6+
getPrTitle,
7+
parseDependencyInputs,
8+
resolveDependencyInfos,
9+
updatePackageDependencies,
10+
validatePackageManager,
11+
} from './main'
12+
13+
vi.mock('@actions/core', () => ({
14+
endGroup: vi.fn(),
15+
error: vi.fn(),
16+
getBooleanInput: vi.fn(),
17+
getInput: vi.fn(),
18+
getMultilineInput: vi.fn(),
19+
info: vi.fn(),
20+
setFailed: vi.fn(),
21+
startGroup: vi.fn(),
22+
}))
23+
24+
vi.mock('@actions/exec', () => ({
25+
exec: vi.fn(),
26+
getExecOutput: vi.fn(),
27+
}))
28+
29+
vi.mock('@actions/github', () => ({
30+
context: {
31+
eventName: 'workflow_dispatch',
32+
repo: {
33+
owner: 'Tencent',
34+
repo: 'tdesign-vue-next',
35+
},
36+
},
37+
getOctokit: vi.fn(),
38+
}))
39+
40+
describe('升级依赖', () => {
41+
beforeEach(() => {
42+
vi.clearAllMocks()
43+
vi.stubGlobal('fetch', vi.fn())
44+
})
45+
46+
it('解析空格和换行分隔的依赖输入', () => {
47+
expect(parseDependencyInputs([
48+
'@tdesign/site-components @tdesign/theme-generator',
49+
'vite',
50+
])).toEqual([
51+
'@tdesign/site-components',
52+
'@tdesign/theme-generator',
53+
'vite',
54+
])
55+
})
56+
57+
it('拒绝带版本号的依赖输入', () => {
58+
expect(() => parseDependencyInputs(['vite@7.0.0'])).toThrow('Dependency versions are not supported')
59+
expect(() => parseDependencyInputs(['@tdesign/site-components@0.19.1'])).toThrow('Dependency versions are not supported')
60+
})
61+
62+
it('拒绝空依赖输入', () => {
63+
expect(() => parseDependencyInputs(['', ' '])).toThrow('Missing deps input')
64+
})
65+
66+
it('拒绝不支持的 package-manager', () => {
67+
expect(validatePackageManager('pnpm')).toBe('pnpm')
68+
expect(() => validatePackageManager('bun')).toThrow('Unsupported package-manager "bun"')
69+
})
70+
71+
it('全部依赖都查询 npm latest', async () => {
72+
vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ version: '0.19.1' }), { status: 200 }))
73+
vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ version: '7.0.0' }), { status: 200 }))
74+
75+
await expect(resolveDependencyInfos([
76+
'@tdesign/site-components',
77+
'vite',
78+
])).resolves.toEqual([
79+
{ name: '@tdesign/site-components', version: '0.19.1' },
80+
{ name: 'vite', version: '7.0.0' },
81+
])
82+
83+
expect(fetch).toHaveBeenCalledWith('https://registry.npmjs.org/@tdesign/site-components/latest')
84+
})
85+
86+
it('npm registry 查询失败时中止流程', async () => {
87+
vi.mocked(fetch).mockResolvedValueOnce(new Response('', { status: 404 }))
88+
89+
await expect(fetchPackageVersion('@tdesign/missing')).rejects.toThrow(
90+
'Failed to get @tdesign/missing info from npm registry: status code: 404',
91+
)
92+
})
93+
94+
it('按包管理器执行升级命令', async () => {
95+
await updatePackageDependencies('npm', ['vite'], 'tdesign-vue-next', '')
96+
expect(exec.exec).toHaveBeenLastCalledWith('npm', ['install', 'vite'], { cwd: './tdesign-vue-next' })
97+
98+
await updatePackageDependencies('pnpm', ['@tdesign/site-components'], 'tdesign-vue-next', 'site')
99+
expect(exec.exec).toHaveBeenLastCalledWith('pnpm', ['up', '--latest', '@tdesign/site-components'], { cwd: 'tdesign-vue-next/site' })
100+
})
101+
102+
it('生成分支名和默认 PR 标题', () => {
103+
const deps = [
104+
{ name: '@tdesign/site-components', version: '0.19.1' },
105+
{ name: 'vite', version: '^7.0.0' },
106+
]
107+
108+
expect(getBranchName(deps)).toBe('chore/deps/upgrade-tdesign-site-components-0.19.1-vite-7.0.0')
109+
expect(getPrTitle(deps)).toBe('chore: upgrade @tdesign/site-components to 0.19.1, vite to ^7.0.0')
110+
})
111+
})

0 commit comments

Comments
 (0)