|
| 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