Skip to content

Commit 13fa37e

Browse files
authored
Add script for Maven Central publishing (#289)
* Add scripts for deploying artifacts * use native md5sum/sha1sum in Linux * update cases for azure pipelines * wrap passphrase to make terminal happy * add --pinentry-mode=loopback for gpg v2.1+ * update promote script * update * scripts: better naming * poll to see promote status * check args * unify script * address comments
1 parent 9a5d2fc commit 13fa37e

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed

scripts/publishMaven.js

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/**
2+
* Usage:
3+
* node publishMaven.js -task [upload|promote]
4+
*
5+
* upload: Upload artifacts to a nexus staging repo.
6+
* promote: Promote a repo to get it picked up by Maven Central.
7+
*/
8+
9+
const childProcess = require('child_process');
10+
const fs = require('fs');
11+
const path = require('path');
12+
13+
const artifactFolder = process.env.artifactFolder;
14+
const configs = {
15+
nexus_ossrhuser: process.env.NEXUS_OSSRHUSER,
16+
nexus_ossrhpass: process.env.NEXUS_OSSRHPASS,
17+
nexus_stagingProfileId: process.env.NEXUS_STAGINGPROFILEID,
18+
gpgpass: process.env.GPGPASS,
19+
stagingRepoId: process.env.NEXUS_STAGINGREPOID,
20+
groupId: "com.microsoft.java",
21+
projectName: "java-debug",
22+
releaseVersion: process.env.releaseVersion,
23+
moduleNames: [
24+
"java-debug-parent",
25+
"com.microsoft.java.debug.core",
26+
"com.microsoft.java.debug.plugin"
27+
]
28+
};
29+
30+
main(configs, artifactFolder);
31+
32+
function main() {
33+
const argv = process.argv;
34+
const task = argv[argv.indexOf("-task") + 1];
35+
if (task === "upload") {
36+
uploadToStaging(configs, artifactFolder);
37+
} else if (task === "promote") {
38+
promoteToCentral(configs);
39+
} else {
40+
console.error("Task not specified.");
41+
console.log("Usage: node script.js -task [upload|promote]");
42+
}
43+
}
44+
45+
/**
46+
* Task upload: Upload artifacts to a nexus staging repo.
47+
*
48+
* Required binaries:
49+
* - gpg
50+
* - curl
51+
*
52+
* Required Environment Variables:
53+
* - artifactFolder: folder containing *.jar/*.pom files.
54+
* - releaseVersion: version of artifacts.
55+
* - NEXUS_OSSRHUSER: username.
56+
* - NEXUS_OSSRHPASS: password.
57+
* - NEXUS_STAGINGPROFILEID: identifier of the repo to promote.
58+
* - GPGPASS: passphrase of GPG key.
59+
*/
60+
function uploadToStaging(configs, artifactFolder) {
61+
checkPrerequisite(configs);
62+
addChecksumsAndGpgSignature(configs, artifactFolder);
63+
createStagingRepo(configs);
64+
deployToStagingRepo(configs, artifactFolder);
65+
closeStagingRepo(configs);
66+
}
67+
68+
/**
69+
* Task promote: Promote a repo to get it picked up by Maven Central.
70+
*
71+
* Required binaries:
72+
* - curl
73+
*
74+
* Required Environment Variables:
75+
* - NEXUS_OSSRHUSER: username.
76+
* - NEXUS_OSSRHPASS: password.
77+
* - NEXUS_STAGINGPROFILEID: identifier of the repo to promote.
78+
* - NEXUS_STAGINGREPOID: id of staging repo with artifacts to promote.
79+
*/
80+
function promoteToCentral(configs) {
81+
let message = "";
82+
console.log("\n========Nexus: Promote=======");
83+
try {
84+
console.log(`Starting to promote staging repository ${configs.stagingRepoId} ...`);
85+
console.log(`curl -i -X POST -d "<promoteRequest><data><stagedRepositoryId>${configs.stagingRepoId}</stagedRepositoryId></data></promoteRequest>" -H "Content-Type: application/xml" -u **:** -k https://oss.sonatype.org/service/local/staging/profiles/${configs.nexus_stagingProfileId}/promote`);
86+
message = childProcess.execSync(`curl -i -X POST -d "<promoteRequest><data><stagedRepositoryId>${configs.stagingRepoId}</stagedRepositoryId></data></promoteRequest>" -H "Content-Type: application/xml" -u ${configs.nexus_ossrhuser}:${configs.nexus_ossrhpass} -k https://oss.sonatype.org/service/local/staging/profiles/${configs.nexus_stagingProfileId}/promote`);
87+
message = message.toString();
88+
console.log(message);
89+
} catch (ex) {
90+
console.error("\n\n[Failure] Promoting staging repository failed.");
91+
console.error(!message ? ex : message.toString());
92+
process.exit(1);
93+
}
94+
const success = isReleased(configs);
95+
console.log("Below is the public repository url, you could manually validate it.");
96+
console.log(`https://oss.sonatype.org/content/groups/public/${configs.groupId.replace(/\./g, "/")}`);
97+
console.log("\n\n");
98+
if (success) {
99+
console.log("\n\n[Success] Nexus: Promote succeeded.");
100+
} else {
101+
console.error("\n\n[Failure] Nexus: Promote failed.");
102+
process.exit(1)
103+
}
104+
}
105+
106+
function isReleased(configs) {
107+
let pollingCount = 0;
108+
const MAX_POLLINGS = 10;
109+
for (; pollingCount < MAX_POLLINGS; pollingCount++) {
110+
console.log(`\nPolling the release operation finished or not...`);
111+
console.log(`curl -X GET -H "Content-Type:application/xml" -u **:** -k https://oss.sonatype.org/service/local/staging/repository/${configs.stagingRepoId}`);
112+
message = childProcess.execSync(`curl -X GET -H "Content-Type:application/xml" -u ${configs.nexus_ossrhuser}:${configs.nexus_ossrhpass} -k https://oss.sonatype.org/service/local/staging/repository/${configs.stagingRepoId}`);
113+
const status = extractStatus(message.toString());
114+
console.log(status);
115+
if (status !== "closed") {
116+
return true;
117+
}
118+
// use system sleep command to pause the program.
119+
childProcess.execSync(`sleep 6s`);
120+
}
121+
return false;
122+
}
123+
124+
function checkPrerequisite(configs) {
125+
const props = ["releaseVersion", "artifactFolder", "NEXUS_OSSRHUSER", "NEXUS_OSSRHPASS", "NEXUS_STAGINGPROFILEID", "GPGPASS" ];
126+
for (const prop of props) {
127+
if (!configs[prop]) {
128+
console.error(`${prop} is not set.`);
129+
process.exit(1);
130+
}
131+
}
132+
}
133+
134+
function addChecksumsAndGpgSignature(configs, artifactFolder) {
135+
console.log("\n=======Checksum and gpg sign=======");
136+
console.log("Starting to calculate checksum and gpg sign...");
137+
for (let moduleName of configs.moduleNames) {
138+
const modulePath = path.join(artifactFolder, moduleName);
139+
// remove old md5/sha1/asc files.
140+
fs.readdirSync(modulePath)
141+
.filter(name => name.endsWith(".md5") || name.endsWith(".sha1") || name.endsWith(".asc"))
142+
.forEach(name => fs.unlinkSync(path.join(modulePath, name)));
143+
144+
const files = fs.readdirSync(modulePath);
145+
for (let file of files) {
146+
// calc md5.
147+
const md5 = childProcess.execSync(`md5sum "${path.join(modulePath, file)}"`);
148+
const md5Match = /([a-z0-9]{32})/.exec(md5.toString());
149+
fs.writeFileSync(path.join(modulePath, file + ".md5"), md5Match[0]);
150+
151+
// calc sha1.
152+
const sha1 = childProcess.execSync(`sha1sum "${path.join(modulePath, file)}"`);
153+
const sha1Match = /([a-z0-9]{40})/.exec(sha1.toString());
154+
fs.writeFileSync(path.join(modulePath, file + ".sha1"), sha1Match[0]);
155+
156+
// gpg sign.
157+
childProcess.execSync(`gpg --batch --pinentry-mode loopback --passphrase "${configs.gpgpass}" -ab "${path.join(modulePath, file)}"`)
158+
}
159+
}
160+
console.log("\n\n[Success] Checksum and gpg sign finished.");
161+
console.log("\n\n");
162+
}
163+
164+
function createStagingRepo(configs) {
165+
let message = "";
166+
console.log("\n=======Nexus: Create staging repo=======");
167+
console.log("Starting to create staging repository...");
168+
try {
169+
console.log(`curl -X POST -d "<promoteRequest><data><description>${configs.projectName}-${configs.releaseVersion}</description></data></promoteRequest>" -H "Content-Type: application/xml" -u **:** -k https://oss.sonatype.org/service/local/staging/profiles/${configs.nexus_stagingProfileId}/start`);
170+
message = childProcess.execSync(`curl -X POST -d "<promoteRequest><data><description>${configs.projectName}-${configs.releaseVersion}</description></data></promoteRequest>" -H "Content-Type: application/xml" -u ${configs.nexus_ossrhuser}:${configs.nexus_ossrhpass} -k https://oss.sonatype.org/service/local/staging/profiles/${configs.nexus_stagingProfileId}/start`);
171+
message = message.toString();
172+
const match = /<stagedRepositoryId>([a-zA-Z0-9-_]+)<\/stagedRepositoryId>/.exec(message);
173+
if (match != null && match.length > 1) {
174+
configs.stagingRepoId = match[1];
175+
} else {
176+
console.error("\n[Failure] Creating staging repository failed.");
177+
console.error(message);
178+
process.exit(1);
179+
}
180+
} catch (ex) {
181+
console.error("\n[Failure] Creating staging repository failed.");
182+
console.error(!message ? ex : message.toString());
183+
process.exit(1);
184+
}
185+
console.log("\n\n[Success] Nexus: Creating staging repository completion.");
186+
console.log("staging repository id: " + configs.stagingRepoId);
187+
console.log("\n\n");
188+
}
189+
190+
function deployToStagingRepo(configs, artifactFolder) {
191+
console.log("\n========Nexus: Deploy artifacts to staging repo=======");
192+
console.log("Starting to deploy artifacts to staging repository...");
193+
for (let moduleName of configs.moduleNames) {
194+
const modulePath = path.join(artifactFolder, moduleName);
195+
for (let file of fs.readdirSync(modulePath)) {
196+
const realPath = path.join(modulePath, file);
197+
const url = [
198+
"https://oss.sonatype.org/service/local/staging/deployByRepositoryId",
199+
configs.stagingRepoId,
200+
configs.groupId.replace(/\./g, "/"),
201+
moduleName,
202+
configs.releaseVersion,
203+
file
204+
];
205+
console.log(`curl --upload-file "${realPath}" -u **:** -k ${url.join("/")}`);
206+
message = childProcess.execSync(`curl --upload-file "${realPath}" -u ${configs.nexus_ossrhuser}:${configs.nexus_ossrhpass} -k ${url.join("/")}`);
207+
message = message.toString();
208+
console.log(message);
209+
console.log("Succeeded.\n");
210+
}
211+
}
212+
console.log("\n\n[Success] Nexus: Deploying completion.");
213+
console.log("\n\n");
214+
}
215+
216+
function closeStagingRepo(configs) {
217+
let message = "";
218+
let pollingCount = 0;
219+
const MAX_POLLINGS = 10;
220+
console.log("\n========Nexus: Verify and Close staging repo=======");
221+
try {
222+
console.log(`Starting to close staging repository ${configs.stagingRepoId} ...`);
223+
console.log(`curl -X POST -d "<promoteRequest><data><stagedRepositoryId>${configs.stagingRepoId}</stagedRepositoryId></data></promoteRequest>" -H "Content-Type: application/xml" -u **:** -k https://oss.sonatype.org/service/local/staging/profiles/${configs.nexus_stagingProfileId}/finish`);
224+
message = childProcess.execSync(`curl -X POST -d "<promoteRequest><data><stagedRepositoryId>${configs.stagingRepoId}</stagedRepositoryId></data></promoteRequest>" -H "Content-Type: application/xml" -u ${configs.nexus_ossrhuser}:${configs.nexus_ossrhpass} -k https://oss.sonatype.org/service/local/staging/profiles/${configs.nexus_stagingProfileId}/finish`);
225+
message = message.toString();
226+
227+
for (; pollingCount < MAX_POLLINGS; pollingCount++) {
228+
console.log(`\nPolling the close operation finished or not...`);
229+
console.log(`curl -X GET -H "Content-Type:application/xml" -u **:** -k https://oss.sonatype.org/service/local/staging/repository/${configs.stagingRepoId}`);
230+
message = childProcess.execSync(`curl -X GET -H "Content-Type:application/xml" -u ${configs.nexus_ossrhuser}:${configs.nexus_ossrhpass} -k https://oss.sonatype.org/service/local/staging/repository/${configs.stagingRepoId}`);
231+
// console.log(message.toString());
232+
if (extractStatus(message.toString()) === "closed") {
233+
break;
234+
}
235+
// use system sleep command to pause the program.
236+
childProcess.execSync(`sleep 6s`);
237+
}
238+
239+
if (pollingCount >= MAX_POLLINGS) {
240+
console.log("\nQuerying the close operation result...");
241+
message = childProcess.execSync(`curl -X GET -H "Content-Type:application/xml" -u ${configs.nexus_ossrhuser}:${configs.nexus_ossrhpass} -k https://oss.sonatype.org/service/local/staging/repository/${configs.stagingRepoId}/activity`);
242+
// console.log(message.toString());
243+
const errors = extractErrorMessage(message.toString());
244+
console.error(`\n\n[Failure] Closing staging repository failed.`);
245+
console.error(`See failure messages:`);
246+
console.error(errors.join("\n\n"));
247+
process.exit(1);
248+
}
249+
} catch (ex) {
250+
console.error("\n\n[Failure] Closing staging repository failed.");
251+
console.error(!message ? ex : message.toString());
252+
process.exit(1);
253+
}
254+
fs.writeFileSync(".stagingRepoId", configs.stagingRepoId);
255+
console.log("\n\n[Success] Nexus: Staging completion.");
256+
console.log("Below is the staging repository url, you could use it to test deployment.");
257+
console.log(`https://oss.sonatype.org/content/repositories/${configs.stagingRepoId}`);
258+
console.log("\n\n");
259+
}
260+
261+
function extractStatus(message) {
262+
const group = /<type>([a-zA-Z0-9-_\.]+)<\/type>/.exec(message);
263+
return group[1];
264+
}
265+
266+
function extractErrorMessage(message) {
267+
const errors = [];
268+
const group = message.match(/<name>failureMessage<\/name>[\r?\n ]+<value>(.*)<\/value>/g);
269+
for (let error of group) {
270+
errors.push(error.match(/<value>(.*)<\/value>/)[1])
271+
}
272+
return errors;
273+
}

0 commit comments

Comments
 (0)