diff --git a/README.md b/README.md index 0f1d0f20..6a7ac3c6 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ steps: | `server-id` | Maven repository ID used in generated `settings.xml`. | `github` | | `server-username-env-var` | Environment variable name for Maven repository username. | `GITHUB_ACTOR` | | `server-password-env-var` | Environment variable name for Maven repository password or token. | `GITHUB_TOKEN` | +| `mvn-server-credentials` | Multiline Maven server credentials in the format `server-id:USERNAME_ENV:PASSWORD_ENV`. Replaces the single server configured by the three inputs above when set. | | | `settings-path` | Directory where `settings.xml` is written. | `~/.m2` | | `overwrite-settings` | Overwrite an existing `settings.xml`. | `true` | | `gpg-private-key` | GPG private key to import into an isolated temporary keyring. | | diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index 7457eb60..da4c9450 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -26,7 +26,7 @@ jest.unstable_mockModule('@actions/core', () => ({ setOutput: jest.fn(), getInput: jest.fn(), getBooleanInput: jest.fn(), - getMultilineInput: jest.fn(), + getMultilineInput: jest.fn(() => []), addPath: jest.fn(), exportVariable: jest.fn(), saveState: jest.fn(), @@ -57,6 +57,9 @@ const {M2_DIR, MVN_SETTINGS_FILE, STATE_GPG_HOME} = const __dirname = path.dirname(fileURLToPath(import.meta.url)); const m2Dir = path.join(__dirname, M2_DIR); const settingsFile = path.join(m2Dir, MVN_SETTINGS_FILE); +const credentials = (id: string, username: string, password: string) => [ + {id, usernameEnvVar: username, passwordEnvVar: password} +]; describe('auth tests', () => { let spyOSHomedir: any; @@ -73,6 +76,9 @@ describe('auth tests', () => { afterEach(() => { (core.getInput as jest.Mock).mockReset(); + (core.getMultilineInput as jest.Mock).mockReset(); + (core.getMultilineInput as jest.Mock).mockReturnValue([]); + (core.warning as jest.Mock).mockReset(); (core.exportVariable as jest.Mock).mockReset(); (gpg.importKey as jest.Mock).mockReset(); (gpg.removeGpgHome as jest.Mock).mockReset(); @@ -100,9 +106,7 @@ describe('auth tests', () => { await io.rmRF(altHome); // ensure it doesn't already exist await auth.createAuthenticationSettings( - id, - username, - password, + credentials(id, username, password), altHome, true ); @@ -113,7 +117,7 @@ describe('auth tests', () => { expect(fs.existsSync(altHome)).toBe(true); expect(fs.existsSync(altSettingsFile)).toBe(true); expect(fs.readFileSync(altSettingsFile, 'utf-8')).toEqual( - auth.generate(id, username, password) + auth.generate(credentials(id, username, password)) ); await io.rmRF(altHome); @@ -125,9 +129,7 @@ describe('auth tests', () => { const password = 'TOKEN'; await auth.createAuthenticationSettings( - id, - username, - password, + credentials(id, username, password), m2Dir, true ); @@ -135,7 +137,7 @@ describe('auth tests', () => { expect(fs.existsSync(m2Dir)).toBe(true); expect(fs.existsSync(settingsFile)).toBe(true); expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( - auth.generate(id, username, password) + auth.generate(credentials(id, username, password)) ); }, 100000); @@ -146,9 +148,7 @@ describe('auth tests', () => { const gpgPassphrase = 'GPG'; await auth.createAuthenticationSettings( - id, - username, - password, + credentials(id, username, password), m2Dir, true, gpgPassphrase @@ -157,7 +157,7 @@ describe('auth tests', () => { expect(fs.existsSync(m2Dir)).toBe(true); expect(fs.existsSync(settingsFile)).toBe(true); expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( - auth.generate(id, username, password, gpgPassphrase) + auth.generate(credentials(id, username, password), gpgPassphrase) ); }, 100000); @@ -218,9 +218,7 @@ describe('auth tests', () => { expect(fs.existsSync(settingsFile)).toBe(true); await auth.createAuthenticationSettings( - id, - username, - password, + credentials(id, username, password), m2Dir, true ); @@ -228,7 +226,7 @@ describe('auth tests', () => { expect(fs.existsSync(m2Dir)).toBe(true); expect(fs.existsSync(settingsFile)).toBe(true); expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( - auth.generate(id, username, password) + auth.generate(credentials(id, username, password)) ); }, 100000); @@ -243,9 +241,7 @@ describe('auth tests', () => { expect(fs.existsSync(settingsFile)).toBe(true); await auth.createAuthenticationSettings( - id, - username, - password, + credentials(id, username, password), m2Dir, false ); @@ -273,7 +269,9 @@ describe('auth tests', () => { `; - expect(auth.generate(id, username, password)).toEqual(expectedSettings); + expect(auth.generate(credentials(id, username, password))).toEqual( + expectedSettings + ); }); it('generates valid settings.xml with additional configuration', () => { @@ -306,9 +304,9 @@ describe('auth tests', () => { `; - expect(auth.generate(id, username, password, gpgPassphrase)).toEqual( - expectedSettings - ); + expect( + auth.generate(credentials(id, username, password), gpgPassphrase) + ).toEqual(expectedSettings); }); it('does not add a gpg profile when the passphrase env var is the maven-gpg-plugin default', () => { @@ -330,9 +328,9 @@ describe('auth tests', () => { `; - expect(auth.generate(id, username, password, gpgPassphrase)).toEqual( - expectedSettings - ); + expect( + auth.generate(credentials(id, username, password), gpgPassphrase) + ).toEqual(expectedSettings); }); it('escapes settings.xml values while preserving parsed semantics', () => { @@ -341,7 +339,10 @@ describe('auth tests', () => { const password = `TOKEN&<>"'é`; const gpgPassphrase = `GPG&<>"'é`; - const xml = auth.generate(id, username, password, gpgPassphrase); + const xml = auth.generate( + credentials(id, username, password), + gpgPassphrase + ); const parsed = parseXmlObject(xml) as any; expect(parsed.settings.interactiveMode).toBe('false'); @@ -352,6 +353,144 @@ describe('auth tests', () => { expect(parsed.settings.activeProfiles.activeProfile).toBe('setup-java-gpg'); }); + it('generates ordered settings.xml entries for multiple servers', () => { + const servers = [ + { + id: 'releases', + usernameEnvVar: 'RELEASES_USERNAME', + passwordEnvVar: 'RELEASES_PASSWORD' + }, + { + id: 'snapshots', + usernameEnvVar: 'SNAPSHOTS_USERNAME', + passwordEnvVar: 'SNAPSHOTS_PASSWORD' + } + ]; + + const parsed = parseXmlObject(auth.generate(servers)) as any; + + expect(parsed.settings.servers.server).toEqual([ + { + id: 'releases', + username: '${env.RELEASES_USERNAME}', + password: '${env.RELEASES_PASSWORD}' + }, + { + id: 'snapshots', + username: '${env.SNAPSHOTS_USERNAME}', + password: '${env.SNAPSHOTS_PASSWORD}' + } + ]); + }); + + it('parses and trims multiline Maven server credentials', () => { + expect( + auth.parseMavenServerCredentials([ + '', + ' releases : RELEASES_USERNAME : RELEASES_PASSWORD ', + 'snapshots:SNAPSHOTS_USERNAME:SNAPSHOTS_PASSWORD' + ]) + ).toEqual([ + { + id: 'releases', + usernameEnvVar: 'RELEASES_USERNAME', + passwordEnvVar: 'RELEASES_PASSWORD' + }, + { + id: 'snapshots', + usernameEnvVar: 'SNAPSHOTS_USERNAME', + passwordEnvVar: 'SNAPSHOTS_PASSWORD' + } + ]); + }); + + it.each([ + { + entries: ['releases:RELEASES_USERNAME'], + error: + 'Invalid mvn-server-credentials entry at line 1. Expected format: server-id:USERNAME_ENV:PASSWORD_ENV' + }, + { + entries: ['', 'releases:RELEASES_USERNAME:RELEASES_PASSWORD:EXTRA'], + error: + 'Invalid mvn-server-credentials entry at line 2. Expected format: server-id:USERNAME_ENV:PASSWORD_ENV' + }, + { + entries: ['releases::RELEASES_PASSWORD'], + error: + 'Invalid mvn-server-credentials entry at line 1. server-id, username environment variable, and password environment variable are required' + } + ])( + 'rejects malformed Maven server credentials: $entries', + ({entries, error}) => { + expect(() => auth.parseMavenServerCredentials(entries)).toThrow(error); + } + ); + + it('rejects duplicate Maven server ids', () => { + expect(() => + auth.parseMavenServerCredentials([ + 'releases:RELEASES_USERNAME:RELEASES_PASSWORD', + 'releases:OTHER_USERNAME:OTHER_PASSWORD' + ]) + ).toThrow("Duplicate server-id 'releases' in mvn-server-credentials input"); + }); + + it('uses multiline Maven server credentials instead of single-server inputs', () => { + (core.getMultilineInput as jest.Mock).mockReturnValue([ + 'releases:RELEASES_USERNAME:RELEASES_PASSWORD', + 'snapshots:SNAPSHOTS_USERNAME:SNAPSHOTS_PASSWORD' + ]); + + expect(auth.getMavenServerSettings()).toEqual([ + { + id: 'releases', + usernameEnvVar: 'RELEASES_USERNAME', + passwordEnvVar: 'RELEASES_PASSWORD' + }, + { + id: 'snapshots', + usernameEnvVar: 'SNAPSHOTS_USERNAME', + passwordEnvVar: 'SNAPSHOTS_PASSWORD' + } + ]); + expect(core.getInput).not.toHaveBeenCalled(); + }); + + it('uses the existing single-server inputs when multiline credentials are absent', () => { + (core.getInput as jest.Mock).mockImplementation((name: string) => + name === 'server-id' ? 'github' : '' + ); + + expect(auth.getMavenServerSettings()).toEqual([ + { + id: 'github', + usernameEnvVar: 'GITHUB_ACTOR', + passwordEnvVar: 'GITHUB_TOKEN' + } + ]); + }); + + it('preserves deprecated single-server aliases as fallback inputs', () => { + (core.getInput as jest.Mock).mockImplementation((name: string) => { + const inputs: Record = { + 'server-id': 'legacy', + 'server-username': 'LEGACY_USERNAME', + 'server-password': 'LEGACY_PASSWORD' + }; + return inputs[name] ?? ''; + }); + + expect(auth.getMavenServerSettings()).toEqual([ + { + id: 'legacy', + usernameEnvVar: 'LEGACY_USERNAME', + passwordEnvVar: 'LEGACY_PASSWORD' + } + ]); + expect(core.warning).toHaveBeenCalledTimes(2); + }); + function xmlElementText(xml: string, tagName: string): string { const match = new RegExp(`<${tagName}>([\\s\\S]*?)`).exec(xml); expect(match).not.toBeNull(); diff --git a/action.yml b/action.yml index 60c1a00e..e5f9cb89 100644 --- a/action.yml +++ b/action.yml @@ -64,6 +64,9 @@ inputs: server-password: description: 'Deprecated alias for server-password-env-var' required: false + mvn-server-credentials: + description: 'Multiline list of Maven server credentials in the format `server-id:USERNAME_ENV:PASSWORD_ENV`. When set, replaces the single server configured by server-id, server-username-env-var, and server-password-env-var.' + required: false settings-path: description: 'Path to where the settings.xml file will be written. Default is ~/.m2.' required: false diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 17a6e553..7d7de461 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -30774,7 +30774,7 @@ module.exports = { /* harmony export */ gk: () => (/* binding */ INPUT_CACHE), /* harmony export */ wG: () => (/* binding */ INPUT_JOB_STATUS) /* harmony export */ }); -/* unused harmony exports MACOS_JAVA_CONTENT_POSTFIX, INPUT_JAVA_VERSION, INPUT_JAVA_VERSION_FILE, INPUT_ARCHITECTURE, INPUT_JAVA_PACKAGE, INPUT_DISTRIBUTION, INPUT_JDK_FILE, INPUT_JDK_FILE_DEPRECATED, INPUT_CHECK_LATEST, INPUT_FORCE_DOWNLOAD, INPUT_SET_DEFAULT, INPUT_PROBLEM_MATCHER, INPUT_VERIFY_SIGNATURE, INPUT_VERIFY_SIGNATURE_PUBLIC_KEY, INPUT_SERVER_ID, INPUT_SERVER_USERNAME_ENV_VAR, INPUT_SERVER_PASSWORD_ENV_VAR, INPUT_SERVER_USERNAME_DEPRECATED, INPUT_SERVER_PASSWORD_DEPRECATED, INPUT_SETTINGS_PATH, INPUT_OVERWRITE_SETTINGS, INPUT_GPG_PRIVATE_KEY, INPUT_GPG_PASSPHRASE_ENV_VAR, INPUT_GPG_PASSPHRASE_DEPRECATED, INPUT_DEFAULT_SERVER_USERNAME, INPUT_DEFAULT_SERVER_PASSWORD, INPUT_DEFAULT_GPG_PRIVATE_KEY, INPUT_DEFAULT_GPG_PASSPHRASE, MAVEN_GPG_PASSPHRASE_DEFAULT_ENV, GPG_PASSPHRASE_PROFILE_ID, INPUT_CACHE_DEPENDENCY_PATH, INPUT_CACHE_PATH, M2_DIR, MVN_SETTINGS_FILE, MVN_TOOLCHAINS_FILE, INPUT_MVN_TOOLCHAIN_ID, INPUT_MVN_TOOLCHAIN_VENDOR, INPUT_SHOW_DOWNLOAD_PROGRESS, MAVEN_ARGS_ENV, MAVEN_NO_TRANSFER_PROGRESS_FLAG, MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG, DISTRIBUTIONS_ONLY_MAJOR_VERSION */ +/* unused harmony exports MACOS_JAVA_CONTENT_POSTFIX, INPUT_JAVA_VERSION, INPUT_JAVA_VERSION_FILE, INPUT_ARCHITECTURE, INPUT_JAVA_PACKAGE, INPUT_DISTRIBUTION, INPUT_JDK_FILE, INPUT_JDK_FILE_DEPRECATED, INPUT_CHECK_LATEST, INPUT_FORCE_DOWNLOAD, INPUT_SET_DEFAULT, INPUT_PROBLEM_MATCHER, INPUT_VERIFY_SIGNATURE, INPUT_VERIFY_SIGNATURE_PUBLIC_KEY, INPUT_MVN_SERVER_CREDENTIALS, INPUT_SERVER_ID, INPUT_SERVER_USERNAME_ENV_VAR, INPUT_SERVER_PASSWORD_ENV_VAR, INPUT_SERVER_USERNAME_DEPRECATED, INPUT_SERVER_PASSWORD_DEPRECATED, INPUT_SETTINGS_PATH, INPUT_OVERWRITE_SETTINGS, INPUT_GPG_PRIVATE_KEY, INPUT_GPG_PASSPHRASE_ENV_VAR, INPUT_GPG_PASSPHRASE_DEPRECATED, INPUT_DEFAULT_SERVER_USERNAME, INPUT_DEFAULT_SERVER_PASSWORD, INPUT_DEFAULT_GPG_PRIVATE_KEY, INPUT_DEFAULT_GPG_PASSPHRASE, MAVEN_GPG_PASSPHRASE_DEFAULT_ENV, GPG_PASSPHRASE_PROFILE_ID, INPUT_CACHE_DEPENDENCY_PATH, INPUT_CACHE_PATH, M2_DIR, MVN_SETTINGS_FILE, MVN_TOOLCHAINS_FILE, INPUT_MVN_TOOLCHAIN_ID, INPUT_MVN_TOOLCHAIN_VENDOR, INPUT_SHOW_DOWNLOAD_PROGRESS, MAVEN_ARGS_ENV, MAVEN_NO_TRANSFER_PROGRESS_FLAG, MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG, DISTRIBUTIONS_ONLY_MAJOR_VERSION */ const MACOS_JAVA_CONTENT_POSTFIX = 'Contents/Home'; const INPUT_JAVA_VERSION = 'java-version'; const INPUT_JAVA_VERSION_FILE = 'java-version-file'; @@ -30789,6 +30789,7 @@ const INPUT_SET_DEFAULT = 'set-default'; const INPUT_PROBLEM_MATCHER = 'problem-matcher'; const INPUT_VERIFY_SIGNATURE = 'verify-signature'; const INPUT_VERIFY_SIGNATURE_PUBLIC_KEY = 'verify-signature-public-key'; +const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials'; const INPUT_SERVER_ID = 'server-id'; const INPUT_SERVER_USERNAME_ENV_VAR = 'server-username-env-var'; const INPUT_SERVER_PASSWORD_ENV_VAR = 'server-password-env-var'; diff --git a/dist/setup/81.index.js b/dist/setup/81.index.js index 4d98b9c2..a4e79cb5 100644 --- a/dist/setup/81.index.js +++ b/dist/setup/81.index.js @@ -10,7 +10,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ configureAuthentication: () => (/* binding */ configureAuthentication), /* harmony export */ createAuthenticationSettings: () => (/* binding */ createAuthenticationSettings), /* harmony export */ generate: () => (/* binding */ generate), -/* harmony export */ getInputWithDeprecatedAlias: () => (/* binding */ getInputWithDeprecatedAlias) +/* harmony export */ getInputWithDeprecatedAlias: () => (/* binding */ getInputWithDeprecatedAlias), +/* harmony export */ getMavenServerSettings: () => (/* binding */ getMavenServerSettings), +/* harmony export */ parseMavenServerCredentials: () => (/* binding */ parseMavenServerCredentials) /* harmony export */ }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6928); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); @@ -34,9 +36,7 @@ __webpack_require__.r(__webpack_exports__); async function configureAuthentication() { - const id = _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getInput */ .V4(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_ID */ .fd); - const usernameEnvVar = getInputWithDeprecatedAlias(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_USERNAME_ENV_VAR */ .sc, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_USERNAME_DEPRECATED */ .sp, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_DEFAULT_SERVER_USERNAME */ .Wj); - const passwordEnvVar = getInputWithDeprecatedAlias(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_PASSWORD_ENV_VAR */ .r4, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_PASSWORD_DEPRECATED */ .Vt, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_DEFAULT_SERVER_PASSWORD */ .xp); + const servers = getMavenServerSettings(); const settingsDirectory = _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getInput */ .V4(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SETTINGS_PATH */ .Xh) || path__WEBPACK_IMPORTED_MODULE_0__.join(os__WEBPACK_IMPORTED_MODULE_4__.homedir(), _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .M2_DIR */ .iT); const overwriteSettings = (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getBooleanInput */ .Vt)(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_OVERWRITE_SETTINGS */ .TS, true); @@ -46,7 +46,7 @@ async function configureAuthentication() { if (gpgPrivateKey) { _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .setSecret */ .Pq(gpgPrivateKey); } - await createAuthenticationSettings(id, usernameEnvVar, passwordEnvVar, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar); + await createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar); if (gpgPrivateKey) { _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq('Importing private gpg key'); const gpgHome = await _gpg_js__WEBPACK_IMPORTED_MODULE_5__/* .importKey */ .Fh(gpgPrivateKey); @@ -68,15 +68,53 @@ function getInputWithDeprecatedAlias(inputName, deprecatedInputName, defaultValu } return value || deprecatedValue || defaultValue || ''; } -async function createAuthenticationSettings(id, usernameEnvVar, passwordEnvVar, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar = undefined) { - _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq(`Creating ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MVN_SETTINGS_FILE */ .vO} with server-id: ${id}`); +// only exported for testing purposes +function getMavenServerSettings() { + const entries = _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getMultilineInput */ .q3(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_SERVER_CREDENTIALS */ .MM); + if (entries.some(entry => entry.trim())) { + return parseMavenServerCredentials(entries); + } + return [ + { + id: _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getInput */ .V4(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_ID */ .fd), + usernameEnvVar: getInputWithDeprecatedAlias(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_USERNAME_ENV_VAR */ .sc, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_USERNAME_DEPRECATED */ .sp, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_DEFAULT_SERVER_USERNAME */ .Wj), + passwordEnvVar: getInputWithDeprecatedAlias(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_PASSWORD_ENV_VAR */ .r4, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SERVER_PASSWORD_DEPRECATED */ .Vt, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_DEFAULT_SERVER_PASSWORD */ .xp) + } + ]; +} +// only exported for testing purposes +function parseMavenServerCredentials(entries) { + const servers = []; + const serverIds = new Set(); + entries.forEach((entry, index) => { + if (!entry.trim()) { + return; + } + const fields = entry.split(':'); + if (fields.length !== 3) { + throw new Error(`Invalid mvn-server-credentials entry at line ${index + 1}. Expected format: server-id:USERNAME_ENV:PASSWORD_ENV`); + } + const [id, usernameEnvVar, passwordEnvVar] = fields.map(field => field.trim()); + if (!id || !usernameEnvVar || !passwordEnvVar) { + throw new Error(`Invalid mvn-server-credentials entry at line ${index + 1}. server-id, username environment variable, and password environment variable are required`); + } + if (serverIds.has(id)) { + throw new Error(`Duplicate server-id '${id}' in mvn-server-credentials input`); + } + serverIds.add(id); + servers.push({ id, usernameEnvVar, passwordEnvVar }); + }); + return servers; +} +async function createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar = undefined) { + _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq(`Creating ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MVN_SETTINGS_FILE */ .vO} with server-id: ${servers.map(server => server.id).join(', ')}`); // when an alternate m2 location is specified use only that location (no .m2 directory) // otherwise use the home/.m2/ path await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .mkdirP */ .U$(settingsDirectory); - await write(settingsDirectory, generate(id, usernameEnvVar, passwordEnvVar, gpgPassphraseEnvVar), overwriteSettings); + await write(settingsDirectory, generate(servers, gpgPassphraseEnvVar), overwriteSettings); } // only exported for testing purposes -function generate(id, usernameEnvVar, passwordEnvVar, gpgPassphraseEnvVar) { +function generate(servers, gpgPassphraseEnvVar) { // The maven-gpg-plugin reads the passphrase from the environment variable // named by the `gpg.passphraseEnvName` property (default MAVEN_GPG_PASSPHRASE). // Only configure it when the requested env var name differs from that default; @@ -90,14 +128,12 @@ function generate(id, usernameEnvVar, passwordEnvVar, gpgPassphraseEnvVar) { ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"', ' xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">', ' false', - ' ', - ' ', - ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(id)}`, - ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(`\${env.${usernameEnvVar}}`)}`, - ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(`\${env.${passwordEnvVar}}`)}`, - ' ', - ' ' + ' ' ]; + for (const server of servers) { + lines.push(' ', ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(server.id)}`, ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(`\${env.${server.usernameEnvVar}}`)}`, ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(`\${env.${server.passwordEnvVar}}`)}`, ' '); + } + lines.push(' '); if (includeGpgPassphraseProfile) { lines.push(' ', ' ', ` ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .GPG_PASSPHRASE_PROFILE_ID */ .K$}`, ' ', ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(gpgPassphraseEnvVar)}`, ' ', ' ', ' ', ' ', ` ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .GPG_PASSPHRASE_PROFILE_ID */ .K$}`, ' '); } diff --git a/dist/setup/index.js b/dist/setup/index.js index 1d557e4a..97b679e6 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -30775,6 +30775,7 @@ module.exports = { /* harmony export */ I9: () => (/* binding */ INPUT_FORCE_DOWNLOAD), /* harmony export */ K$: () => (/* binding */ GPG_PASSPHRASE_PROFILE_ID), /* harmony export */ LS: () => (/* binding */ INPUT_ARCHITECTURE), +/* harmony export */ MM: () => (/* binding */ INPUT_MVN_SERVER_CREDENTIALS), /* harmony export */ OD: () => (/* binding */ INPUT_DEFAULT_GPG_PRIVATE_KEY), /* harmony export */ PG: () => (/* binding */ MACOS_JAVA_CONTENT_POSTFIX), /* harmony export */ QM: () => (/* binding */ INPUT_JAVA_VERSION), @@ -30829,6 +30830,7 @@ const INPUT_SET_DEFAULT = 'set-default'; const INPUT_PROBLEM_MATCHER = 'problem-matcher'; const INPUT_VERIFY_SIGNATURE = 'verify-signature'; const INPUT_VERIFY_SIGNATURE_PUBLIC_KEY = 'verify-signature-public-key'; +const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials'; const INPUT_SERVER_ID = 'server-id'; const INPUT_SERVER_USERNAME_ENV_VAR = 'server-username-env-var'; const INPUT_SERVER_PASSWORD_ENV_VAR = 'server-password-env-var'; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 4a6ec897..c193c234 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -27,6 +27,7 @@ - [Testing against different Java distributions](#Testing-against-different-Java-distributions) - [Testing against different platforms](#Testing-against-different-platforms) - [Publishing using Apache Maven](#Publishing-using-Apache-Maven) +- [Publishing to multiple Maven servers](#publishing-to-multiple-maven-servers) - [Apache Maven with a settings path](#apache-maven-with-a-settings-path) - [Maven transfer progress (download logs)](#Maven-transfer-progress-download-logs) - [Java problem matcher (compiler annotations)](#java-problem-matcher-compiler-annotations) @@ -863,6 +864,49 @@ The two `settings.xml` files created from the above example look like the follow If you don't want to overwrite the `settings.xml` file, you can set `overwrite-settings: false` +### Publishing to multiple Maven servers + +Use `mvn-server-credentials` to add more than one credential entry to the generated `settings.xml`. Each line has the format `server-id:USERNAME_ENV:PASSWORD_ENV`. The username and password fields are environment variable names, not credential values. + +When this input is set, it replaces the single server configured by `server-id`, `server-username-env-var`, and `server-password-env-var`. + +```yaml +steps: + - uses: actions/checkout@v7 + - name: Set up release and snapshot repositories + uses: actions/setup-java@v6 + with: + distribution: 'temurin' + java-version: '21' + mvn-server-credentials: | + releases:RELEASES_USERNAME:RELEASES_PASSWORD + snapshots:SNAPSHOTS_USERNAME:SNAPSHOTS_PASSWORD + - name: Publish with Maven + run: mvn deploy + env: + RELEASES_USERNAME: ${{ secrets.RELEASES_USERNAME }} + RELEASES_PASSWORD: ${{ secrets.RELEASES_PASSWORD }} + SNAPSHOTS_USERNAME: ${{ secrets.SNAPSHOTS_USERNAME }} + SNAPSHOTS_PASSWORD: ${{ secrets.SNAPSHOTS_PASSWORD }} +``` + +This configuration produces the following server entries: + +```xml + + + releases + ${env.RELEASES_USERNAME} + ${env.RELEASES_PASSWORD} + + + snapshots + ${env.SNAPSHOTS_USERNAME} + ${env.SNAPSHOTS_PASSWORD} + + +``` + ### GPG The example above uses the [Maven GPG Plugin](https://maven.apache.org/plugins/maven-gpg-plugin/)'s Bouncy Castle signer (`-Dgpg.signer=bc`, available since `maven-gpg-plugin` 3.2.0). It is a pure-Java signer that reads the key directly from the `MAVEN_GPG_KEY` environment variable, so it does **not** require the `gpg` executable, importing the key into a GPG keychain, or the `--pinentry-mode loopback` workaround in your `pom.xml`. The key must be an ASCII-armored secret key (transferable secret key format). diff --git a/src/auth.ts b/src/auth.ts index 49bf9ae3..adb66782 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -9,18 +9,14 @@ import * as gpg from './gpg.js'; import {getBooleanInput} from './util.js'; import {escapeXmlText} from './xml.js'; +export interface MavenServerCredentials { + id: string; + usernameEnvVar: string; + passwordEnvVar: string; +} + export async function configureAuthentication() { - const id = core.getInput(constants.INPUT_SERVER_ID); - const usernameEnvVar = getInputWithDeprecatedAlias( - constants.INPUT_SERVER_USERNAME_ENV_VAR, - constants.INPUT_SERVER_USERNAME_DEPRECATED, - constants.INPUT_DEFAULT_SERVER_USERNAME - ); - const passwordEnvVar = getInputWithDeprecatedAlias( - constants.INPUT_SERVER_PASSWORD_ENV_VAR, - constants.INPUT_SERVER_PASSWORD_DEPRECATED, - constants.INPUT_DEFAULT_SERVER_PASSWORD - ); + const servers = getMavenServerSettings(); const settingsDirectory = core.getInput(constants.INPUT_SETTINGS_PATH) || path.join(os.homedir(), constants.M2_DIR); @@ -42,9 +38,7 @@ export async function configureAuthentication() { } await createAuthenticationSettings( - id, - usernameEnvVar, - passwordEnvVar, + servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar @@ -80,30 +74,95 @@ export function getInputWithDeprecatedAlias( return value || deprecatedValue || defaultValue || ''; } +// only exported for testing purposes +export function getMavenServerSettings(): MavenServerCredentials[] { + const entries = core.getMultilineInput( + constants.INPUT_MVN_SERVER_CREDENTIALS + ); + + if (entries.some(entry => entry.trim())) { + return parseMavenServerCredentials(entries); + } + + return [ + { + id: core.getInput(constants.INPUT_SERVER_ID), + usernameEnvVar: getInputWithDeprecatedAlias( + constants.INPUT_SERVER_USERNAME_ENV_VAR, + constants.INPUT_SERVER_USERNAME_DEPRECATED, + constants.INPUT_DEFAULT_SERVER_USERNAME + ), + passwordEnvVar: getInputWithDeprecatedAlias( + constants.INPUT_SERVER_PASSWORD_ENV_VAR, + constants.INPUT_SERVER_PASSWORD_DEPRECATED, + constants.INPUT_DEFAULT_SERVER_PASSWORD + ) + } + ]; +} + +// only exported for testing purposes +export function parseMavenServerCredentials( + entries: string[] +): MavenServerCredentials[] { + const servers: MavenServerCredentials[] = []; + const serverIds = new Set(); + + entries.forEach((entry, index) => { + if (!entry.trim()) { + return; + } + + const fields = entry.split(':'); + if (fields.length !== 3) { + throw new Error( + `Invalid mvn-server-credentials entry at line ${index + 1}. Expected format: server-id:USERNAME_ENV:PASSWORD_ENV` + ); + } + + const [id, usernameEnvVar, passwordEnvVar] = fields.map(field => + field.trim() + ); + if (!id || !usernameEnvVar || !passwordEnvVar) { + throw new Error( + `Invalid mvn-server-credentials entry at line ${index + 1}. server-id, username environment variable, and password environment variable are required` + ); + } + if (serverIds.has(id)) { + throw new Error( + `Duplicate server-id '${id}' in mvn-server-credentials input` + ); + } + + serverIds.add(id); + servers.push({id, usernameEnvVar, passwordEnvVar}); + }); + + return servers; +} + export async function createAuthenticationSettings( - id: string, - usernameEnvVar: string, - passwordEnvVar: string, + servers: MavenServerCredentials[], settingsDirectory: string, overwriteSettings: boolean, gpgPassphraseEnvVar: string | undefined = undefined ) { - core.info(`Creating ${constants.MVN_SETTINGS_FILE} with server-id: ${id}`); + core.info( + `Creating ${constants.MVN_SETTINGS_FILE} with server-id: ${servers.map(server => server.id).join(', ')}` + ); // when an alternate m2 location is specified use only that location (no .m2 directory) // otherwise use the home/.m2/ path await io.mkdirP(settingsDirectory); await write( settingsDirectory, - generate(id, usernameEnvVar, passwordEnvVar, gpgPassphraseEnvVar), + generate(servers, gpgPassphraseEnvVar), overwriteSettings ); } // only exported for testing purposes export function generate( - id: string, - usernameEnvVar: string, - passwordEnvVar: string, + servers: MavenServerCredentials[], gpgPassphraseEnvVar?: string | undefined ) { // The maven-gpg-plugin reads the passphrase from the environment variable @@ -121,15 +180,20 @@ export function generate( ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"', ' xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">', ' false', - ' ', - ' ', - ` ${escapeXmlText(id)}`, - ` ${escapeXmlText(`\${env.${usernameEnvVar}}`)}`, - ` ${escapeXmlText(`\${env.${passwordEnvVar}}`)}`, - ' ', - ' ' + ' ' ]; + for (const server of servers) { + lines.push( + ' ', + ` ${escapeXmlText(server.id)}`, + ` ${escapeXmlText(`\${env.${server.usernameEnvVar}}`)}`, + ` ${escapeXmlText(`\${env.${server.passwordEnvVar}}`)}`, + ' ' + ); + } + lines.push(' '); + if (includeGpgPassphraseProfile) { lines.push( ' ', diff --git a/src/constants.ts b/src/constants.ts index 83a2aaf0..7a13679a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,7 @@ export const INPUT_SET_DEFAULT = 'set-default'; export const INPUT_PROBLEM_MATCHER = 'problem-matcher'; export const INPUT_VERIFY_SIGNATURE = 'verify-signature'; export const INPUT_VERIFY_SIGNATURE_PUBLIC_KEY = 'verify-signature-public-key'; +export const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials'; export const INPUT_SERVER_ID = 'server-id'; export const INPUT_SERVER_USERNAME_ENV_VAR = 'server-username-env-var'; export const INPUT_SERVER_PASSWORD_ENV_VAR = 'server-password-env-var';