diff --git a/README.md b/README.md index 5fe1cca96..1fde7c540 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ 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-server-repository-origins` | Multiline Maven credential origins in the format `server-id:repository-origin`. Each origin must belong to a configured server ID. | | | `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` | diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index a169451f4..ecb230a28 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -318,6 +318,21 @@ describe('auth tests', () => { ).toEqual(expectedSettings); }); + it('generates repository origins for a Maven server', () => { + const settings = auth.generate([ + { + id: 'central', + usernameEnvVar: 'CENTRAL_USER', + passwordEnvVar: 'CENTRAL_PASS', + repositoryOrigins: ['https://central.sonatype.com'] + } + ]); + + expect(settings).toContain(` + https://central.sonatype.com + `); + }); + it('does not add a gpg profile when the passphrase env var is the maven-gpg-plugin default', () => { const id = 'packages'; const username = 'USER'; @@ -749,16 +764,23 @@ describe('auth tests', () => { }); 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' - ]); + (core.getMultilineInput as jest.Mock).mockImplementation((name: string) => + name === 'mvn-server-credentials' + ? [ + 'releases:RELEASES_USERNAME:RELEASES_PASSWORD', + 'snapshots:SNAPSHOTS_USERNAME:SNAPSHOTS_PASSWORD' + ] + : name === 'mvn-server-repository-origins' + ? ['releases:https://repo.example.com'] + : [] + ); expect(auth.getMavenServerSettings()).toEqual([ { id: 'releases', usernameEnvVar: 'RELEASES_USERNAME', - passwordEnvVar: 'RELEASES_PASSWORD' + passwordEnvVar: 'RELEASES_PASSWORD', + repositoryOrigins: ['https://repo.example.com'] }, { id: 'snapshots', @@ -803,6 +825,106 @@ describe('auth tests', () => { expect(core.warning).toHaveBeenCalledTimes(2); }); + it('adds normalized repository origins to matching Maven servers', () => { + expect( + auth.addMavenServerRepositoryOrigins( + [ + { + id: 'central', + usernameEnvVar: 'CENTRAL_USER', + passwordEnvVar: 'CENTRAL_PASS' + }, + { + id: 'packages', + usernameEnvVar: 'GITHUB_ACTOR', + passwordEnvVar: 'GITHUB_TOKEN' + } + ], + [ + 'central:https://central.sonatype.com/', + 'central:https://central.sonatype.com', + 'central:https://central.sonatype.com:443', + 'packages:https://maven.pkg.github.com', + 'packages:ssh://packages.example.com:22', + 'packages:ftp://packages.example.com:21' + ] + ) + ).toEqual([ + { + id: 'central', + usernameEnvVar: 'CENTRAL_USER', + passwordEnvVar: 'CENTRAL_PASS', + repositoryOrigins: ['https://central.sonatype.com'] + }, + { + id: 'packages', + usernameEnvVar: 'GITHUB_ACTOR', + passwordEnvVar: 'GITHUB_TOKEN', + repositoryOrigins: [ + 'https://maven.pkg.github.com', + 'ssh://packages.example.com:22', + 'ftp://packages.example.com:21' + ] + } + ]); + }); + + it.each([ + { + entries: ['central'], + error: + 'Invalid mvn-server-repository-origins entry at line 1. Expected format: server-id:repository-origin' + }, + { + entries: ['other:https://repo.example.com'], + error: + "Unknown server-id 'other' in mvn-server-repository-origins at line 1" + }, + { + entries: ['central:https://repo.example.com/path'], + error: + "Invalid repository origin 'https://repo.example.com/path' in mvn-server-repository-origins at line 1" + }, + { + entries: ['central:https:repo.example.com'], + error: + "Invalid repository origin 'https:repo.example.com' in mvn-server-repository-origins at line 1" + }, + { + entries: ['central:https:/repo.example.com'], + error: + "Invalid repository origin 'https:/repo.example.com' in mvn-server-repository-origins at line 1" + }, + { + entries: ['central:https://repo.example.com?'], + error: + "Invalid repository origin 'https://repo.example.com?' in mvn-server-repository-origins at line 1" + }, + { + entries: ['central:https://repo.example.com#'], + error: + "Invalid repository origin 'https://repo.example.com#' in mvn-server-repository-origins at line 1" + }, + { + entries: ['central:https://@repo.example.com'], + error: + "Invalid repository origin 'https://@repo.example.com' in mvn-server-repository-origins at line 1" + } + ])('rejects invalid Maven server repository origins', ({entries, error}) => { + expect(() => + auth.addMavenServerRepositoryOrigins( + [ + { + id: 'central', + usernameEnvVar: 'CENTRAL_USER', + passwordEnvVar: 'CENTRAL_PASS' + } + ], + entries + ) + ).toThrow(error); + }); + 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 69f73ff40..5613d7cf7 100644 --- a/action.yml +++ b/action.yml @@ -66,6 +66,9 @@ 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-server-repository-origins: + description: 'Multiline list of Maven credential origins in the format `server-id:repository-origin`. Each origin must belong to a configured server ID.' + required: false mvn-repositories: description: 'Multiline list of Maven dependency repositories in the format `repository-id:repository-url:snapshots-enabled`.' required: false diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 43b8c0acd..772499681 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -31040,7 +31040,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, SIGNATURE_VERIFICATION_DOCUMENTATION_URL, SIGNATURE_VERIFICATION_FAILURE_HELP, 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 */ +/* 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, SIGNATURE_VERIFICATION_DOCUMENTATION_URL, SIGNATURE_VERIFICATION_FAILURE_HELP, INPUT_MVN_SERVER_CREDENTIALS, INPUT_MVN_SERVER_REPOSITORY_ORIGINS, 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'; @@ -31058,6 +31058,7 @@ const INPUT_VERIFY_SIGNATURE_PUBLIC_KEY = 'verify-signature-public-key'; const SIGNATURE_VERIFICATION_DOCUMENTATION_URL = 'https://github.com/actions/setup-java#download-integrity-and-signatures'; const SIGNATURE_VERIFICATION_FAILURE_HELP = (/* unused pure expression or super */ null && (`If this is a legitimate vendor signing-key rotation, see ${SIGNATURE_VERIFICATION_DOCUMENTATION_URL} for instructions to configure the updated public key or temporarily disable signature verification.`)); const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials'; +const INPUT_MVN_SERVER_REPOSITORY_ORIGINS = 'mvn-server-repository-origins'; 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'; diff --git a/dist/setup/81.index.js b/dist/setup/81.index.js index 1aecd7fad..9309d61de 100644 --- a/dist/setup/81.index.js +++ b/dist/setup/81.index.js @@ -7,6 +7,7 @@ export const modules = { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ addMavenServerRepositoryOrigins: () => (/* binding */ addMavenServerRepositoryOrigins), /* harmony export */ configureAuthentication: () => (/* binding */ configureAuthentication), /* harmony export */ createAuthenticationSettings: () => (/* binding */ createAuthenticationSettings), /* harmony export */ generate: () => (/* binding */ generate), @@ -37,6 +38,7 @@ __webpack_require__.r(__webpack_exports__); +const MAVEN_REPOSITORY_ORIGIN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#@]+\/?$/; async function configureAuthentication() { const servers = getMavenServerSettings(); const repositorySettings = getMavenRepositorySettings(); @@ -74,16 +76,16 @@ function getInputWithDeprecatedAlias(inputName, deprecatedInputName, defaultValu // 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) - } - ]; + const servers = entries.some(entry => entry.trim()) + ? parseMavenServerCredentials(entries) + : [ + { + 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) + } + ]; + return addMavenServerRepositoryOrigins(servers, _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getMultilineInput */ .q3(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_SERVER_REPOSITORY_ORIGINS */ .gR)); } // only exported for testing purposes function parseMavenServerCredentials(entries) { @@ -110,6 +112,61 @@ function parseMavenServerCredentials(entries) { return servers; } // only exported for testing purposes +function addMavenServerRepositoryOrigins(servers, entries) { + const serverIds = new Set(servers.map(server => server.id)); + const originsByServer = new Map(); + entries.forEach((entry, index) => { + if (!entry.trim()) { + return; + } + const separator = entry.indexOf(':'); + if (separator <= 0 || separator === entry.length - 1) { + throw new Error(`Invalid mvn-server-repository-origins entry at line ${index + 1}. Expected format: server-id:repository-origin`); + } + const id = entry.slice(0, separator).trim(); + const value = entry.slice(separator + 1).trim(); + if (!id || !value) { + throw new Error(`Invalid mvn-server-repository-origins entry at line ${index + 1}. Server ID and repository origin are required`); + } + if (!serverIds.has(id)) { + throw new Error(`Unknown server-id '${id}' in mvn-server-repository-origins at line ${index + 1}`); + } + let url; + try { + if (!MAVEN_REPOSITORY_ORIGIN.test(value)) { + throw new Error(); + } + url = new URL(value); + } + catch { + throw new Error(`Invalid repository origin '${value}' in mvn-server-repository-origins at line ${index + 1}`); + } + if (!url.host || + url.username || + url.password || + (url.pathname !== '' && url.pathname !== '/') || + url.search || + url.hash) { + throw new Error(`Invalid repository origin '${value}' in mvn-server-repository-origins at line ${index + 1}`); + } + const explicitPort = /:(\d+)\/?$/.exec(value)?.[1]; + const port = explicitPort ? Number.parseInt(explicitPort, 10) : undefined; + const includePort = port !== undefined && + !((url.protocol === 'http:' && port === 80) || + (url.protocol === 'https:' && port === 443)); + const origin = `${url.protocol}//${url.hostname}${includePort ? `:${port}` : ''}`; + const origins = originsByServer.get(id) || []; + if (!origins.includes(origin)) { + origins.push(origin); + originsByServer.set(id, origins); + } + }); + return servers.map(server => { + const repositoryOrigins = originsByServer.get(server.id); + return repositoryOrigins ? { ...server, repositoryOrigins } : server; + }); +} +// 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())) { @@ -187,7 +244,15 @@ function generate(servers, gpgPassphraseEnvVar, repositorySettings) { ' ' ]; 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(' ', ` ${(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}}`)}`); + if (server.repositoryOrigins) { + lines.push(' '); + for (const origin of server.repositoryOrigins) { + lines.push(` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(origin)}`); + } + lines.push(' '); + } + lines.push(' '); } lines.push(' '); if (repositorySettings || includeGpgPassphraseProfile) { diff --git a/dist/setup/index.js b/dist/setup/index.js index 18e223de8..1a178aaf7 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -31060,6 +31060,7 @@ module.exports = { /* harmony export */ af: () => (/* binding */ INPUT_JAVA_VERSION_FILE), /* harmony export */ db: () => (/* binding */ INPUT_GPG_PASSPHRASE_ENV_VAR), /* harmony export */ fd: () => (/* binding */ INPUT_SERVER_ID), +/* harmony export */ gR: () => (/* binding */ INPUT_MVN_SERVER_REPOSITORY_ORIGINS), /* harmony export */ g_: () => (/* binding */ INPUT_DISTRIBUTION), /* harmony export */ gk: () => (/* binding */ INPUT_CACHE), /* harmony export */ hq: () => (/* binding */ MAVEN_REPOSITORIES_PROFILE_ID), @@ -31106,6 +31107,7 @@ const INPUT_VERIFY_SIGNATURE_PUBLIC_KEY = 'verify-signature-public-key'; const SIGNATURE_VERIFICATION_DOCUMENTATION_URL = 'https://github.com/actions/setup-java#download-integrity-and-signatures'; const SIGNATURE_VERIFICATION_FAILURE_HELP = `If this is a legitimate vendor signing-key rotation, see ${SIGNATURE_VERIFICATION_DOCUMENTATION_URL} for instructions to configure the updated public key or temporarily disable signature verification.`; const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials'; +const INPUT_MVN_SERVER_REPOSITORY_ORIGINS = 'mvn-server-repository-origins'; 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'; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index a00251d61..fba494cee 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -885,6 +885,8 @@ Use `mvn-server-credentials` to add more than one credential entry to the genera When this input is set, it replaces the single server configured by `server-id`, `server-username-env-var`, and `server-password-env-var`. +Use `mvn-server-repository-origins` when a Maven server credential must be allowed for an explicit repository origin. Each line has the format `server-id:repository-origin`; a server can have multiple origins. This is required by Maven 3.10 and later when the repository URL is not otherwise associated with the server ID. + ```yaml steps: - uses: actions/checkout@v7 @@ -896,6 +898,9 @@ steps: mvn-server-credentials: | releases:RELEASES_USERNAME:RELEASES_PASSWORD snapshots:SNAPSHOTS_USERNAME:SNAPSHOTS_PASSWORD + mvn-server-repository-origins: | + releases:https://central.sonatype.com + snapshots:https://central.sonatype.com - name: Publish with Maven run: mvn deploy env: @@ -913,11 +918,17 @@ This configuration produces the following server entries: releases ${env.RELEASES_USERNAME} ${env.RELEASES_PASSWORD} + + https://central.sonatype.com + snapshots ${env.SNAPSHOTS_USERNAME} ${env.SNAPSHOTS_PASSWORD} + + https://central.sonatype.com + ``` diff --git a/src/auth.ts b/src/auth.ts index 3e20cb25b..096e1fb54 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -13,6 +13,7 @@ export interface MavenServerCredentials { id: string; usernameEnvVar: string; passwordEnvVar: string; + repositoryOrigins?: string[]; } export interface MavenRepository { @@ -28,6 +29,8 @@ export interface MavenRepositorySettings { prioritizeCentral: boolean; } +const MAVEN_REPOSITORY_ORIGIN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#@]+\/?$/; + export async function configureAuthentication() { const servers = getMavenServerSettings(); const repositorySettings = getMavenRepositorySettings(); @@ -95,25 +98,28 @@ export function getMavenServerSettings(): MavenServerCredentials[] { 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 - ) - } - ]; + const servers = entries.some(entry => entry.trim()) + ? parseMavenServerCredentials(entries) + : [ + { + 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 + ) + } + ]; + + return addMavenServerRepositoryOrigins( + servers, + core.getMultilineInput(constants.INPUT_MVN_SERVER_REPOSITORY_ORIGINS) + ); } // only exported for testing purposes @@ -156,6 +162,85 @@ export function parseMavenServerCredentials( return servers; } +// only exported for testing purposes +export function addMavenServerRepositoryOrigins( + servers: MavenServerCredentials[], + entries: string[] +): MavenServerCredentials[] { + const serverIds = new Set(servers.map(server => server.id)); + const originsByServer = new Map(); + + entries.forEach((entry, index) => { + if (!entry.trim()) { + return; + } + + const separator = entry.indexOf(':'); + if (separator <= 0 || separator === entry.length - 1) { + throw new Error( + `Invalid mvn-server-repository-origins entry at line ${index + 1}. Expected format: server-id:repository-origin` + ); + } + + const id = entry.slice(0, separator).trim(); + const value = entry.slice(separator + 1).trim(); + if (!id || !value) { + throw new Error( + `Invalid mvn-server-repository-origins entry at line ${index + 1}. Server ID and repository origin are required` + ); + } + if (!serverIds.has(id)) { + throw new Error( + `Unknown server-id '${id}' in mvn-server-repository-origins at line ${index + 1}` + ); + } + + let url: URL; + try { + if (!MAVEN_REPOSITORY_ORIGIN.test(value)) { + throw new Error(); + } + url = new URL(value); + } catch { + throw new Error( + `Invalid repository origin '${value}' in mvn-server-repository-origins at line ${index + 1}` + ); + } + if ( + !url.host || + url.username || + url.password || + (url.pathname !== '' && url.pathname !== '/') || + url.search || + url.hash + ) { + throw new Error( + `Invalid repository origin '${value}' in mvn-server-repository-origins at line ${index + 1}` + ); + } + + const explicitPort = /:(\d+)\/?$/.exec(value)?.[1]; + const port = explicitPort ? Number.parseInt(explicitPort, 10) : undefined; + const includePort = + port !== undefined && + !( + (url.protocol === 'http:' && port === 80) || + (url.protocol === 'https:' && port === 443) + ); + const origin = `${url.protocol}//${url.hostname}${includePort ? `:${port}` : ''}`; + const origins = originsByServer.get(id) || []; + if (!origins.includes(origin)) { + origins.push(origin); + originsByServer.set(id, origins); + } + }); + + return servers.map(server => { + const repositoryOrigins = originsByServer.get(server.id); + return repositoryOrigins ? {...server, repositoryOrigins} : server; + }); +} + // only exported for testing purposes export function getMavenRepositorySettings(): MavenRepositorySettings | undefined { @@ -286,9 +371,18 @@ export function generate( ' ', ` ${escapeXmlText(server.id)}`, ` ${escapeXmlText(`\${env.${server.usernameEnvVar}}`)}`, - ` ${escapeXmlText(`\${env.${server.passwordEnvVar}}`)}`, - ' ' + ` ${escapeXmlText(`\${env.${server.passwordEnvVar}}`)}` ); + if (server.repositoryOrigins) { + lines.push(' '); + for (const origin of server.repositoryOrigins) { + lines.push( + ` ${escapeXmlText(origin)}` + ); + } + lines.push(' '); + } + lines.push(' '); } lines.push(' '); diff --git a/src/constants.ts b/src/constants.ts index 9404e188d..6a213a592 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,8 @@ export const SIGNATURE_VERIFICATION_DOCUMENTATION_URL = 'https://github.com/actions/setup-java#download-integrity-and-signatures'; export const SIGNATURE_VERIFICATION_FAILURE_HELP = `If this is a legitimate vendor signing-key rotation, see ${SIGNATURE_VERIFICATION_DOCUMENTATION_URL} for instructions to configure the updated public key or temporarily disable signature verification.`; export const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials'; +export const INPUT_MVN_SERVER_REPOSITORY_ORIGINS = + 'mvn-server-repository-origins'; export const INPUT_MVN_REPOSITORIES = 'mvn-repositories'; export const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL = 'mvn-repositories-include-central';