@infly/libs 2.0.53 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -3
- package/adapters/webpack/auto-export.js +52 -52
- package/bin/cli.js +12 -0
- package/build/build-utils/command.js +57 -57
- package/build/build-utils/git.js +254 -254
- package/build/build-utils/preview.js +78 -78
- package/build/build-utils/webhook-single.js +136 -136
- package/build/docker/compose-command.js +73 -73
- package/build/docker/compose-release.js +19 -12
- package/build/docker/deployment/deploy.sh +30 -0
- package/build/docker/deployment/health-check.sh +28 -0
- package/build/docker/deployment/promote-image.sh +61 -0
- package/build/docker/deployment/release.sh +74 -0
- package/build/docker/deployment-bundle.js +87 -0
- package/build/docker/target-release.js +180 -0
- package/build/project-files.js +47 -47
- package/cli/progress.js +2 -0
- package/cli/project-command.js +96 -12
- package/cli/select-option.js +62 -30
- package/core/async/concurrency.js +29 -29
- package/package.json +2 -2
- package/script/git-automation/submodule-utils.js +16 -16
- package/workspace/config.js +144 -144
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
function shouldSkipWebhook() {
|
|
2
|
-
const args = process.argv || [];
|
|
3
|
-
const hasNoWebhookArg = args.some((arg = "") =>
|
|
4
|
-
["--nowebhook", "--noWebhook", "--no-webhook"].includes(arg)
|
|
5
|
-
);
|
|
6
|
-
|
|
7
|
-
const env = process.env || {};
|
|
8
|
-
const envValues = [
|
|
9
|
-
env.npm_config_nowebhook,
|
|
10
|
-
env.npm_config_noWebhook,
|
|
11
|
-
env.NO_WEBHOOK,
|
|
12
|
-
env.DISABLE_WEBHOOK
|
|
13
|
-
];
|
|
14
|
-
const hasNoWebhookEnv = envValues.some((value) => {
|
|
15
|
-
const normalized = String(value || "").toLowerCase();
|
|
16
|
-
return ["1", "true", "yes", "on"].includes(normalized);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return hasNoWebhookArg || hasNoWebhookEnv;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getWebhookMessage() {
|
|
23
|
-
const args = process.argv || [];
|
|
24
|
-
const env = process.env || {};
|
|
25
|
-
const messageEnv =
|
|
26
|
-
env.npm_config_m ||
|
|
27
|
-
env.npm_config_message ||
|
|
28
|
-
env.WEBHOOK_MESSAGE ||
|
|
29
|
-
env.BUILD_MESSAGE ||
|
|
30
|
-
"";
|
|
31
|
-
|
|
32
|
-
if (messageEnv) {
|
|
33
|
-
return String(messageEnv).trim();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
37
|
-
const arg = args[index] || "";
|
|
38
|
-
|
|
39
|
-
if (arg === "-m" || arg === "--message") {
|
|
40
|
-
return String(args[index + 1] || "").trim();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (arg.startsWith("-m=")) {
|
|
44
|
-
return arg.slice(3).trim();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (arg.startsWith("--message=")) {
|
|
48
|
-
return arg.slice(10).trim();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return "";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function postVersionFileAndMsg(publishText, extraParams = {}) {
|
|
56
|
-
const {
|
|
57
|
-
projectName,
|
|
58
|
-
newFileName,
|
|
59
|
-
gitInfo,
|
|
60
|
-
webhookUrl = "", // 启用企业微信推送
|
|
61
|
-
webhookAtUser = [],
|
|
62
|
-
webhookExtraText = "",
|
|
63
|
-
gitAutoPushRepos,
|
|
64
|
-
gitAutoPushReposBranchMap,
|
|
65
|
-
repos,
|
|
66
|
-
deployUrl = ""
|
|
67
|
-
} = extraParams || {};
|
|
68
|
-
const { branch, commitMsg, author } = gitInfo || {};
|
|
69
|
-
const webhookMessage = getWebhookMessage();
|
|
70
|
-
const GITPATH = `https://gitee.com/gdinfly_1/${projectName}/blob/${branch}/${newFileName}`;
|
|
71
|
-
const UPLOADURL = `https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=&type=file`;
|
|
72
|
-
/* const data = {
|
|
73
|
-
msgtype: "markdown",
|
|
74
|
-
markdown: {
|
|
75
|
-
content: `${publishText}\n构建文件:[点击下载](${GITPATH})`,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
if (Array.isArray(webhookAtUser) && webhookAtUser.length > 0) {
|
|
79
|
-
webhookAtUser.forEach((item, index) => {
|
|
80
|
-
data.markdown.content = `${data.markdown.content}\n<@${item}>`;
|
|
81
|
-
});
|
|
82
|
-
} */
|
|
83
|
-
let content = publishText;
|
|
84
|
-
if (gitAutoPushReposBranchMap) {
|
|
85
|
-
content = [
|
|
86
|
-
publishText,
|
|
87
|
-
`构建文件:已推送对应仓库【${repos}】- 分支【${gitAutoPushReposBranchMap[branch]}】`,
|
|
88
|
-
`构建人:${author}`,
|
|
89
|
-
deployUrl ? `部署系统:${deployUrl}` : ""
|
|
90
|
-
].filter(Boolean).join("\n");
|
|
91
|
-
} else {
|
|
92
|
-
content = `${publishText}\n构建文件下载地址:${GITPATH}\n${webhookExtraText}\n`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (webhookMessage) {
|
|
96
|
-
content = `${content}\n${webhookMessage}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 防御性过滤:防止运行时数据中的 %s 等格式化占位符泄露到推送内容
|
|
100
|
-
content = (content || "").replace(/%s/g, "");
|
|
101
|
-
|
|
102
|
-
const data = {
|
|
103
|
-
msgtype: "text",
|
|
104
|
-
text: {
|
|
105
|
-
content,
|
|
106
|
-
mentioned_mobile_list: Array.isArray(webhookAtUser) ? webhookAtUser : webhookAtUser[branch] // 支持按分支配置不同的提醒人
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
if (shouldSkipWebhook()) {
|
|
111
|
-
console.log(global.logColor?.info || "%s", "🔕 已跳过企业微信机器人推送(nowebhook)");
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const axios = require("axios");
|
|
117
|
-
|
|
118
|
-
if (axios) {
|
|
119
|
-
axios
|
|
120
|
-
.post(webhookUrl, data, {
|
|
121
|
-
headers: {
|
|
122
|
-
"Content-Type": "application/json"
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
.catch((e) => {
|
|
126
|
-
console.error("请求出错:", e);
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error("axios is not available, please install it.");
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
module.exports = {
|
|
135
|
-
postVersionFileAndMsg
|
|
136
|
-
};
|
|
1
|
+
function shouldSkipWebhook() {
|
|
2
|
+
const args = process.argv || [];
|
|
3
|
+
const hasNoWebhookArg = args.some((arg = "") =>
|
|
4
|
+
["--nowebhook", "--noWebhook", "--no-webhook"].includes(arg)
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
const env = process.env || {};
|
|
8
|
+
const envValues = [
|
|
9
|
+
env.npm_config_nowebhook,
|
|
10
|
+
env.npm_config_noWebhook,
|
|
11
|
+
env.NO_WEBHOOK,
|
|
12
|
+
env.DISABLE_WEBHOOK
|
|
13
|
+
];
|
|
14
|
+
const hasNoWebhookEnv = envValues.some((value) => {
|
|
15
|
+
const normalized = String(value || "").toLowerCase();
|
|
16
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return hasNoWebhookArg || hasNoWebhookEnv;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getWebhookMessage() {
|
|
23
|
+
const args = process.argv || [];
|
|
24
|
+
const env = process.env || {};
|
|
25
|
+
const messageEnv =
|
|
26
|
+
env.npm_config_m ||
|
|
27
|
+
env.npm_config_message ||
|
|
28
|
+
env.WEBHOOK_MESSAGE ||
|
|
29
|
+
env.BUILD_MESSAGE ||
|
|
30
|
+
"";
|
|
31
|
+
|
|
32
|
+
if (messageEnv) {
|
|
33
|
+
return String(messageEnv).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
37
|
+
const arg = args[index] || "";
|
|
38
|
+
|
|
39
|
+
if (arg === "-m" || arg === "--message") {
|
|
40
|
+
return String(args[index + 1] || "").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (arg.startsWith("-m=")) {
|
|
44
|
+
return arg.slice(3).trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (arg.startsWith("--message=")) {
|
|
48
|
+
return arg.slice(10).trim();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function postVersionFileAndMsg(publishText, extraParams = {}) {
|
|
56
|
+
const {
|
|
57
|
+
projectName,
|
|
58
|
+
newFileName,
|
|
59
|
+
gitInfo,
|
|
60
|
+
webhookUrl = "", // 启用企业微信推送
|
|
61
|
+
webhookAtUser = [],
|
|
62
|
+
webhookExtraText = "",
|
|
63
|
+
gitAutoPushRepos,
|
|
64
|
+
gitAutoPushReposBranchMap,
|
|
65
|
+
repos,
|
|
66
|
+
deployUrl = ""
|
|
67
|
+
} = extraParams || {};
|
|
68
|
+
const { branch, commitMsg, author } = gitInfo || {};
|
|
69
|
+
const webhookMessage = getWebhookMessage();
|
|
70
|
+
const GITPATH = `https://gitee.com/gdinfly_1/${projectName}/blob/${branch}/${newFileName}`;
|
|
71
|
+
const UPLOADURL = `https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=&type=file`;
|
|
72
|
+
/* const data = {
|
|
73
|
+
msgtype: "markdown",
|
|
74
|
+
markdown: {
|
|
75
|
+
content: `${publishText}\n构建文件:[点击下载](${GITPATH})`,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
if (Array.isArray(webhookAtUser) && webhookAtUser.length > 0) {
|
|
79
|
+
webhookAtUser.forEach((item, index) => {
|
|
80
|
+
data.markdown.content = `${data.markdown.content}\n<@${item}>`;
|
|
81
|
+
});
|
|
82
|
+
} */
|
|
83
|
+
let content = publishText;
|
|
84
|
+
if (gitAutoPushReposBranchMap) {
|
|
85
|
+
content = [
|
|
86
|
+
publishText,
|
|
87
|
+
`构建文件:已推送对应仓库【${repos}】- 分支【${gitAutoPushReposBranchMap[branch]}】`,
|
|
88
|
+
`构建人:${author}`,
|
|
89
|
+
deployUrl ? `部署系统:${deployUrl}` : ""
|
|
90
|
+
].filter(Boolean).join("\n");
|
|
91
|
+
} else {
|
|
92
|
+
content = `${publishText}\n构建文件下载地址:${GITPATH}\n${webhookExtraText}\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (webhookMessage) {
|
|
96
|
+
content = `${content}\n${webhookMessage}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 防御性过滤:防止运行时数据中的 %s 等格式化占位符泄露到推送内容
|
|
100
|
+
content = (content || "").replace(/%s/g, "");
|
|
101
|
+
|
|
102
|
+
const data = {
|
|
103
|
+
msgtype: "text",
|
|
104
|
+
text: {
|
|
105
|
+
content,
|
|
106
|
+
mentioned_mobile_list: Array.isArray(webhookAtUser) ? webhookAtUser : webhookAtUser[branch] // 支持按分支配置不同的提醒人
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (shouldSkipWebhook()) {
|
|
111
|
+
console.log(global.logColor?.info || "%s", "🔕 已跳过企业微信机器人推送(nowebhook)");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const axios = require("axios");
|
|
117
|
+
|
|
118
|
+
if (axios) {
|
|
119
|
+
axios
|
|
120
|
+
.post(webhookUrl, data, {
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Type": "application/json"
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.catch((e) => {
|
|
126
|
+
console.error("请求出错:", e);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("axios is not available, please install it.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
postVersionFileAndMsg
|
|
136
|
+
};
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
const fs = require("node:fs");
|
|
2
|
-
const path = require("node:path");
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
EXIT_SELECTION,
|
|
6
|
-
selectOption: defaultSelectOption,
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
EXIT_SELECTION,
|
|
6
|
+
selectOption: defaultSelectOption,
|
|
7
7
|
} = require("../../cli/select-option");
|
|
8
|
-
const { composeRelease: defaultComposeRelease } = require("./compose-release");
|
|
9
|
-
|
|
10
|
-
function loadTargetConfig(configPath) {
|
|
11
|
-
const resolvedPath = path.resolve(configPath);
|
|
12
|
-
delete require.cache[require.resolve(resolvedPath)];
|
|
13
|
-
return {
|
|
14
|
-
config: require(resolvedPath),
|
|
15
|
-
configDir: path.dirname(resolvedPath),
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function loadPackageTargetConfig(cwd = process.cwd()) {
|
|
20
|
-
const packagePath = path.resolve(cwd, "package.json");
|
|
21
|
-
if (!fs.existsSync(packagePath)) {
|
|
22
|
-
throw new Error(`Package config not found: ${packagePath}`);
|
|
23
|
-
}
|
|
24
|
-
delete require.cache[require.resolve(packagePath)];
|
|
25
|
-
const packageJson = require(packagePath);
|
|
26
|
-
return {
|
|
27
|
-
config: packageJson.infly?.docker,
|
|
28
|
-
configDir: path.dirname(packagePath),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function runComposeCommand(options = {}, dependencies = {}) {
|
|
33
|
-
const composeRelease = dependencies.composeRelease || defaultComposeRelease;
|
|
34
|
-
const usesDirectConfig = !options.config
|
|
35
|
-
&& !options.configObject
|
|
36
|
-
&& (options.projectDir || options.file);
|
|
37
|
-
if (usesDirectConfig) return composeRelease(options);
|
|
38
|
-
|
|
39
|
-
const loaded = options.config
|
|
40
|
-
? loadTargetConfig(options.config)
|
|
41
|
-
: options.configObject
|
|
42
|
-
? { config: options.configObject, configDir: options.configDir || process.cwd() }
|
|
43
|
-
: loadPackageTargetConfig(options.cwd || process.cwd());
|
|
44
|
-
const targets = loaded.config?.targets;
|
|
45
|
-
if (!targets || Object.keys(targets).length === 0) {
|
|
46
|
-
throw new Error("Docker target config must define at least one target.");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const selectOption = dependencies.selectOption || defaultSelectOption;
|
|
50
|
-
const log = dependencies.log || console.log;
|
|
51
|
-
const targetName = options.target || await selectOption(
|
|
52
|
-
"请选择 Docker 发布目标",
|
|
53
|
-
Object.entries(targets).map(([value, target]) => ({
|
|
54
|
-
value,
|
|
55
|
-
label: target.label || value,
|
|
56
|
-
})),
|
|
57
|
-
);
|
|
58
|
-
if (targetName === EXIT_SELECTION) {
|
|
59
|
-
log("已退出,未执行任何操作。");
|
|
60
|
-
return { cancelled: true };
|
|
61
|
-
}
|
|
62
|
-
const target = targets[targetName];
|
|
63
|
-
if (!target) throw new Error(`Unknown Docker target: ${targetName}`);
|
|
64
|
-
|
|
65
|
-
return composeRelease({
|
|
66
|
-
projectDir: loaded.configDir,
|
|
67
|
-
file: target.file,
|
|
68
|
-
bump: options.bump || "patch",
|
|
69
|
-
push: Boolean(options.push),
|
|
70
|
-
dryRun: Boolean(options.dryRun),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
module.exports = { loadPackageTargetConfig, loadTargetConfig, runComposeCommand };
|
|
8
|
+
const { composeRelease: defaultComposeRelease } = require("./compose-release");
|
|
9
|
+
|
|
10
|
+
function loadTargetConfig(configPath) {
|
|
11
|
+
const resolvedPath = path.resolve(configPath);
|
|
12
|
+
delete require.cache[require.resolve(resolvedPath)];
|
|
13
|
+
return {
|
|
14
|
+
config: require(resolvedPath),
|
|
15
|
+
configDir: path.dirname(resolvedPath),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadPackageTargetConfig(cwd = process.cwd()) {
|
|
20
|
+
const packagePath = path.resolve(cwd, "package.json");
|
|
21
|
+
if (!fs.existsSync(packagePath)) {
|
|
22
|
+
throw new Error(`Package config not found: ${packagePath}`);
|
|
23
|
+
}
|
|
24
|
+
delete require.cache[require.resolve(packagePath)];
|
|
25
|
+
const packageJson = require(packagePath);
|
|
26
|
+
return {
|
|
27
|
+
config: packageJson.infly?.docker,
|
|
28
|
+
configDir: path.dirname(packagePath),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runComposeCommand(options = {}, dependencies = {}) {
|
|
33
|
+
const composeRelease = dependencies.composeRelease || defaultComposeRelease;
|
|
34
|
+
const usesDirectConfig = !options.config
|
|
35
|
+
&& !options.configObject
|
|
36
|
+
&& (options.projectDir || options.file);
|
|
37
|
+
if (usesDirectConfig) return composeRelease(options);
|
|
38
|
+
|
|
39
|
+
const loaded = options.config
|
|
40
|
+
? loadTargetConfig(options.config)
|
|
41
|
+
: options.configObject
|
|
42
|
+
? { config: options.configObject, configDir: options.configDir || process.cwd() }
|
|
43
|
+
: loadPackageTargetConfig(options.cwd || process.cwd());
|
|
44
|
+
const targets = loaded.config?.targets;
|
|
45
|
+
if (!targets || Object.keys(targets).length === 0) {
|
|
46
|
+
throw new Error("Docker target config must define at least one target.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const selectOption = dependencies.selectOption || defaultSelectOption;
|
|
50
|
+
const log = dependencies.log || console.log;
|
|
51
|
+
const targetName = options.target || await selectOption(
|
|
52
|
+
"请选择 Docker 发布目标",
|
|
53
|
+
Object.entries(targets).map(([value, target]) => ({
|
|
54
|
+
value,
|
|
55
|
+
label: target.label || value,
|
|
56
|
+
})),
|
|
57
|
+
);
|
|
58
|
+
if (targetName === EXIT_SELECTION) {
|
|
59
|
+
log("已退出,未执行任何操作。");
|
|
60
|
+
return { cancelled: true };
|
|
61
|
+
}
|
|
62
|
+
const target = targets[targetName];
|
|
63
|
+
if (!target) throw new Error(`Unknown Docker target: ${targetName}`);
|
|
64
|
+
|
|
65
|
+
return composeRelease({
|
|
66
|
+
projectDir: loaded.configDir,
|
|
67
|
+
file: target.file,
|
|
68
|
+
bump: options.bump || "patch",
|
|
69
|
+
push: Boolean(options.push),
|
|
70
|
+
dryRun: Boolean(options.dryRun),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { loadPackageTargetConfig, loadTargetConfig, runComposeCommand };
|
|
@@ -2,9 +2,9 @@ const { execFileSync } = require("node:child_process");
|
|
|
2
2
|
const fs = require("node:fs");
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
|
|
5
|
-
function bumpComposeImageVersion(content, bump = "patch") {
|
|
6
|
-
if (
|
|
7
|
-
throw new Error(`Unsupported version bump: ${bump}.
|
|
5
|
+
function bumpComposeImageVersion(content, bump = "patch") {
|
|
6
|
+
if (!["none", "patch"].includes(bump)) {
|
|
7
|
+
throw new Error(`Unsupported version bump: ${bump}. Supported values: none, patch.`);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const imageRegex = /^(\s*image:\s*)([^\s#]+):(\d+)\.(\d+)\.(\d+)(\s*(?:#.*)?)$/gm;
|
|
@@ -14,10 +14,12 @@ function bumpComposeImageVersion(content, bump = "patch") {
|
|
|
14
14
|
throw new Error(`Compose file must contain exactly one versioned image; found ${matches.length}.`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const [, prefix, image, major, minor, patchVersion, suffix] = matches[0];
|
|
18
|
-
const from = `${image}:${major}.${minor}.${patchVersion}`;
|
|
19
|
-
const to =
|
|
20
|
-
|
|
17
|
+
const [, prefix, image, major, minor, patchVersion, suffix] = matches[0];
|
|
18
|
+
const from = `${image}:${major}.${minor}.${patchVersion}`;
|
|
19
|
+
const to = bump === "none"
|
|
20
|
+
? from
|
|
21
|
+
: `${image}:${major}.${minor}.${Number(patchVersion) + 1}`;
|
|
22
|
+
const replacement = `${prefix}${to}${suffix}`;
|
|
21
23
|
|
|
22
24
|
return {
|
|
23
25
|
content: content.replace(imageRegex, replacement),
|
|
@@ -62,7 +64,11 @@ function composeRelease(options = {}, dependencies = {}) {
|
|
|
62
64
|
const updated = bumpComposeImageVersion(original, bump);
|
|
63
65
|
const baseArgs = ["compose", "-f", resolved.composePath];
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
if (bump === "none") {
|
|
68
|
+
console.log(`Version fixed: ${updated.from}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.log(`Version update: ${updated.from} -> ${updated.to}`);
|
|
71
|
+
}
|
|
66
72
|
|
|
67
73
|
if (dryRun) {
|
|
68
74
|
console.log(`[dry-run] docker ${[...baseArgs, "config"].join(" ")}`);
|
|
@@ -71,13 +77,14 @@ function composeRelease(options = {}, dependencies = {}) {
|
|
|
71
77
|
return { ...updated, projectDir: resolved.projectDir, composePath: resolved.composePath };
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
const versionChanged = updated.content !== original;
|
|
81
|
+
if (versionChanged) fs.writeFileSync(resolved.composePath, updated.content, "utf8");
|
|
82
|
+
try {
|
|
76
83
|
runDocker([...baseArgs, "config"], { cwd: resolved.projectDir });
|
|
77
84
|
runDocker([...baseArgs, "build"], { cwd: resolved.projectDir });
|
|
78
85
|
if (push) runDocker([...baseArgs, "push"], { cwd: resolved.projectDir });
|
|
79
|
-
} catch (error) {
|
|
80
|
-
fs.writeFileSync(resolved.composePath, original, "utf8");
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (versionChanged) fs.writeFileSync(resolved.composePath, original, "utf8");
|
|
81
88
|
throw error;
|
|
82
89
|
}
|
|
83
90
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
bundle_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
4
|
+
source "$bundle_dir/config.sh"
|
|
5
|
+
: "${CCR_USERNAME:?未注入镜像仓库用户名}"
|
|
6
|
+
: "${CCR_PASSWORD:?未注入镜像仓库密码}"
|
|
7
|
+
: "${SSH_KNOWN_HOSTS:?未注入已核验的 SSH host keys}"
|
|
8
|
+
[[ $# -gt 0 ]] || { echo '需要组件=正式镜像参数' >&2; exit 2; }
|
|
9
|
+
local_tmp=$(mktemp -d)
|
|
10
|
+
remote_tmp=""
|
|
11
|
+
ssh_target="${ssh_user}@${server}"
|
|
12
|
+
ssh_options=(-o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile="$SSH_KNOWN_HOSTS")
|
|
13
|
+
cleanup() {
|
|
14
|
+
if [[ "$remote_tmp" =~ ^/tmp/tmp\.[a-zA-Z0-9]+$ ]]; then
|
|
15
|
+
ssh "${ssh_options[@]}" "$ssh_target" "rm -rf -- '$remote_tmp'" >/dev/null 2>&1 || true
|
|
16
|
+
fi
|
|
17
|
+
rm -rf -- "$local_tmp"
|
|
18
|
+
}
|
|
19
|
+
trap cleanup EXIT
|
|
20
|
+
remote_tmp=$(ssh "${ssh_options[@]}" "$ssh_target" 'mktemp -d')
|
|
21
|
+
[[ "$remote_tmp" =~ ^/tmp/tmp\.[a-zA-Z0-9]+$ ]] || { echo '服务器返回非法临时目录' >&2; exit 1; }
|
|
22
|
+
# 凭据只写入本次临时 Docker 配置,退出时清理,不进入项目配置或日志。
|
|
23
|
+
auth_value=$(printf '%s:%s' "$CCR_USERNAME" "$CCR_PASSWORD" | base64 | tr -d '\r\n')
|
|
24
|
+
umask 077
|
|
25
|
+
printf '{"auths":{"%s":{"auth":"%s"}}}\n' "$registry" "$auth_value" > "$local_tmp/config.json"
|
|
26
|
+
scp "${ssh_options[@]}" "$bundle_dir/release.sh" "$bundle_dir/health-check.sh" "$bundle_dir/config.sh" \
|
|
27
|
+
"$bundle_dir/docker-compose.release.yml" "$local_tmp/config.json" "$ssh_target:$remote_tmp/"
|
|
28
|
+
remote_command=(sudo env "DOCKER_CONFIG=$remote_tmp" bash "$remote_tmp/release.sh" "$@")
|
|
29
|
+
printf -v remote_quoted '%q ' "${remote_command[@]}"
|
|
30
|
+
ssh "${ssh_options[@]}" "$ssh_target" "$remote_quoted"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
bundle_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
4
|
+
source "$bundle_dir/config.sh"
|
|
5
|
+
cd "$project_dir"
|
|
6
|
+
compose=(docker compose --env-file "$secret_env_file" --env-file "$release_env_file" -f "$compose_file" -f "$bundle_dir/docker-compose.release.yml")
|
|
7
|
+
for service in "${health_services[@]}"; do
|
|
8
|
+
container_id=$("${compose[@]}" ps -q "$service")
|
|
9
|
+
[[ -n "$container_id" ]] || { echo "服务没有容器: $service" >&2; exit 1; }
|
|
10
|
+
[[ "$(docker inspect --format '{{.State.Running}}' "$container_id")" == true ]] || {
|
|
11
|
+
echo "服务未运行: $service" >&2
|
|
12
|
+
docker logs --tail 100 "$container_id" >&2 || true
|
|
13
|
+
exit 1
|
|
14
|
+
}
|
|
15
|
+
done
|
|
16
|
+
check_application
|
|
17
|
+
for health_url in "${health_urls[@]}"; do
|
|
18
|
+
checked=false
|
|
19
|
+
for ((attempt=1; attempt<=health_attempts; attempt++)); do
|
|
20
|
+
if curl --fail --silent --show-error --location --max-time "$health_timeout" --output /dev/null "$health_url"; then
|
|
21
|
+
checked=true
|
|
22
|
+
break
|
|
23
|
+
fi
|
|
24
|
+
sleep "$health_interval"
|
|
25
|
+
done
|
|
26
|
+
[[ "$checked" == true ]] || { echo "健康检查失败: $health_url" >&2; exit 1; }
|
|
27
|
+
echo "健康检查通过: $health_url"
|
|
28
|
+
done
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
echo "用法: $0 <组件名称> <qc镜像> <正式镜像> <完整40位commit>" >&2
|
|
6
|
+
exit 2
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
[[ $# -eq 4 ]] || usage
|
|
10
|
+
component=$1
|
|
11
|
+
source_image=$2
|
|
12
|
+
final_image=$3
|
|
13
|
+
expected_revision=$4
|
|
14
|
+
|
|
15
|
+
[[ "$expected_revision" =~ ^[0-9a-f]{40}$ ]] || {
|
|
16
|
+
echo "$component commit 必须是 40 位小写十六进制值" >&2
|
|
17
|
+
exit 2
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
manifest_error=$(mktemp)
|
|
21
|
+
cleanup() {
|
|
22
|
+
rm -f -- "$manifest_error"
|
|
23
|
+
}
|
|
24
|
+
trap cleanup EXIT
|
|
25
|
+
|
|
26
|
+
docker manifest inspect "$source_image" >/dev/null
|
|
27
|
+
docker pull "$source_image"
|
|
28
|
+
actual_revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$source_image")
|
|
29
|
+
if [[ "$actual_revision" != "$expected_revision" ]]; then
|
|
30
|
+
echo "$component qc 镜像完整 revision 校验失败: image=$source_image" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if docker manifest inspect "$final_image" >/dev/null 2>"$manifest_error"; then
|
|
35
|
+
docker pull "$final_image"
|
|
36
|
+
final_revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$final_image")
|
|
37
|
+
if [[ "$final_revision" != "$expected_revision" ]]; then
|
|
38
|
+
echo "$component 正式标签已存在但 revision 不一致,禁止覆盖: image=$final_image" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
echo "$component 正式镜像已存在且 revision 一致,跳过重复晋升: $final_image"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if ! grep -Eqi 'manifest unknown|no such manifest|manifest[^[:cntrl:]]*not found' "$manifest_error"; then
|
|
46
|
+
echo "$component 正式镜像查询失败,无法确认标签不存在,禁止晋升: $final_image" >&2
|
|
47
|
+
sed -n '1,5p' "$manifest_error" >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# 本地构建只写 qc 标签;Jenkins 人工确认后才允许创建不可变正式标签。
|
|
52
|
+
# 使用 pull/tag/push 避免部署节点依赖 Buildx,同时不在 Jenkins 执行 Dockerfile 构建。
|
|
53
|
+
docker tag "$source_image" "$final_image"
|
|
54
|
+
docker push "$final_image"
|
|
55
|
+
docker pull "$final_image"
|
|
56
|
+
final_revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$final_image")
|
|
57
|
+
if [[ "$final_revision" != "$expected_revision" ]]; then
|
|
58
|
+
echo "$component 晋升后的正式镜像 revision 校验失败: image=$final_image" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
echo "$component qc 镜像已晋升为正式 commit 标签: $final_image"
|