From 5f75b27283990add95cd9c4ceaca74d789324bf7 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 17 Aug 2026 18:18:45 -0400 Subject: [PATCH] Add Maven dependency-resolution repositories (#1240) * Add Maven dependency repositories Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8897ad63-2d05-4a6d-8ccd-c1155348b59e * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Copilot-Session: 8897ad63-2d05-4a6d-8ccd-c1155348b59e --- README.md | 8 ++ __tests__/auth.test.ts | 312 +++++++++++++++++++++++++++++++++++++++++ action.yml | 11 ++ dist/cleanup/index.js | 8 +- dist/setup/81.index.js | 108 +++++++++++++- dist/setup/index.js | 12 ++ docs/advanced-usage.md | 76 ++++++++++ src/auth.ts | 189 ++++++++++++++++++++++--- src/constants.ts | 9 ++ 9 files changed, 708 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 6a7ac3c6..e2645633 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,9 @@ steps: | `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. | | +| `mvn-repositories` | Multiline Maven dependency repositories in the format `repository-id:repository-url:snapshots-enabled`. | | +| `mvn-repositories-include-central` | Include Maven Central in the generated dependency repositories profile. When `false`, Central is disabled unless an explicit `central` repository is declared. | `true` | +| `mvn-repositories-prioritize-central` | Place Maven Central before custom dependency repositories. Has no effect when Maven Central is excluded. | `true` | | `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. | | @@ -453,6 +456,11 @@ steps: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` +For dependencies hosted outside Maven Central, use `mvn-repositories` to add +resolution repositories to an active profile in the generated `settings.xml`. +Repository IDs can match `mvn-server-credentials` IDs when authentication is +required. See [Resolving Maven dependencies from custom repositories](docs/advanced-usage.md#resolving-maven-dependencies-from-custom-repositories). + ### GPG signing ```yaml diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index da4c9450..a169451f 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -60,6 +60,15 @@ const settingsFile = path.join(m2Dir, MVN_SETTINGS_FILE); const credentials = (id: string, username: string, password: string) => [ {id, usernameEnvVar: username, passwordEnvVar: password} ]; +const repositorySettings = ( + repositories: auth.MavenRepository[], + includeCentral = true, + prioritizeCentral = true +): auth.MavenRepositorySettings => ({ + repositories, + includeCentral, + prioritizeCentral +}); describe('auth tests', () => { let spyOSHomedir: any; @@ -383,6 +392,179 @@ describe('auth tests', () => { ]); }); + it('generates an active profile with Central before custom repositories by default', () => { + const parsed = parseXmlObject( + auth.generate( + credentials('packages', 'USERNAME', 'PASSWORD'), + undefined, + repositorySettings([ + { + id: 'private', + url: 'https://repo.example.com/maven', + snapshotsEnabled: true + } + ]) + ) + ) as any; + + expect(parsed.settings.profiles.profile).toEqual({ + id: 'setup-java-repositories', + repositories: { + repository: [ + { + id: 'central', + url: 'https://repo.maven.apache.org/maven2', + snapshots: {enabled: 'false'} + }, + { + id: 'private', + url: 'https://repo.example.com/maven', + snapshots: {enabled: 'true'} + } + ] + } + }); + expect(parsed.settings.activeProfiles.activeProfile).toBe( + 'setup-java-repositories' + ); + }); + + it('places custom repositories before Central when configured', () => { + const parsed = parseXmlObject( + auth.generate( + credentials('packages', 'USERNAME', 'PASSWORD'), + undefined, + repositorySettings( + [ + { + id: 'first', + url: 'https://first.example.com', + snapshotsEnabled: false + }, + { + id: 'second', + url: 'https://second.example.com', + snapshotsEnabled: true + } + ], + true, + false + ) + ) + ) as any; + + expect( + parsed.settings.profiles.profile.repositories.repository.map( + (repository: {id: string}) => repository.id + ) + ).toEqual(['first', 'second', 'central']); + }); + + it('excludes Central from the generated repository profile when configured', () => { + const parsed = parseXmlObject( + auth.generate( + credentials('packages', 'USERNAME', 'PASSWORD'), + undefined, + repositorySettings( + [ + { + id: 'private', + url: 'https://repo.example.com', + snapshotsEnabled: false + } + ], + false + ) + ) + ) as any; + + expect(parsed.settings.profiles.profile.repositories.repository).toEqual([ + { + id: 'private', + url: 'https://repo.example.com', + snapshots: {enabled: 'false'} + }, + { + id: 'central', + url: 'https://repo.maven.apache.org/maven2', + releases: {enabled: 'false'}, + snapshots: {enabled: 'false'} + } + ]); + }); + + it('combines repository and GPG configuration in shared profile blocks', () => { + const parsed = parseXmlObject( + auth.generate( + credentials('packages', 'USERNAME', 'PASSWORD'), + 'GPG_PASSPHRASE', + repositorySettings( + [ + { + id: 'private', + url: 'https://repo.example.com', + snapshotsEnabled: false + } + ], + false + ) + ) + ) as any; + + expect(parsed.settings.profiles.profile).toEqual([ + { + id: 'setup-java-repositories', + repositories: { + repository: [ + { + id: 'private', + url: 'https://repo.example.com', + snapshots: {enabled: 'false'} + }, + { + id: 'central', + url: 'https://repo.maven.apache.org/maven2', + releases: {enabled: 'false'}, + snapshots: {enabled: 'false'} + } + ] + } + }, + { + id: 'setup-java-gpg', + properties: {['gpg.passphraseEnvName']: 'GPG_PASSPHRASE'} + } + ]); + expect(parsed.settings.activeProfiles.activeProfile).toEqual([ + 'setup-java-repositories', + 'setup-java-gpg' + ]); + }); + + it('escapes repository values while preserving parsed semantics', () => { + const repository = { + id: `private&<>"'é`, + url: `https://repo.example.com/a?x=1&y="'é`, + snapshotsEnabled: true + }; + const xml = auth.generate( + credentials('packages', 'USERNAME', 'PASSWORD'), + undefined, + repositorySettings([repository], false) + ); + const parsed = parseXmlObject(xml) as any; + + expect( + parsed.settings.profiles.profile.repositories.repository.find( + (entry: {id: string}) => entry.id === repository.id + ) + ).toEqual({ + id: repository.id, + url: repository.url, + snapshots: {enabled: 'true'} + }); + }); + it('parses and trims multiline Maven server credentials', () => { expect( auth.parseMavenServerCredentials([ @@ -404,6 +586,136 @@ describe('auth tests', () => { ]); }); + it('parses Maven repositories while preserving colons in URLs', () => { + expect( + auth.parseMavenRepositories( + [ + '', + ' private : https://repo.example.com:8443/maven : true ', + 'releases:https://repo.example.com/releases:false' + ], + true + ) + ).toEqual([ + { + id: 'private', + url: 'https://repo.example.com:8443/maven', + snapshotsEnabled: true + }, + { + id: 'releases', + url: 'https://repo.example.com/releases', + snapshotsEnabled: false + } + ]); + }); + + it.each([ + { + entries: ['private'], + error: + 'Invalid mvn-repositories entry at line 1. Expected format: repository-id:repository-url:snapshots-enabled' + }, + { + entries: ['private:https://repo.example.com'], + error: + "Invalid snapshots-enabled value '//repo.example.com' in mvn-repositories entry at line 1. Expected true or false" + }, + { + entries: ['private::true'], + error: + 'Invalid mvn-repositories entry at line 1. repository-id, repository URL, and snapshots-enabled are required' + }, + { + entries: ['private:https://repo.example.com:sometimes'], + error: + "Invalid snapshots-enabled value 'sometimes' in mvn-repositories entry at line 1. Expected true or false" + } + ])('rejects malformed Maven repositories: $entries', ({entries, error}) => { + expect(() => auth.parseMavenRepositories(entries, true)).toThrow(error); + }); + + it('rejects duplicate Maven repository ids', () => { + expect(() => + auth.parseMavenRepositories( + [ + 'private:https://first.example.com:false', + 'private:https://second.example.com:true' + ], + true + ) + ).toThrow("Duplicate repository-id 'private' in mvn-repositories input"); + }); + + it('reserves the Central repository id only when automatic Central inclusion is enabled', () => { + const central = 'central:https://custom.example.com/maven:false'; + + expect(() => auth.parseMavenRepositories([central], true)).toThrow( + "Repository-id 'central' is reserved when mvn-repositories-include-central is enabled" + ); + expect(auth.parseMavenRepositories([central], false)).toEqual([ + { + id: 'central', + url: 'https://custom.example.com/maven', + snapshotsEnabled: false + } + ]); + }); + + it('uses an explicit Central repository instead of adding a disabled override', () => { + const parsed = parseXmlObject( + auth.generate( + credentials('packages', 'USERNAME', 'PASSWORD'), + undefined, + repositorySettings( + auth.parseMavenRepositories( + ['central:https://mirror.example.com/maven:true'], + false + ), + false + ) + ) + ) as any; + + expect(parsed.settings.profiles.profile.repositories.repository).toEqual({ + id: 'central', + url: 'https://mirror.example.com/maven', + snapshots: {enabled: 'true'} + }); + }); + + it('reads Maven repository settings and Central controls', () => { + (core.getMultilineInput as jest.Mock).mockImplementation((name: string) => + name === 'mvn-repositories' + ? ['private:https://repo.example.com:true'] + : [] + ); + (core.getInput as jest.Mock).mockImplementation((name: string) => { + const inputs: Record = { + 'mvn-repositories-include-central': 'false', + 'mvn-repositories-prioritize-central': 'false' + }; + return inputs[name] ?? ''; + }); + + expect(auth.getMavenRepositorySettings()).toEqual({ + repositories: [ + { + id: 'private', + url: 'https://repo.example.com', + snapshotsEnabled: true + } + ], + includeCentral: false, + prioritizeCentral: false + }); + }); + + it('does not read repository controls when no repositories are configured', () => { + expect(auth.getMavenRepositorySettings()).toBeUndefined(); + expect(core.getInput).not.toHaveBeenCalled(); + }); + it.each([ { entries: ['releases:RELEASES_USERNAME'], diff --git a/action.yml b/action.yml index e5f9cb89..e4251184 100644 --- a/action.yml +++ b/action.yml @@ -67,6 +67,17 @@ inputs: 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 + mvn-repositories: + description: 'Multiline list of Maven dependency repositories in the format `repository-id:repository-url:snapshots-enabled`.' + required: false + mvn-repositories-include-central: + description: 'Include Maven Central in the generated dependency repositories profile. When false, Central is disabled unless an explicit repository with ID `central` is declared.' + required: false + default: true + mvn-repositories-prioritize-central: + description: 'Place Maven Central before custom dependency repositories. Has no effect when Maven Central is excluded.' + required: false + default: true 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 7d7de461..a880a7be 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_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 */ +/* 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_MVN_REPOSITORIES, INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL, INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL, 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, MAVEN_REPOSITORIES_PROFILE_ID, MAVEN_CENTRAL_REPOSITORY_ID, MAVEN_CENTRAL_REPOSITORY_URL, 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'; @@ -30790,6 +30790,9 @@ 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_MVN_REPOSITORIES = 'mvn-repositories'; +const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL = 'mvn-repositories-include-central'; +const INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL = 'mvn-repositories-prioritize-central'; 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'; @@ -30810,6 +30813,9 @@ const INPUT_DEFAULT_GPG_PASSPHRASE = 'GPG_PASSPHRASE'; const MAVEN_GPG_PASSPHRASE_DEFAULT_ENV = 'MAVEN_GPG_PASSPHRASE'; // Id of the settings.xml profile used to set `gpg.passphraseEnvName`. const GPG_PASSPHRASE_PROFILE_ID = 'setup-java-gpg'; +const MAVEN_REPOSITORIES_PROFILE_ID = 'setup-java-repositories'; +const MAVEN_CENTRAL_REPOSITORY_ID = 'central'; +const MAVEN_CENTRAL_REPOSITORY_URL = 'https://repo.maven.apache.org/maven2'; const INPUT_CACHE = 'cache'; const INPUT_CACHE_JDK = 'cache-jdk'; const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path'; diff --git a/dist/setup/81.index.js b/dist/setup/81.index.js index a4e79cb5..7e1fa785 100644 --- a/dist/setup/81.index.js +++ b/dist/setup/81.index.js @@ -11,7 +11,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ createAuthenticationSettings: () => (/* binding */ createAuthenticationSettings), /* harmony export */ generate: () => (/* binding */ generate), /* harmony export */ getInputWithDeprecatedAlias: () => (/* binding */ getInputWithDeprecatedAlias), +/* harmony export */ getMavenRepositorySettings: () => (/* binding */ getMavenRepositorySettings), /* harmony export */ getMavenServerSettings: () => (/* binding */ getMavenServerSettings), +/* harmony export */ parseMavenRepositories: () => (/* binding */ parseMavenRepositories), /* harmony export */ parseMavenServerCredentials: () => (/* binding */ parseMavenServerCredentials) /* harmony export */ }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6928); @@ -37,6 +39,7 @@ __webpack_require__.r(__webpack_exports__); async function configureAuthentication() { const servers = getMavenServerSettings(); + const repositorySettings = getMavenRepositorySettings(); 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 +49,7 @@ async function configureAuthentication() { if (gpgPrivateKey) { _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .setSecret */ .Pq(gpgPrivateKey); } - await createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar); + await createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar, repositorySettings); 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); @@ -106,15 +109,68 @@ function parseMavenServerCredentials(entries) { }); return servers; } -async function createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar = undefined) { +// only exported for testing purposes +function getMavenRepositorySettings() { + const entries = _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getMultilineInput */ .q3(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_REPOSITORIES */ .W2); + if (!entries.some(entry => entry.trim())) { + return undefined; + } + const includeCentral = (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getBooleanInput */ .Vt)(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL */ .H5, true); + return { + repositories: parseMavenRepositories(entries, includeCentral), + includeCentral, + prioritizeCentral: (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getBooleanInput */ .Vt)(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL */ .OT, true) + }; +} +// only exported for testing purposes +function parseMavenRepositories(entries, includeCentral) { + const repositories = []; + const repositoryIds = new Set(); + entries.forEach((entry, index) => { + if (!entry.trim()) { + return; + } + const firstSeparator = entry.indexOf(':'); + const lastSeparator = entry.lastIndexOf(':'); + if (firstSeparator <= 0 || lastSeparator <= firstSeparator) { + throw new Error(`Invalid mvn-repositories entry at line ${index + 1}. Expected format: repository-id:repository-url:snapshots-enabled`); + } + const id = entry.slice(0, firstSeparator).trim(); + const url = entry.slice(firstSeparator + 1, lastSeparator).trim(); + const snapshotsValue = entry + .slice(lastSeparator + 1) + .trim() + .toLowerCase(); + if (!id || !url || !snapshotsValue) { + throw new Error(`Invalid mvn-repositories entry at line ${index + 1}. repository-id, repository URL, and snapshots-enabled are required`); + } + if (snapshotsValue !== 'true' && snapshotsValue !== 'false') { + throw new Error(`Invalid snapshots-enabled value '${snapshotsValue}' in mvn-repositories entry at line ${index + 1}. Expected true or false`); + } + if (repositoryIds.has(id)) { + throw new Error(`Duplicate repository-id '${id}' in mvn-repositories input`); + } + if (includeCentral && id === _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_CENTRAL_REPOSITORY_ID */ .xg) { + throw new Error(`Repository-id '${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_CENTRAL_REPOSITORY_ID */ .xg}' is reserved when ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL */ .H5} is enabled`); + } + repositoryIds.add(id); + repositories.push({ + id, + url, + snapshotsEnabled: snapshotsValue === 'true' + }); + }); + return repositories; +} +async function createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar = undefined, repositorySettings = 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(servers, gpgPassphraseEnvVar), overwriteSettings); + await write(settingsDirectory, generate(servers, gpgPassphraseEnvVar, repositorySettings), overwriteSettings); } // only exported for testing purposes -function generate(servers, gpgPassphraseEnvVar) { +function generate(servers, gpgPassphraseEnvVar, repositorySettings) { // 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; @@ -134,8 +190,48 @@ function generate(servers, gpgPassphraseEnvVar) { 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$}`, ' '); + if (repositorySettings || includeGpgPassphraseProfile) { + lines.push(' '); + if (repositorySettings) { + const centralRepository = { + id: _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_CENTRAL_REPOSITORY_ID */ .xg, + url: _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_CENTRAL_REPOSITORY_URL */ .jv, + snapshotsEnabled: false + }; + const customCentralConfigured = repositorySettings.repositories.some(repository => repository.id === _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_CENTRAL_REPOSITORY_ID */ .xg); + const repositories = repositorySettings.includeCentral + ? repositorySettings.prioritizeCentral + ? [centralRepository, ...repositorySettings.repositories] + : [...repositorySettings.repositories, centralRepository] + : customCentralConfigured + ? repositorySettings.repositories + : [ + ...repositorySettings.repositories, + { ...centralRepository, releasesEnabled: false } + ]; + lines.push(' ', ` ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_REPOSITORIES_PROFILE_ID */ .hq}`, ' '); + for (const repository of repositories) { + lines.push(' ', ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(repository.id)}`, ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(repository.url)}`, ...(repository.releasesEnabled === undefined + ? [] + : [ + ' ', + ` ${repository.releasesEnabled}`, + ' ' + ]), ' ', ` ${repository.snapshotsEnabled}`, ' ', ' '); + } + 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)}`, ' ', ' '); + } + lines.push(' ', ' '); + if (repositorySettings) { + lines.push(` ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_REPOSITORIES_PROFILE_ID */ .hq}`); + } + if (includeGpgPassphraseProfile) { + lines.push(` ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .GPG_PASSPHRASE_PROFILE_ID */ .K$}`); + } + lines.push(' '); } lines.push(''); return lines.join('\n'); diff --git a/dist/setup/index.js b/dist/setup/index.js index 97b679e6..ab4c3e70 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -30772,17 +30772,20 @@ module.exports = { /* harmony export */ E8: () => (/* binding */ INPUT_SET_DEFAULT), /* harmony export */ Fi: () => (/* binding */ STATE_GPG_HOME), /* harmony export */ GL: () => (/* binding */ INPUT_CACHE_JDK), +/* harmony export */ H5: () => (/* binding */ INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL), /* 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 */ OT: () => (/* binding */ INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL), /* harmony export */ PG: () => (/* binding */ MACOS_JAVA_CONTENT_POSTFIX), /* harmony export */ QM: () => (/* binding */ INPUT_JAVA_VERSION), /* harmony export */ RX: () => (/* binding */ INPUT_DEFAULT_GPG_PASSPHRASE), /* harmony export */ TS: () => (/* binding */ INPUT_OVERWRITE_SETTINGS), /* harmony export */ TY: () => (/* binding */ INPUT_GPG_PASSPHRASE_DEPRECATED), /* harmony export */ Vt: () => (/* binding */ INPUT_SERVER_PASSWORD_DEPRECATED), +/* harmony export */ W2: () => (/* binding */ INPUT_MVN_REPOSITORIES), /* harmony export */ Wj: () => (/* binding */ INPUT_DEFAULT_SERVER_USERNAME), /* harmony export */ Wt: () => (/* binding */ INPUT_PROBLEM_MATCHER), /* harmony export */ Xh: () => (/* binding */ INPUT_SETTINGS_PATH), @@ -30793,7 +30796,9 @@ module.exports = { /* harmony export */ fd: () => (/* binding */ INPUT_SERVER_ID), /* harmony export */ g_: () => (/* binding */ INPUT_DISTRIBUTION), /* harmony export */ gk: () => (/* binding */ INPUT_CACHE), +/* harmony export */ hq: () => (/* binding */ MAVEN_REPOSITORIES_PROFILE_ID), /* harmony export */ iT: () => (/* binding */ M2_DIR), +/* harmony export */ jv: () => (/* binding */ MAVEN_CENTRAL_REPOSITORY_URL), /* harmony export */ kM: () => (/* binding */ INPUT_JDK_FILE), /* harmony export */ kN: () => (/* binding */ MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG), /* harmony export */ ko: () => (/* binding */ MAVEN_GPG_PASSPHRASE_DEFAULT_ENV), @@ -30813,6 +30818,7 @@ module.exports = { /* harmony export */ wX: () => (/* binding */ INPUT_SHOW_DOWNLOAD_PROGRESS), /* harmony export */ wc: () => (/* binding */ INPUT_JDK_FILE_DEPRECATED), /* harmony export */ wz: () => (/* binding */ INPUT_GPG_PRIVATE_KEY), +/* harmony export */ xg: () => (/* binding */ MAVEN_CENTRAL_REPOSITORY_ID), /* harmony export */ xp: () => (/* binding */ INPUT_DEFAULT_SERVER_PASSWORD) /* harmony export */ }); /* unused harmony exports INPUT_CACHE_READ_ONLY, INPUT_JOB_STATUS */ @@ -30831,6 +30837,9 @@ 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_MVN_REPOSITORIES = 'mvn-repositories'; +const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL = 'mvn-repositories-include-central'; +const INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL = 'mvn-repositories-prioritize-central'; 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'; @@ -30851,6 +30860,9 @@ const INPUT_DEFAULT_GPG_PASSPHRASE = 'GPG_PASSPHRASE'; const MAVEN_GPG_PASSPHRASE_DEFAULT_ENV = 'MAVEN_GPG_PASSPHRASE'; // Id of the settings.xml profile used to set `gpg.passphraseEnvName`. const GPG_PASSPHRASE_PROFILE_ID = 'setup-java-gpg'; +const MAVEN_REPOSITORIES_PROFILE_ID = 'setup-java-repositories'; +const MAVEN_CENTRAL_REPOSITORY_ID = 'central'; +const MAVEN_CENTRAL_REPOSITORY_URL = 'https://repo.maven.apache.org/maven2'; const INPUT_CACHE = 'cache'; const INPUT_CACHE_JDK = 'cache-jdk'; const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path'; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index c193c234..def22343 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -907,6 +907,82 @@ This configuration produces the following server entries: ``` +### Resolving Maven dependencies from custom repositories + +Use `mvn-repositories` when Maven must download dependencies from repositories +outside Maven Central. Each line has the format +`repository-id:repository-url:snapshots-enabled`. The parser uses the first and +last colons as separators, so repository URLs can contain a scheme or port. + +The repository ID can match a `mvn-server-credentials` server ID to authenticate +requests to a private repository: + +```yaml +steps: + - uses: actions/checkout@v7 + - name: Set up Java and private Maven repositories + uses: actions/setup-java@v6 + with: + distribution: 'temurin' + java-version: '21' + mvn-server-credentials: | + private:PRIVATE_REPOSITORY_USERNAME:PRIVATE_REPOSITORY_TOKEN + mvn-repositories: | + private:https://maven.example.com:8443/releases:false + snapshots:https://maven.example.com:8443/snapshots:true + mvn-repositories-include-central: true + mvn-repositories-prioritize-central: true + - run: mvn --batch-mode verify + env: + PRIVATE_REPOSITORY_USERNAME: ${{ secrets.PRIVATE_REPOSITORY_USERNAME }} + PRIVATE_REPOSITORY_TOKEN: ${{ secrets.PRIVATE_REPOSITORY_TOKEN }} +``` + +This configuration adds the following active profile to `settings.xml`: + +```xml + + + setup-java-repositories + + + central + https://repo.maven.apache.org/maven2 + + false + + + + private + https://maven.example.com:8443/releases + + false + + + + snapshots + https://maven.example.com:8443/snapshots + + true + + + + + + + setup-java-repositories + +``` + +Maven Central is included first by default. Set +`mvn-repositories-prioritize-central: false` to place custom repositories +first, or set `mvn-repositories-include-central: false` to disable Central. The +generated profile overrides the Central repository inherited from Maven's Super +POM with releases and snapshots disabled. When automatic Central inclusion is +off, the ID `central` may instead be declared explicitly in `mvn-repositories` +to replace it with a user-specified repository; otherwise that ID is reserved +to prevent duplicate entries. + ### 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 adb66782..3e20cb25 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -15,8 +15,22 @@ export interface MavenServerCredentials { passwordEnvVar: string; } +export interface MavenRepository { + id: string; + url: string; + snapshotsEnabled: boolean; + releasesEnabled?: boolean; +} + +export interface MavenRepositorySettings { + repositories: MavenRepository[]; + includeCentral: boolean; + prioritizeCentral: boolean; +} + export async function configureAuthentication() { const servers = getMavenServerSettings(); + const repositorySettings = getMavenRepositorySettings(); const settingsDirectory = core.getInput(constants.INPUT_SETTINGS_PATH) || path.join(os.homedir(), constants.M2_DIR); @@ -41,7 +55,8 @@ export async function configureAuthentication() { servers, settingsDirectory, overwriteSettings, - gpgPassphraseEnvVar + gpgPassphraseEnvVar, + repositorySettings ); if (gpgPrivateKey) { @@ -141,11 +156,93 @@ export function parseMavenServerCredentials( return servers; } +// only exported for testing purposes +export function getMavenRepositorySettings(): + MavenRepositorySettings | undefined { + const entries = core.getMultilineInput(constants.INPUT_MVN_REPOSITORIES); + if (!entries.some(entry => entry.trim())) { + return undefined; + } + + const includeCentral = getBooleanInput( + constants.INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL, + true + ); + return { + repositories: parseMavenRepositories(entries, includeCentral), + includeCentral, + prioritizeCentral: getBooleanInput( + constants.INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL, + true + ) + }; +} + +// only exported for testing purposes +export function parseMavenRepositories( + entries: string[], + includeCentral: boolean +): MavenRepository[] { + const repositories: MavenRepository[] = []; + const repositoryIds = new Set(); + + entries.forEach((entry, index) => { + if (!entry.trim()) { + return; + } + + const firstSeparator = entry.indexOf(':'); + const lastSeparator = entry.lastIndexOf(':'); + if (firstSeparator <= 0 || lastSeparator <= firstSeparator) { + throw new Error( + `Invalid mvn-repositories entry at line ${index + 1}. Expected format: repository-id:repository-url:snapshots-enabled` + ); + } + + const id = entry.slice(0, firstSeparator).trim(); + const url = entry.slice(firstSeparator + 1, lastSeparator).trim(); + const snapshotsValue = entry + .slice(lastSeparator + 1) + .trim() + .toLowerCase(); + if (!id || !url || !snapshotsValue) { + throw new Error( + `Invalid mvn-repositories entry at line ${index + 1}. repository-id, repository URL, and snapshots-enabled are required` + ); + } + if (snapshotsValue !== 'true' && snapshotsValue !== 'false') { + throw new Error( + `Invalid snapshots-enabled value '${snapshotsValue}' in mvn-repositories entry at line ${index + 1}. Expected true or false` + ); + } + if (repositoryIds.has(id)) { + throw new Error( + `Duplicate repository-id '${id}' in mvn-repositories input` + ); + } + if (includeCentral && id === constants.MAVEN_CENTRAL_REPOSITORY_ID) { + throw new Error( + `Repository-id '${constants.MAVEN_CENTRAL_REPOSITORY_ID}' is reserved when ${constants.INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL} is enabled` + ); + } + + repositoryIds.add(id); + repositories.push({ + id, + url, + snapshotsEnabled: snapshotsValue === 'true' + }); + }); + + return repositories; +} + export async function createAuthenticationSettings( servers: MavenServerCredentials[], settingsDirectory: string, overwriteSettings: boolean, - gpgPassphraseEnvVar: string | undefined = undefined + gpgPassphraseEnvVar: string | undefined = undefined, + repositorySettings: MavenRepositorySettings | undefined = undefined ) { core.info( `Creating ${constants.MVN_SETTINGS_FILE} with server-id: ${servers.map(server => server.id).join(', ')}` @@ -155,7 +252,7 @@ export async function createAuthenticationSettings( await io.mkdirP(settingsDirectory); await write( settingsDirectory, - generate(servers, gpgPassphraseEnvVar), + generate(servers, gpgPassphraseEnvVar, repositorySettings), overwriteSettings ); } @@ -163,7 +260,8 @@ export async function createAuthenticationSettings( // only exported for testing purposes export function generate( servers: MavenServerCredentials[], - gpgPassphraseEnvVar?: string | undefined + gpgPassphraseEnvVar?: string | undefined, + repositorySettings?: MavenRepositorySettings | undefined ) { // The maven-gpg-plugin reads the passphrase from the environment variable // named by the `gpg.passphraseEnvName` property (default MAVEN_GPG_PASSPHRASE). @@ -194,20 +292,75 @@ export function generate( } lines.push(' '); - if (includeGpgPassphraseProfile) { - lines.push( - ' ', - ' ', - ` ${constants.GPG_PASSPHRASE_PROFILE_ID}`, - ' ', - ` ${escapeXmlText(gpgPassphraseEnvVar)}`, - ' ', - ' ', - ' ', - ' ', - ` ${constants.GPG_PASSPHRASE_PROFILE_ID}`, - ' ' - ); + if (repositorySettings || includeGpgPassphraseProfile) { + lines.push(' '); + if (repositorySettings) { + const centralRepository: MavenRepository = { + id: constants.MAVEN_CENTRAL_REPOSITORY_ID, + url: constants.MAVEN_CENTRAL_REPOSITORY_URL, + snapshotsEnabled: false + }; + const customCentralConfigured = repositorySettings.repositories.some( + repository => repository.id === constants.MAVEN_CENTRAL_REPOSITORY_ID + ); + const repositories = repositorySettings.includeCentral + ? repositorySettings.prioritizeCentral + ? [centralRepository, ...repositorySettings.repositories] + : [...repositorySettings.repositories, centralRepository] + : customCentralConfigured + ? repositorySettings.repositories + : [ + ...repositorySettings.repositories, + {...centralRepository, releasesEnabled: false} + ]; + + lines.push( + ' ', + ` ${constants.MAVEN_REPOSITORIES_PROFILE_ID}`, + ' ' + ); + for (const repository of repositories) { + lines.push( + ' ', + ` ${escapeXmlText(repository.id)}`, + ` ${escapeXmlText(repository.url)}`, + ...(repository.releasesEnabled === undefined + ? [] + : [ + ' ', + ` ${repository.releasesEnabled}`, + ' ' + ]), + ' ', + ` ${repository.snapshotsEnabled}`, + ' ', + ' ' + ); + } + lines.push(' ', ' '); + } + if (includeGpgPassphraseProfile) { + lines.push( + ' ', + ` ${constants.GPG_PASSPHRASE_PROFILE_ID}`, + ' ', + ` ${escapeXmlText(gpgPassphraseEnvVar)}`, + ' ', + ' ' + ); + } + lines.push(' ', ' '); + if (repositorySettings) { + lines.push( + ` ${constants.MAVEN_REPOSITORIES_PROFILE_ID}` + ); + } + if (includeGpgPassphraseProfile) { + lines.push( + ` ${constants.GPG_PASSPHRASE_PROFILE_ID}` + ); + } + lines.push(' '); } lines.push(''); diff --git a/src/constants.ts b/src/constants.ts index 7a13679a..a11ab3a5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -13,6 +13,11 @@ 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_MVN_REPOSITORIES = 'mvn-repositories'; +export const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL = + 'mvn-repositories-include-central'; +export const INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL = + 'mvn-repositories-prioritize-central'; 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'; @@ -36,6 +41,10 @@ export const MAVEN_GPG_PASSPHRASE_DEFAULT_ENV = 'MAVEN_GPG_PASSPHRASE'; // Id of the settings.xml profile used to set `gpg.passphraseEnvName`. export const GPG_PASSPHRASE_PROFILE_ID = 'setup-java-gpg'; +export const MAVEN_REPOSITORIES_PROFILE_ID = 'setup-java-repositories'; +export const MAVEN_CENTRAL_REPOSITORY_ID = 'central'; +export const MAVEN_CENTRAL_REPOSITORY_URL = + 'https://repo.maven.apache.org/maven2'; export const INPUT_CACHE = 'cache'; export const INPUT_CACHE_JDK = 'cache-jdk';