diff --git a/.github/workflows/e2e-publishing.yml b/.github/workflows/e2e-publishing.yml index 4dcab411..35c1596c 100644 --- a/.github/workflows/e2e-publishing.yml +++ b/.github/workflows/e2e-publishing.yml @@ -174,3 +174,115 @@ jobs: if (-not (Test-Path $path)) { throw "settings.xml file is not found in expected location" } + + test-maven-repositories: + name: Maven ${{ matrix.repositories }} repositories + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] + repositories: [dependencies, plugins, both] + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Prepare Maven model inspection + run: | + $directory = Join-Path $env:RUNNER_TEMP "maven-repository-test" + New-Item -Path $directory -ItemType Directory -Force | Out-Null + $pomPath = Join-Path $directory "pom.xml" + $warmSettingsPath = Join-Path $directory "warm-settings.xml" + $cachePath = Join-Path $directory "repository" + $outputPath = Join-Path $directory "warm-effective-pom.xml" + + # This project repository keeps the inspection plugin available offline + # after the generated settings disable inherited plugin Central. + @' + + 4.0.0 + setup-java.test + repository-settings + 1.0 + + + inspection + https://repo.maven.apache.org/maven2 + + + + '@ | Set-Content -Path $pomPath + '' | Set-Content -Path $warmSettingsPath + + mvn --batch-mode --no-transfer-progress --settings $warmSettingsPath ` + "-Dmaven.repo.local=$cachePath" --file $pomPath ` + org.apache.maven.plugins:maven-help-plugin:3.5.1:effective-pom ` + "-Doutput=$outputPath" + if ($LASTEXITCODE -ne 0) { + throw "Could not prepare the Maven Help Plugin" + } + - name: Configure dependency and plugin repositories + uses: ./ + with: + distribution: 'temurin' + java-version: '21' + settings-path: ${{ runner.temp }}/maven-repository-test/settings + mvn-server-credentials: | + private:PRIVATE_USERNAME:PRIVATE_TOKEN + mvn-repositories: ${{ matrix.repositories != 'plugins' && 'private:https://repo.example.invalid/maven:false' || '' }} + mvn-plugin-repositories: ${{ matrix.repositories != 'dependencies' && 'private:https://repo.example.invalid/maven:false' || '' }} + mvn-repositories-include-central: false + - name: Validate generated settings and effective Maven repositories + env: + REPOSITORY_CONFIGURATION: ${{ matrix.repositories }} + run: | + $directory = Join-Path $env:RUNNER_TEMP "maven-repository-test" + $settingsPath = Join-Path $directory "settings/settings.xml" + [xml]$settings = Get-Content -Path $settingsPath -Raw + $profiles = @($settings.settings.profiles.profile) + if ($profiles.Count -ne 1 -or $profiles[0].id -ne "setup-java-repositories") { + throw "Expected one repository profile" + } + $activeProfiles = @($settings.settings.activeProfiles.activeProfile) + if ($activeProfiles.Count -ne 1 -or $activeProfiles[0] -ne "setup-java-repositories") { + throw "Expected one active repository profile" + } + $server = $settings.settings.servers.server + if ($server.id -ne "private" -or $server.username -ne '${env.PRIVATE_USERNAME}' -or $server.password -ne '${env.PRIVATE_TOKEN}') { + throw "Repository credentials were not preserved" + } + + function Assert-Repositories($repositories, $expectPrivate, $context) { + $central = @($repositories | Where-Object { $_.id -eq "central" }) + if ($central.Count -ne 1 -or $central[0].releases.enabled -ne "false" -or $central[0].snapshots.enabled -ne "false") { + throw "$context did not disable inherited Maven Central" + } + $private = @($repositories | Where-Object { $_.id -eq "private" }) + $expectedCount = if ($expectPrivate) { 1 } else { 0 } + if ($private.Count -ne $expectedCount) { + throw "$context has incorrect private repositories" + } + if ($expectPrivate -and ($private[0].url -ne "https://repo.example.invalid/maven" -or $private[0].snapshots.enabled -ne "false")) { + throw "$context has incorrect private repository settings" + } + } + + $expectDependencies = $env:REPOSITORY_CONFIGURATION -ne "plugins" + $expectPlugins = $env:REPOSITORY_CONFIGURATION -ne "dependencies" + Assert-Repositories @($profiles[0].repositories.repository) $expectDependencies "Settings dependency repositories" + Assert-Repositories @($profiles[0].pluginRepositories.pluginRepository) $expectPlugins "Settings plugin repositories" + + $pomPath = Join-Path $directory "pom.xml" + $cachePath = Join-Path $directory "repository" + $outputPath = Join-Path $directory "effective-pom.xml" + mvn --batch-mode --no-transfer-progress --offline --settings $settingsPath ` + "-Dmaven.repo.local=$cachePath" --file $pomPath ` + org.apache.maven.plugins:maven-help-plugin:3.5.1:effective-pom ` + "-Doutput=$outputPath" + if ($LASTEXITCODE -ne 0) { + throw "Could not inspect the effective Maven model" + } + [xml]$effectivePom = Get-Content -Path $outputPath -Raw + Assert-Repositories @($effectivePom.project.repositories.repository) $expectDependencies "Effective dependency repositories" + Assert-Repositories @($effectivePom.project.pluginRepositories.pluginRepository) $expectPlugins "Effective plugin repositories" diff --git a/README.md b/README.md index 18e89f28..58822e68 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,9 @@ steps: | `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. Requires Maven 3.10+. | | | `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` | +| `mvn-plugin-repositories` | Multiline Maven plugin repositories in the format `repository-id:repository-url:snapshots-enabled`. Can be used independently of `mvn-repositories`. | | +| `mvn-repositories-include-central` | Include Maven Central for dependency and plugin repositories in the generated profile. When `false`, Central is disabled for both types unless that type explicitly declares `central`. Has no effect when neither repository list is configured. | `true` | +| `mvn-repositories-prioritize-central` | Place Maven Central before custom dependency and plugin repositories. Has no effect when Central is excluded or neither repository list is configured. | `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. | | @@ -482,10 +483,20 @@ 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). +For dependencies hosted outside Maven Central, use `mvn-repositories`; for build +plugins, use `mvn-plugin-repositories`. Either input adds its own repository type +to the same 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) +and [Resolving Maven plugins from custom repositories](docs/advanced-usage.md#resolving-maven-plugins-from-custom-repositories). + +> [!WARNING] +> `mvn-repositories-include-central: false` now disables inherited Maven Central +> for **both dependencies and plugins**, even in existing workflows that only +> configure `mvn-repositories`. Builds relying on uncached plugins from Central +> must configure `mvn-plugin-repositories` with an accessible plugin repository +> or explicitly declare `central` there. Neither repository list configured +> means the Central controls remain inactive. ### GPG signing diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index ecb230a2..7af82c8f 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -63,12 +63,51 @@ const credentials = (id: string, username: string, password: string) => [ const repositorySettings = ( repositories: auth.MavenRepository[], includeCentral = true, - prioritizeCentral = true + prioritizeCentral = true, + pluginRepositories: auth.MavenRepository[] = [] ): auth.MavenRepositorySettings => ({ repositories, + pluginRepositories, includeCentral, prioritizeCentral }); +const repositoryInputs = ['mvn-repositories', 'mvn-plugin-repositories']; +const privateRepository = { + id: 'private', + url: 'https://repo.example.com/maven', + snapshotsEnabled: true +}; +const privateRepositoryXml = { + id: privateRepository.id, + url: privateRepository.url, + snapshots: {enabled: 'true'} +}; +const centralRepositoryXml = { + id: 'central', + url: 'https://repo.maven.apache.org/maven2', + snapshots: {enabled: 'false'} +}; +const disabledCentralRepositoryXml = { + ...centralRepositoryXml, + releases: {enabled: 'false'} +}; +const repositoryConfigurations = [ + { + name: 'dependency-only', + repositories: [privateRepository], + pluginRepositories: [] + }, + { + name: 'plugin-only', + repositories: [], + pluginRepositories: [privateRepository] + }, + { + name: 'combined', + repositories: [privateRepository], + pluginRepositories: [privateRepository] + } +]; describe('auth tests', () => { let spyOSHomedir: any; @@ -475,6 +514,87 @@ describe('auth tests', () => { ).toEqual(['first', 'second', 'central']); }); + it.each([ + { + name: 'Central first by default', + prioritizeCentral: undefined, + expectedRepositories: [ + { + id: 'central', + url: 'https://repo.maven.apache.org/maven2', + snapshots: {enabled: 'false'} + }, + { + id: 'first', + url: 'https://first.example.com/plugins', + snapshots: {enabled: 'false'} + }, + { + id: 'second', + url: 'https://second.example.com/plugins', + snapshots: {enabled: 'true'} + } + ] + }, + { + name: 'custom plugins before Central', + prioritizeCentral: false, + expectedRepositories: [ + { + id: 'first', + url: 'https://first.example.com/plugins', + snapshots: {enabled: 'false'} + }, + { + id: 'second', + url: 'https://second.example.com/plugins', + snapshots: {enabled: 'true'} + }, + { + id: 'central', + url: 'https://repo.maven.apache.org/maven2', + snapshots: {enabled: 'false'} + } + ] + } + ])( + 'generates plugin-only settings with $name', + ({prioritizeCentral, expectedRepositories}) => { + const xml = auth.generate( + [], + undefined, + repositorySettings([], true, prioritizeCentral, [ + { + id: 'first', + url: 'https://first.example.com/plugins', + snapshotsEnabled: false + }, + { + id: 'second', + url: 'https://second.example.com/plugins', + snapshotsEnabled: true + } + ]) + ); + + expect(parseXmlObject(xml)).toEqual( + expect.objectContaining({ + settings: expect.objectContaining({ + profiles: { + profile: { + id: 'setup-java-repositories', + pluginRepositories: { + pluginRepository: expectedRepositories + } + } + }, + activeProfiles: {activeProfile: 'setup-java-repositories'} + }) + }) + ); + } + ); + it('excludes Central from the generated repository profile when configured', () => { const parsed = parseXmlObject( auth.generate( @@ -506,6 +626,9 @@ describe('auth tests', () => { snapshots: {enabled: 'false'} } ]); + expect( + parsed.settings.profiles.profile.pluginRepositories.pluginRepository + ).toEqual(disabledCentralRepositoryXml); }); it('combines repository and GPG configuration in shared profile blocks', () => { @@ -543,6 +666,9 @@ describe('auth tests', () => { snapshots: {enabled: 'false'} } ] + }, + pluginRepositories: { + pluginRepository: disabledCentralRepositoryXml } }, { @@ -556,6 +682,161 @@ describe('auth tests', () => { ]); }); + it.each( + repositoryConfigurations.flatMap(configuration => + [ + {includeCentral: true, prioritizeCentral: true}, + {includeCentral: true, prioritizeCentral: false}, + {includeCentral: false, prioritizeCentral: true}, + {includeCentral: false, prioritizeCentral: false} + ].map(controls => ({...configuration, ...controls})) + ) + )( + 'generates $name repositories with includeCentral=$includeCentral and prioritizeCentral=$prioritizeCentral', + ({repositories, pluginRepositories, includeCentral, prioritizeCentral}) => { + const xml = auth.generate( + credentials('private', 'USERNAME', 'PASSWORD'), + undefined, + repositorySettings( + repositories, + includeCentral, + prioritizeCentral, + pluginRepositories + ) + ); + const expectedEntries = (configured: boolean) => { + const entries = includeCentral + ? configured + ? prioritizeCentral + ? [centralRepositoryXml, privateRepositoryXml] + : [privateRepositoryXml, centralRepositoryXml] + : [] + : configured + ? [privateRepositoryXml, disabledCentralRepositoryXml] + : [disabledCentralRepositoryXml]; + return entries.length === 1 ? entries[0] : entries; + }; + const includeRepositories = repositories.length > 0 || !includeCentral; + const includePluginRepositories = + pluginRepositories.length > 0 || !includeCentral; + + expect(parseXmlObject(xml)).toMatchObject({ + settings: { + profiles: { + profile: { + id: 'setup-java-repositories', + ...(includeRepositories + ? { + repositories: { + repository: expectedEntries(repositories.length > 0) + } + } + : {}), + ...(includePluginRepositories + ? { + pluginRepositories: { + pluginRepository: expectedEntries( + pluginRepositories.length > 0 + ) + } + } + : {}) + } + }, + activeProfiles: {activeProfile: 'setup-java-repositories'} + } + }); + if (!includeRepositories) { + expect(xml).not.toContain(''); + } + if (!includePluginRepositories) { + expect(xml).not.toContain(''); + } + } + ); + + it.each(repositoryConfigurations)( + 'handles explicit Central independently for $name repositories', + ({repositories, pluginRepositories}) => { + const central = { + id: 'central', + url: 'https://mirror.example.com/maven', + snapshotsEnabled: true + }; + const explicitCentralXml = { + id: central.id, + url: central.url, + snapshots: {enabled: 'true'} + }; + const xml = auth.generate( + [], + undefined, + repositorySettings( + repositories.length ? [central] : [], + false, + true, + pluginRepositories.length ? [central] : [] + ) + ); + + expect(parseXmlObject(xml)).toMatchObject({ + settings: { + profiles: { + profile: { + repositories: { + repository: repositories.length + ? explicitCentralXml + : disabledCentralRepositoryXml + }, + pluginRepositories: { + pluginRepository: pluginRepositories.length + ? explicitCentralXml + : disabledCentralRepositoryXml + } + } + } + } + }); + expect(xml.match(/central<\/id>/g)).toHaveLength(2); + } + ); + + it('escapes plugin repository values while preserving parsed semantics', () => { + const repository = { + id: `private&<>"'é`, + url: `https://repo.example.com:8443/a?x=1&y="'é`, + snapshotsEnabled: false + }; + const xml = auth.generate( + credentials(repository.id, 'USERNAME', 'PASSWORD'), + undefined, + repositorySettings([], true, true, [repository]) + ); + + expect(parseXmlObject(xml)).toMatchObject({ + settings: { + servers: {server: {id: repository.id}}, + profiles: { + profile: { + pluginRepositories: { + pluginRepository: [ + centralRepositoryXml, + { + id: repository.id, + url: repository.url, + snapshots: {enabled: 'false'} + } + ] + } + } + } + } + }); + expect(xml).toContain('private&<>'); + expect(xml).toContain('x=1&y=<value>'); + expect(xml).not.toContain(''); + }); + it('escapes repository values while preserving parsed semantics', () => { const repository = { id: `private&<>"'é`, @@ -601,81 +882,111 @@ 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([ + it.each(repositoryInputs)( + 'parses %s while preserving colons in URLs', + inputName => { + expect( + auth.parseMavenRepositories( + [ + '', + ' private : https://repo.example.com:8443/maven : TRUE ', + 'releases:https://repo.example.com/releases:false' + ], + true, + inputName + ) + ).toEqual([ + { + id: 'private', + url: 'https://repo.example.com:8443/maven', + snapshotsEnabled: true + }, + { + id: 'releases', + url: 'https://repo.example.com/releases', + snapshotsEnabled: false + } + ]); + } + ); + + it.each( + [ { - id: 'private', - url: 'https://repo.example.com:8443/maven', - snapshotsEnabled: true + entries: ['private'], + error: + 'Invalid mvn-repositories entry at line 1. Expected format: repository-id:repository-url:snapshots-enabled' }, { - id: 'releases', - url: 'https://repo.example.com/releases', - snapshotsEnabled: false + 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:'], + error: + 'Invalid mvn-repositories entry at line 1. repository-id, repository URL, and snapshots-enabled are required' + }, + { + entries: ['', 'private:https://repo.example.com:true', ' :url:false'], + error: + 'Invalid mvn-repositories entry at line 3. 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" } - ]); + ].flatMap(testCase => + repositoryInputs.map(inputName => ({ + ...testCase, + inputName, + error: testCase.error.replace('mvn-repositories', inputName) + })) + ) + )('rejects malformed $inputName: $entries', ({entries, error, inputName}) => { + expect(() => auth.parseMavenRepositories(entries, true, inputName)).toThrow( + error + ); }); - 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', () => { + it.each(repositoryInputs)('rejects duplicate IDs in %s', inputName => { expect(() => auth.parseMavenRepositories( [ 'private:https://first.example.com:false', 'private:https://second.example.com:true' ], - true + true, + inputName ) - ).toThrow("Duplicate repository-id 'private' in mvn-repositories input"); + ).toThrow(`Duplicate repository-id 'private' in ${inputName} input`); }); - it('reserves the Central repository id only when automatic Central inclusion is enabled', () => { - const central = 'central:https://custom.example.com/maven:false'; + it.each(repositoryInputs)( + 'reserves the Central ID in %s only when automatic inclusion is enabled', + inputName => { + 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 - } - ]); - }); + expect(() => + auth.parseMavenRepositories([central], true, inputName) + ).toThrow( + "Repository-id 'central' is reserved when mvn-repositories-include-central is enabled" + ); + expect(auth.parseMavenRepositories([central], false, inputName)).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( @@ -721,16 +1032,138 @@ describe('auth tests', () => { snapshotsEnabled: true } ], + pluginRepositories: [], includeCentral: false, prioritizeCentral: false }); }); it('does not read repository controls when no repositories are configured', () => { + ( + core.getInput as jest.MockedFunction + ).mockReturnValue('false'); expect(auth.getMavenRepositorySettings()).toBeUndefined(); expect(core.getInput).not.toHaveBeenCalled(); }); + it('ignores blank dependency and plugin repository inputs', () => { + ( + core.getMultilineInput as jest.MockedFunction< + typeof core.getMultilineInput + > + ).mockReturnValue(['', ' ', '\t']); + + expect(auth.getMavenRepositorySettings()).toBeUndefined(); + expect(core.getInput).not.toHaveBeenCalled(); + }); + + it.each(repositoryConfigurations)( + 'reads $name inputs with default Central controls and shared IDs', + ({repositories, pluginRepositories}) => { + ( + core.getInput as jest.MockedFunction + ).mockReturnValue(''); + ( + core.getMultilineInput as jest.MockedFunction< + typeof core.getMultilineInput + > + ).mockImplementation(name => { + const configured = + name === 'mvn-repositories' ? repositories : pluginRepositories; + return configured.map( + repository => + `${repository.id}:${repository.url}:${repository.snapshotsEnabled}` + ); + }); + + expect(auth.getMavenRepositorySettings()).toEqual({ + repositories, + pluginRepositories, + includeCentral: true, + prioritizeCentral: true + }); + } + ); + + it.each([true, false])( + 'configures plugin repositories with credentials, GPG, a custom path, and overwrite=%s', + async overwriteSettings => { + const directory = path.join(m2Dir, 'custom'); + const location = path.join(directory, MVN_SETTINGS_FILE); + fs.mkdirSync(directory, {recursive: true}); + fs.writeFileSync(location, 'EXISTING SETTINGS'); + ( + core.getInput as jest.MockedFunction + ).mockImplementation(name => { + const inputs: Record = { + 'settings-path': directory, + 'overwrite-settings': String(overwriteSettings), + 'gpg-passphrase-env-var': 'GPG_PASSPHRASE', + 'mvn-repositories-include-central': 'false' + }; + return inputs[name] ?? ''; + }); + ( + core.getMultilineInput as jest.MockedFunction< + typeof core.getMultilineInput + > + ).mockImplementation(name => { + const inputs: Record = { + 'mvn-server-credentials': ['private:USERNAME:PASSWORD'], + 'mvn-server-repository-origins': ['private:https://repo.example.com'], + 'mvn-plugin-repositories': [ + 'private:https://repo.example.com/maven:true' + ] + }; + return inputs[name] ?? []; + }); + + await auth.configureAuthentication(); + + const xml = fs.readFileSync(location, 'utf8'); + if (!overwriteSettings) { + expect(xml).toBe('EXISTING SETTINGS'); + return; + } + expect(parseXmlObject(xml)).toMatchObject({ + settings: { + servers: { + server: { + id: 'private', + username: '${env.USERNAME}', + password: '${env.PASSWORD}', + repositoryOrigins: { + repositoryOrigin: 'https://repo.example.com' + } + } + }, + profiles: { + profile: [ + { + id: 'setup-java-repositories', + repositories: {repository: disabledCentralRepositoryXml}, + pluginRepositories: { + pluginRepository: [ + privateRepositoryXml, + disabledCentralRepositoryXml + ] + } + }, + { + id: 'setup-java-gpg', + properties: {'gpg.passphraseEnvName': 'GPG_PASSPHRASE'} + } + ] + }, + activeProfiles: { + activeProfile: ['setup-java-repositories', 'setup-java-gpg'] + } + } + }); + expect(fs.existsSync(settingsFile)).toBe(false); + } + ); + it.each([ { entries: ['releases:RELEASES_USERNAME'], diff --git a/action.yml b/action.yml index 554d4980..eb32ec42 100644 --- a/action.yml +++ b/action.yml @@ -72,12 +72,15 @@ inputs: mvn-repositories: description: 'Multiline list of Maven dependency repositories in the format `repository-id:repository-url:snapshots-enabled`.' required: false + mvn-plugin-repositories: + description: 'Multiline list of Maven plugin 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.' + description: 'Include Maven Central for dependency and plugin repositories in the generated profile. When false, Central is disabled for both types unless that type explicitly declares ID `central`. Has no effect when neither repository list is configured.' required: false default: true mvn-repositories-prioritize-central: - description: 'Place Maven Central before custom dependency repositories. Has no effect when Maven Central is excluded.' + description: 'Place Maven Central before custom dependency and plugin repositories. Has no effect when Maven Central is excluded or neither repository list is configured.' required: false default: true settings-path: diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 77249968..03ab6fe1 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_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 */ +/* 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_PLUGIN_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'; @@ -31060,6 +31060,7 @@ const SIGNATURE_VERIFICATION_FAILURE_HELP = (/* unused pure expression or super 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_PLUGIN_REPOSITORIES = 'mvn-plugin-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'; diff --git a/dist/setup/81.index.js b/dist/setup/81.index.js index 9309d61d..39f98e37 100644 --- a/dist/setup/81.index.js +++ b/dist/setup/81.index.js @@ -85,7 +85,7 @@ function getMavenServerSettings() { 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)); + return addMavenServerRepositoryOrigins(servers, _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getMultilineInput */ .q3(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_SERVER_REPOSITORY_ORIGINS */ .Pt)); } // only exported for testing purposes function parseMavenServerCredentials(entries) { @@ -169,18 +169,21 @@ function addMavenServerRepositoryOrigins(servers, entries) { // 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())) { + const pluginEntries = _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getMultilineInput */ .q3(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_PLUGIN_REPOSITORIES */ .gR); + if (!entries.some(entry => entry.trim()) && + !pluginEntries.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), + pluginRepositories: parseMavenRepositories(pluginEntries, includeCentral, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_PLUGIN_REPOSITORIES */ .gR), 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) { +function parseMavenRepositories(entries, includeCentral, inputName = _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_REPOSITORIES */ .W2) { const repositories = []; const repositoryIds = new Set(); entries.forEach((entry, index) => { @@ -190,7 +193,7 @@ function parseMavenRepositories(entries, includeCentral) { 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`); + throw new Error(`Invalid ${inputName} 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(); @@ -199,13 +202,13 @@ function parseMavenRepositories(entries, includeCentral) { .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`); + throw new Error(`Invalid ${inputName} 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`); + throw new Error(`Invalid snapshots-enabled value '${snapshotsValue}' in ${inputName} entry at line ${index + 1}. Expected true or false`); } if (repositoryIds.has(id)) { - throw new Error(`Duplicate repository-id '${id}' in mvn-repositories input`); + throw new Error(`Duplicate repository-id '${id}' in ${inputName} 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`); @@ -219,6 +222,37 @@ function parseMavenRepositories(entries, includeCentral) { }); return repositories; } +function generateRepositorySection(customRepositories, settings, element) { + if (!customRepositories.length && settings.includeCentral) { + return []; + } + 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 = customRepositories.some(repository => repository.id === _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_CENTRAL_REPOSITORY_ID */ .xg); + const repositories = settings.includeCentral + ? settings.prioritizeCentral + ? [centralRepository, ...customRepositories] + : [...customRepositories, centralRepository] + : customCentralConfigured + ? customRepositories + : [...customRepositories, { ...centralRepository, releasesEnabled: false }]; + const container = element === 'repository' ? 'repositories' : 'pluginRepositories'; + const lines = [` <${container}>`]; + for (const repository of repositories) { + lines.push(` <${element}>`, ` ${(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(` `); + return lines; +} 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) @@ -258,33 +292,7 @@ function generate(servers, gpgPassphraseEnvVar, repositorySettings) { 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(' ', ' '); + lines.push(' ', ` ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MAVEN_REPOSITORIES_PROFILE_ID */ .hq}`, ...generateRepositorySection(repositorySettings.repositories, repositorySettings, 'repository'), ...generateRepositorySection(repositorySettings.pluginRepositories ?? [], repositorySettings, 'pluginRepository'), ' '); } 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)}`, ' ', ' '); diff --git a/dist/setup/index.js b/dist/setup/index.js index 1a178aaf..5c053664 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -31046,6 +31046,7 @@ module.exports = { /* 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 */ Pt: () => (/* binding */ INPUT_MVN_SERVER_REPOSITORY_ORIGINS), /* harmony export */ QM: () => (/* binding */ INPUT_JAVA_VERSION), /* harmony export */ RX: () => (/* binding */ INPUT_DEFAULT_GPG_PASSPHRASE), /* harmony export */ TS: () => (/* binding */ INPUT_OVERWRITE_SETTINGS), @@ -31060,7 +31061,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 */ gR: () => (/* binding */ INPUT_MVN_PLUGIN_REPOSITORIES), /* harmony export */ g_: () => (/* binding */ INPUT_DISTRIBUTION), /* harmony export */ gk: () => (/* binding */ INPUT_CACHE), /* harmony export */ hq: () => (/* binding */ MAVEN_REPOSITORIES_PROFILE_ID), @@ -31109,6 +31110,7 @@ const SIGNATURE_VERIFICATION_FAILURE_HELP = `If this is a legitimate vendor sign 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_PLUGIN_REPOSITORIES = 'mvn-plugin-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'; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 72a8ad96..dedca4ed 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -1003,14 +1003,92 @@ This configuration adds the following active profile to `settings.xml`: ``` -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. +Maven Central is included first by default. The Central controls apply to both +dependency and plugin repositories. Set +`mvn-repositories-prioritize-central: false` to place custom repositories first +within each configured list, or set `mvn-repositories-include-central: false` to +disable Central for both types. The generated profile overrides the dependency +and plugin Central repositories inherited from Maven's Super POM with releases +and snapshots disabled, even if only one custom repository list is supplied. +These controls have no effect when neither list contains a nonblank entry. + +When automatic Central inclusion is off, the ID `central` may instead be +declared explicitly in `mvn-repositories` or `mvn-plugin-repositories` to replace +it with a user-specified repository for that type. Declaring it in one list +does not enable Central in the other. Otherwise that ID is reserved to prevent +duplicate entries. Other repositories declared in your project's POM are not +disabled by these controls. + +### Resolving Maven plugins from custom repositories + +Maven resolves build plugins through ``, separately from +dependency ``. Use `mvn-plugin-repositories` to configure plugin +resolution with the same multiline `repository-id:repository-url:snapshots-enabled` +format. It can be supplied without `mvn-repositories`; dependency repositories +are not automatically used as plugin repositories. + +For example, configure the same private repository for dependencies and plugins +and disable inherited Maven Central for both: + +```yaml +steps: + - uses: actions/checkout@v7 + - 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/releases:false + mvn-plugin-repositories: | + private:https://maven.example.com/releases:false + mvn-repositories-include-central: false + - run: mvn --batch-mode verify + env: + PRIVATE_REPOSITORY_USERNAME: ${{ secrets.PRIVATE_REPOSITORY_USERNAME }} + PRIVATE_REPOSITORY_TOKEN: ${{ secrets.PRIVATE_REPOSITORY_TOKEN }} +``` + +The matching ID `private` selects the same server credentials for both types. +IDs must be unique within each list, but can be shared across the two lists. +Public repositories do not require a matching credential entry. + +Alongside dependency repositories, the active `setup-java-repositories` profile +contains: + +```xml + + + private + https://maven.example.com/releases + + false + + + + central + https://repo.maven.apache.org/maven2 + + false + + + false + + + +``` + +Omitting a Central override would leave the Super POM's plugin repository +enabled. In particular, `mvn-repositories-include-central: false` now disables +inherited plugin Central even for existing workflows that only configure +`mvn-repositories`. To download plugins in such workflows, configure a plugin +repository or explicitly declare `central` in `mvn-plugin-repositories`. + +With Central included, it is placed before custom plugin repositories by +default; `mvn-repositories-prioritize-central: false` places it after them. +All repository generation respects `settings-path` and `overwrite-settings`; +an existing settings file is left untouched when `overwrite-settings: false`. ### GPG diff --git a/src/auth.ts b/src/auth.ts index 096e1fb5..91480f76 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -25,6 +25,7 @@ export interface MavenRepository { export interface MavenRepositorySettings { repositories: MavenRepository[]; + pluginRepositories?: MavenRepository[]; includeCentral: boolean; prioritizeCentral: boolean; } @@ -245,7 +246,13 @@ export function addMavenServerRepositoryOrigins( export function getMavenRepositorySettings(): MavenRepositorySettings | undefined { const entries = core.getMultilineInput(constants.INPUT_MVN_REPOSITORIES); - if (!entries.some(entry => entry.trim())) { + const pluginEntries = core.getMultilineInput( + constants.INPUT_MVN_PLUGIN_REPOSITORIES + ); + if ( + !entries.some(entry => entry.trim()) && + !pluginEntries.some(entry => entry.trim()) + ) { return undefined; } @@ -255,6 +262,11 @@ export function getMavenRepositorySettings(): ); return { repositories: parseMavenRepositories(entries, includeCentral), + pluginRepositories: parseMavenRepositories( + pluginEntries, + includeCentral, + constants.INPUT_MVN_PLUGIN_REPOSITORIES + ), includeCentral, prioritizeCentral: getBooleanInput( constants.INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL, @@ -266,7 +278,8 @@ export function getMavenRepositorySettings(): // only exported for testing purposes export function parseMavenRepositories( entries: string[], - includeCentral: boolean + includeCentral: boolean, + inputName = constants.INPUT_MVN_REPOSITORIES ): MavenRepository[] { const repositories: MavenRepository[] = []; const repositoryIds = new Set(); @@ -280,7 +293,7 @@ export function parseMavenRepositories( 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` + `Invalid ${inputName} entry at line ${index + 1}. Expected format: repository-id:repository-url:snapshots-enabled` ); } @@ -292,18 +305,16 @@ export function parseMavenRepositories( .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` + `Invalid ${inputName} 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` + `Invalid snapshots-enabled value '${snapshotsValue}' in ${inputName} entry at line ${index + 1}. Expected true or false` ); } if (repositoryIds.has(id)) { - throw new Error( - `Duplicate repository-id '${id}' in mvn-repositories input` - ); + throw new Error(`Duplicate repository-id '${id}' in ${inputName} input`); } if (includeCentral && id === constants.MAVEN_CENTRAL_REPOSITORY_ID) { throw new Error( @@ -322,6 +333,56 @@ export function parseMavenRepositories( return repositories; } +function generateRepositorySection( + customRepositories: MavenRepository[], + settings: MavenRepositorySettings, + element: 'repository' | 'pluginRepository' +): string[] { + if (!customRepositories.length && settings.includeCentral) { + return []; + } + + const centralRepository: MavenRepository = { + id: constants.MAVEN_CENTRAL_REPOSITORY_ID, + url: constants.MAVEN_CENTRAL_REPOSITORY_URL, + snapshotsEnabled: false + }; + const customCentralConfigured = customRepositories.some( + repository => repository.id === constants.MAVEN_CENTRAL_REPOSITORY_ID + ); + const repositories = settings.includeCentral + ? settings.prioritizeCentral + ? [centralRepository, ...customRepositories] + : [...customRepositories, centralRepository] + : customCentralConfigured + ? customRepositories + : [...customRepositories, {...centralRepository, releasesEnabled: false}]; + const container = + element === 'repository' ? 'repositories' : 'pluginRepositories'; + const lines = [` <${container}>`]; + + for (const repository of repositories) { + lines.push( + ` <${element}>`, + ` ${escapeXmlText(repository.id)}`, + ` ${escapeXmlText(repository.url)}`, + ...(repository.releasesEnabled === undefined + ? [] + : [ + ' ', + ` ${repository.releasesEnabled}`, + ' ' + ]), + ' ', + ` ${repository.snapshotsEnabled}`, + ' ', + ` ` + ); + } + lines.push(` `); + return lines; +} + export async function createAuthenticationSettings( servers: MavenServerCredentials[], settingsDirectory: string, @@ -389,49 +450,21 @@ export function generate( 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}`, - ' ' + ...generateRepositorySection( + repositorySettings.repositories, + repositorySettings, + 'repository' + ), + ...generateRepositorySection( + repositorySettings.pluginRepositories ?? [], + repositorySettings, + 'pluginRepository' + ), + ' ' ); - 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( diff --git a/src/constants.ts b/src/constants.ts index 6a213a59..bdba9224 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,6 +19,7 @@ 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_PLUGIN_REPOSITORIES = 'mvn-plugin-repositories'; export const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL = 'mvn-repositories-include-central'; export const INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL =