diff --git a/.github/workflows/ci-integ-test.yml b/.github/workflows/ci-integ-test.yml index 8d269c60..6f37d384 100644 --- a/.github/workflows/ci-integ-test.yml +++ b/.github/workflows/ci-integ-test.yml @@ -47,6 +47,7 @@ jobs: other-integ-tests: permissions: contents: write + actions: read needs: caching-integ-tests uses: ./.github/workflows/suite-integ-test-other.yml concurrency: diff --git a/.github/workflows/integ-test-gradle-support-status.yml b/.github/workflows/integ-test-gradle-support-status.yml index 463ab274..49876d40 100644 --- a/.github/workflows/integ-test-gradle-support-status.yml +++ b/.github/workflows/integ-test-gradle-support-status.yml @@ -18,14 +18,27 @@ permissions: contents: read jobs: - report-version-status: + report-outdated-versions: runs-on: ubuntu-latest + outputs: + maintenance-version: ${{ steps.maintenance-version.outputs.version }} steps: - name: Checkout sources uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize integ-test uses: ./.github/actions/init-integ-test + - name: Determine the maintenance-only Gradle version + id: maintenance-version + run: | + version=$(jq -r ' + [.[].version | select(test("^[0-9]+\\.[0-9]+(\\.[0-9]+)?$"))] as $finals + | ($finals | map(split(".")[0] | tonumber) | max) as $latest + | [$finals[] | select((split(".")[0] | tonumber) == ($latest - 1))] | first + ' sources/src/wrapper-validation/wrapper-checksums.json) + echo "Newest release in the maintenance-only line: $version" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Setup Gradle with an end-of-life version uses: ./setup-gradle with: @@ -36,25 +49,46 @@ jobs: working-directory: .github/workflow-samples/no-wrapper run: gradle help "-DgradleVersionCheck=7.6.4" - - name: Setup Gradle with the current version + - name: Setup Gradle with the maintenance-only version uses: ./setup-gradle with: - gradle-version: current - - name: Build with the current version - id: build-current + gradle-version: ${{ steps.maintenance-version.outputs.version }} + - name: Build with the maintenance-only version working-directory: .github/workflow-samples/no-wrapper - run: gradle help + run: gradle help "-DgradleVersionCheck=${{ steps.maintenance-version.outputs.version }}" - - name: Check the end-of-life version was reported as 'eol' - if: ${{ steps.build-end-of-life.outputs.gradle-version-status != 'eol' }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - core.setFailed("Expected Gradle 7.6.4 to be reported as 'eol', but was '${{ steps.build-end-of-life.outputs.gradle-version-status }}'") + verify-annotations: + needs: report-outdated-versions + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + steps: + - name: Check the support-status annotations were emitted + env: + GH_TOKEN: ${{ github.token }} + MAINTENANCE_VERSION: ${{ needs.report-outdated-versions.outputs.maintenance-version }} + run: | + set -euo pipefail - - name: Check the current version was reported as 'active' - if: ${{ steps.build-current.outputs.gradle-version-status != 'active' }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - core.setFailed("Expected the current Gradle version to be reported as 'active', but was '${{ steps.build-current.outputs.gradle-version-status }}'") + job_id=$(gh api \ + "/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs" \ + --paginate --jq '.jobs[] | select(.name | endswith("report-outdated-versions")) | .id') + if [ -z "$job_id" ]; then + echo "Could not find the 'report-outdated-versions' Job in this workflow run" + exit 1 + fi + gh api --allow-escape-sequences "/repos/${GITHUB_REPOSITORY}/actions/jobs/${job_id}/logs" > job.log + + # The runner renders '::warning::'/'::notice::' commands into the log as '##[warning]'/'##[notice]'. + assert_logged() { + if ! grep -qF -- "$1" job.log; then + echo "Expected the Job log to contain: $1" + echo "Annotations found instead:" + grep -E '##\[(warning|notice)\]' job.log || echo "(none)" + exit 1 + fi + } + + assert_logged "##[warning]Gradle 7.6.4 has reached end-of-life" + assert_logged "##[notice]Gradle ${MAINTENANCE_VERSION} is in maintenance-only support" diff --git a/.github/workflows/suite-integ-test-other.yml b/.github/workflows/suite-integ-test-other.yml index 3d7894a7..6d656c83 100644 --- a/.github/workflows/suite-integ-test-other.yml +++ b/.github/workflows/suite-integ-test-other.yml @@ -29,6 +29,9 @@ jobs: DEVELOCITY_ACCESS_KEY: ${{ secrets.DV_SOLUTIONS_ACCESS_KEY }} gradle-support-status: + permissions: + contents: read + actions: read uses: ./.github/workflows/integ-test-gradle-support-status.yml with: skip-dist: ${{ inputs.skip-dist }} diff --git a/dependency-submission/action-types.yml b/dependency-submission/action-types.yml index 5e757642..54b43c2e 100644 --- a/dependency-submission/action-types.yml +++ b/dependency-submission/action-types.yml @@ -156,6 +156,3 @@ outputs: gradle-version: type: string - - gradle-version-status: - type: string diff --git a/dependency-submission/action.yml b/dependency-submission/action.yml index 7f3ccadc..73c87630 100644 --- a/dependency-submission/action.yml +++ b/dependency-submission/action.yml @@ -229,11 +229,7 @@ outputs: description: Path to the GitHub Dependency Graph snapshot file generated by a Gradle build. Note that this output applies to a Step executing Gradle, not to the `setup-gradle` Step itself. gradle-version: description: Version of Gradle that was setup by the action - gradle-version-status: - description: | - Support status of the Gradle version that executed the build, following - https://docs.gradle.org/current/userguide/feature_lifecycle.html: - 'active', 'maintenance' or 'eol' + runs: using: 'node24' main: '../dist/dependency-submission/main/index.js' diff --git a/setup-gradle/action-types.yml b/setup-gradle/action-types.yml index 504e4a72..e4c99831 100644 --- a/setup-gradle/action-types.yml +++ b/setup-gradle/action-types.yml @@ -179,6 +179,3 @@ outputs: gradle-version: type: string - - gradle-version-status: - type: string diff --git a/setup-gradle/action.yml b/setup-gradle/action.yml index 25a38517..6130fa36 100644 --- a/setup-gradle/action.yml +++ b/setup-gradle/action.yml @@ -246,11 +246,7 @@ outputs: description: Path to the GitHub Dependency Graph snapshot file generated by a Gradle build. Note that this output applies to a Step executing Gradle, not to the `setup-gradle` Step itself. gradle-version: description: Version of Gradle that was setup by the action - gradle-version-status: - description: | - Support status of the Gradle version that executed the build, following - https://docs.gradle.org/current/userguide/feature_lifecycle.html: - 'active', 'maintenance' or 'eol' + runs: using: 'node24' main: '../dist/setup-gradle/main/index.js' diff --git a/sources/src/build-results.ts b/sources/src/build-results.ts index 363a2268..401e7cba 100644 --- a/sources/src/build-results.ts +++ b/sources/src/build-results.ts @@ -6,7 +6,6 @@ export interface BuildResult { get rootProjectDir(): string get requestedTasks(): string get gradleVersion(): string - get versionStatus(): string | undefined get gradleHomeDir(): string get buildFailed(): boolean get configCacheHit(): boolean diff --git a/sources/src/gradle-support-status.ts b/sources/src/gradle-support-status.ts index 36a6b0ce..b8322a82 100644 --- a/sources/src/gradle-support-status.ts +++ b/sources/src/gradle-support-status.ts @@ -1,13 +1,13 @@ import * as core from '@actions/core' -import {BuildResult} from './build-results' import {GradleVersion} from './execution/gradle-version' import wrapperChecksums from './wrapper-validation/wrapper-checksums.json' -const FEATURE_LIFECYCLE_DOC = 'https://docs.gradle.org/current/userguide/feature_lifecycle.html' +const FEATURE_LIFECYCLE_DOC = 'https://docs.gradle.org/current/userguide/feature_lifecycle.html#eol_support' const LATEST_RELEASED_MAJOR = determineLatestReleasedMajor(wrapperChecksums.map(entry => entry.version)) -const LATEST_MAJOR_VERSION_ENV = 'GRADLE_ACTIONS_LATEST_GRADLE_MAJOR' + +export type SupportStatus = 'active' | 'maintenance' | 'eol' export function determineLatestReleasedMajor(versions: string[]): number | undefined { const releasedMajors = versions @@ -17,27 +17,25 @@ export function determineLatestReleasedMajor(versions: string[]): number | undef return releasedMajors.length > 0 ? Math.max(...releasedMajors) : undefined } -export function exportLatestReleasedMajor(): void { - if (LATEST_RELEASED_MAJOR !== undefined) { - core.exportVariable(LATEST_MAJOR_VERSION_ENV, LATEST_RELEASED_MAJOR) +export function getSupportStatus(version: GradleVersion, latestMajor: number): SupportStatus { + switch (Math.max(0, latestMajor - version.major)) { + case 0: + return 'active' + case 1: + return 'maintenance' + default: + return 'eol' } } -export function reportSupportStatus(buildResults: BuildResult[]): void { +export function reportSupportStatus(gradleVersions: string[]): void { if (LATEST_RELEASED_MAJOR === undefined) { return } - const statusByVersion = new Map() - for (const {gradleVersion, versionStatus} of buildResults) { - if (versionStatus) { - statusByVersion.set(gradleVersion, versionStatus) - } - } - - for (const [gradleVersion, status] of statusByVersion) { + for (const gradleVersion of new Set(gradleVersions)) { const version = new GradleVersion(gradleVersion) - switch (status) { + switch (getSupportStatus(version, LATEST_RELEASED_MAJOR)) { case 'eol': core.warning(eolMessage(version, LATEST_RELEASED_MAJOR), {title: 'Gradle version at end-of-life'}) break diff --git a/sources/src/resources/init-scripts/gradle-actions.build-result-capture-service.plugin.groovy b/sources/src/resources/init-scripts/gradle-actions.build-result-capture-service.plugin.groovy index 6cb95009..47587a71 100644 --- a/sources/src/resources/init-scripts/gradle-actions.build-result-capture-service.plugin.groovy +++ b/sources/src/resources/init-scripts/gradle-actions.build-result-capture-service.plugin.groovy @@ -15,7 +15,6 @@ settingsEvaluated { settings -> spec.getParameters().getRequestedTasks().set(gradle.startParameter.taskNames.join(" ")) spec.getParameters().getGradleHomeDir().set(gradle.gradleHomeDir.absolutePath) spec.getParameters().getInvocationId().set(gradle.ext.invocationId) - spec.getParameters().getVersionStatus().set(gradle.ext.gradleVersionStatus ?: "") }) gradle.services.get(BuildEventListenerRegistryInternal).onOperationCompletion(projectTracker) @@ -31,7 +30,6 @@ abstract class BuildResultsRecorder implements BuildService getRequestedTasks() Property getGradleHomeDir() Property getInvocationId() - Property getVersionStatus() } void started(BuildOperationDescriptor buildOperation, OperationStartEvent startEvent) {} @@ -51,16 +49,6 @@ abstract class BuildResultsRecorder implements BuildService= GradleVersion.version("3.0") def atLeastGradle6 = version >= GradleVersion.version("6.0") def atLeastGradle7 = version >= GradleVersion.version("7.0") @@ -87,46 +84,13 @@ def captureUsingBuildService(invocationId) { apply from: 'gradle-actions.build-result-capture-service.plugin.groovy' } -String supportStatusOf(version, String latestGradleMajor) { - def latestMajor = latestGradleMajor?.find(/^\d+$/)?.toInteger() - def currentMajor = version.version.find(/^\d+/)?.toInteger() - if (!latestMajor || !currentMajor) { - return null - } - - switch (Math.max(0, latestMajor - currentMajor)) { - case 0: - return "active" - case 1: - return "maintenance" - default: - return "eol" - } -} - -void setStepOutput(String name, String value) { - def githubOutput = System.getenv("GITHUB_OUTPUT") - if (githubOutput) { - new File(githubOutput) << "${name}=${value}\n" - } else { - // Retained for compatibility with older GHES versions - println("::set-output name=${name}::${value}") - } -} - void captureUsingBuildFinished(gradle, String invocationId, ResultsWriter resultsWriter) { gradle.buildFinished { result -> - def versionStatus = gradle.ext.gradleVersionStatus - if (versionStatus) { - setStepOutput("gradle-version-status", versionStatus) - } - def buildResults = [ rootProjectName: rootProject.name, rootProjectDir: rootProject.projectDir.absolutePath, requestedTasks: gradle.startParameter.taskNames.join(" "), gradleVersion: GradleVersion.current().version, - versionStatus: versionStatus, gradleHomeDir: gradle.gradleHomeDir.absolutePath, buildFailed: result.failure != null, configCacheHit: false @@ -146,7 +110,13 @@ void captureUsingBuildScanPublished(buildScanExtension, String invocationId, Res ] resultsWriter.writeToResultsFile("build-scans", invocationId, scanResults) - setStepOutput("build-scan-url", buildScan.buildScanUri.toString()) + def githubOutput = System.getenv("GITHUB_OUTPUT") + if (githubOutput) { + new File(githubOutput) << "build-scan-url=${buildScan.buildScanUri}\n" + } else { + // Retained for compatibility with older GHES versions + println("::set-output name=build-scan-url::${buildScan.buildScanUri}") + } } onError { error -> diff --git a/sources/src/setup-gradle.ts b/sources/src/setup-gradle.ts index e929dc15..5f56bac3 100644 --- a/sources/src/setup-gradle.ts +++ b/sources/src/setup-gradle.ts @@ -19,7 +19,7 @@ import { } from './configuration' import * as wrapperValidator from './wrapper-validation/wrapper-validator' import {initializeGradleUserHome} from './gradle-user-home' -import {exportLatestReleasedMajor, reportSupportStatus} from './gradle-support-status' +import {reportSupportStatus} from './gradle-support-status' const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED' const GRADLE_USER_HOME = 'GRADLE_USER_HOME' @@ -47,7 +47,6 @@ export async function setup( core.saveState(GRADLE_USER_HOME, gradleUserHome) initializeGradleUserHome(userHome, gradleUserHome, cacheConfig.getCacheEncryptionKey()) - exportLatestReleasedMajor() // Exchange the long-lived access key(s) for short-lived tokens, resolving the token for the // configured Develocity server and retaining it for the post-action (save) step. @@ -91,7 +90,7 @@ export async function complete( markBuildResultsProcessed() - reportSupportStatus(buildResults) + reportSupportStatus(buildResults.map(result => result.gradleVersion)) core.info('Completed post-action step') diff --git a/sources/test/init-scripts/src/test/groovy/com/gradle/gradlebuildaction/TestBuildResultRecorder.groovy b/sources/test/init-scripts/src/test/groovy/com/gradle/gradlebuildaction/TestBuildResultRecorder.groovy index 6fb4f026..2cca2095 100644 --- a/sources/test/init-scripts/src/test/groovy/com/gradle/gradlebuildaction/TestBuildResultRecorder.groovy +++ b/sources/test/init-scripts/src/test/groovy/com/gradle/gradlebuildaction/TestBuildResultRecorder.groovy @@ -6,7 +6,6 @@ import static org.junit.Assume.assumeTrue class TestBuildResultRecorder extends BaseInitScriptTest { def initScript = 'gradle-actions.build-result-capture.init.gradle' - String latestGradleMajor def "produces build results file for build with #testGradleVersion"() { assumeTrue testGradleVersion.compatibleWithCurrentJvm @@ -270,63 +269,6 @@ task expectFailure { testGradleVersion << SETTINGS_PLUGIN_VERSIONS } - def "captures version status '#expectedStatus' when the latest released major is #latestMajor"() { - assumeTrue GRADLE_8_X.compatibleWithCurrentJvm - - when: - latestGradleMajor = latestMajor - run(GRADLE_8_X.gradleVersion) - - then: - assertVersionStatus(expectedStatus) - - where: - latestMajor | expectedStatus - '7' | 'active' - '8' | 'active' - '9' | 'maintenance' - '10' | 'eol' - '13' | 'eol' - } - - def "captures version status for #testGradleVersion"() { - assumeTrue testGradleVersion.compatibleWithCurrentJvm - - when: - latestGradleMajor = '9' - run(testGradleVersion.gradleVersion) - - then: - assertVersionStatus(expectedStatus) - - where: - testGradleVersion | expectedStatus - GRADLE_6_X | 'eol' - GRADLE_7_X | 'eol' - GRADLE_8_X | 'maintenance' - } - - def "captures no version status when the action did not supply the latest released major"() { - assumeTrue GRADLE_8_X.compatibleWithCurrentJvm - - when: - run(GRADLE_8_X.gradleVersion) - - then: - assertVersionStatus(null) - } - - def "captures no version status when the latest released major is not a number"() { - assumeTrue GRADLE_8_X.compatibleWithCurrentJvm - - when: - latestGradleMajor = 'not-a-number' - run(GRADLE_8_X.gradleVersion) - - then: - assertVersionStatus(null) - } - def run(def args = ['help'], def gradleVersion) { return run(args, initScript, gradleVersion, jvmArgs, envVars) } @@ -336,26 +278,17 @@ task expectFailure { } def getJvmArgs() { - def jvmArgs = [ + [ "-DRUNNER_TEMP=${testProjectDir.absolutePath}".toString(), "-DGITHUB_ACTION=github-step-id".toString() ] - if (latestGradleMajor != null) { - jvmArgs << "-DGRADLE_ACTIONS_LATEST_GRADLE_MAJOR=${latestGradleMajor}".toString() - } - jvmArgs } def getEnvVars() { - def envVars = [ + [ RUNNER_TEMP: testProjectDir.absolutePath, - GITHUB_ACTION: 'github-step-id', - GITHUB_OUTPUT: githubOutputFile.absolutePath + GITHUB_ACTION: 'github-step-id' ] - if (latestGradleMajor != null) { - envVars.GRADLE_ACTIONS_LATEST_GRADLE_MAJOR = latestGradleMajor - } - envVars } void assertResults(String task, TestGradleVersion testGradleVersion, boolean hasFailure, boolean configCacheHit = false) { @@ -375,24 +308,6 @@ task expectFailure { assert scanResults['buildScanFailed'] == scanUploadFailed } - void assertVersionStatus(String expectedStatus) { - def results = new JsonSlurper().parse(buildResultFile) - assert results['versionStatus'] == expectedStatus - if (expectedStatus == null) { - assert !githubOutput.contains('gradle-version-status') - } else { - assert githubOutput.contains("gradle-version-status=${expectedStatus}") - } - } - - private File getGithubOutputFile() { - new File(testProjectDir, 'github-output') - } - - private String getGithubOutput() { - githubOutputFile.exists() ? githubOutputFile.text : '' - } - private File getBuildResultFile() { def buildResultsDir = new File(testProjectDir, '.gradle-actions/build-results') assert buildResultsDir.directory diff --git a/sources/test/jest/gradle-support-status.test.ts b/sources/test/jest/gradle-support-status.test.ts index 03f864e6..76c8e821 100644 --- a/sources/test/jest/gradle-support-status.test.ts +++ b/sources/test/jest/gradle-support-status.test.ts @@ -1,23 +1,22 @@ import {beforeEach, describe, expect, it, jest} from '@jest/globals' -import {BuildResult} from '../../src/build-results' - // Mock @actions/core const mockWarning = jest.fn<(message: string, properties?: {title?: string}) => void>() const mockNotice = jest.fn<(message: string, properties?: {title?: string}) => void>() -const mockExportVariable = jest.fn<(name: string, value: string | number) => void>() jest.unstable_mockModule('@actions/core', () => ({ warning: mockWarning, - notice: mockNotice, - exportVariable: mockExportVariable + notice: mockNotice })) -const {determineLatestReleasedMajor, exportLatestReleasedMajor, reportSupportStatus} = +const {determineLatestReleasedMajor, getSupportStatus, reportSupportStatus} = await import('../../src/gradle-support-status') +const {GradleVersion} = await import('../../src/execution/gradle-version') +import wrapperChecksums from '../../src/wrapper-validation/wrapper-checksums.json' -function build(gradleVersion: string, versionStatus?: string): BuildResult { - return {gradleVersion, versionStatus} as BuildResult -} +const latestReleasedMajor = determineLatestReleasedMajor(wrapperChecksums.map(entry => entry.version)) +const MAINTENANCE_VERSION = `${(latestReleasedMajor ?? 0) - 1}.0` + +const DOC = 'https://docs.gradle.org/current/userguide/feature_lifecycle.html#eol_support' describe('determineLatestReleasedMajor', () => { it('ignores pre-releases and snapshots of an unreleased major', () => { @@ -46,11 +45,17 @@ describe('determineLatestReleasedMajor', () => { }) }) -describe('exportLatestReleasedMajor', () => { - it('exports the boundary the init script needs to classify the running version', () => { - exportLatestReleasedMajor() +describe('getSupportStatus', () => { + it.each(['10.0.0', '10.4.2', '11.0.0-milestone-1', '12.0.0'])('treats %s as active', version => { + expect(getSupportStatus(new GradleVersion(version), 10)).toBe('active') + }) - expect(mockExportVariable).toHaveBeenCalledWith('GRADLE_ACTIONS_LATEST_GRADLE_MAJOR', expect.any(Number)) + it.each(['9.0.0', '9.6.1', '9.7.0-rc-2'])('treats %s as maintenance-only', version => { + expect(getSupportStatus(new GradleVersion(version), 10)).toBe('maintenance') + }) + + it.each(['8.14', '8.0.2', '7.6.4', '4.10.3', '1.0'])('treats %s as end-of-life', version => { + expect(getSupportStatus(new GradleVersion(version), 10)).toBe('eol') }) }) @@ -59,43 +64,38 @@ describe('reportSupportStatus', () => { jest.clearAllMocks() }) - it('warns about a version recorded as end-of-life', () => { - reportSupportStatus([build('7.6.4', 'eol')]) + it('warns about an end-of-life version', () => { + reportSupportStatus(['7.6.4']) expect(mockNotice).not.toHaveBeenCalled() expect(mockWarning).toHaveBeenCalledTimes(1) const [message, properties] = mockWarning.mock.calls[0] expect(message).toContain('Gradle 7.6.4 has reached end-of-life') - expect(message).toContain('the 7.x release line') + expect(message).toContain('the 7.x release line no longer receives bug fixes or security fixes') + expect(message).toContain(DOC) expect(properties?.title).toBe('Gradle version at end-of-life') }) - it('notices a version recorded as maintenance-only', () => { - reportSupportStatus([build('8.14', 'maintenance')]) + it('notices a maintenance-only version', () => { + reportSupportStatus([MAINTENANCE_VERSION]) expect(mockWarning).not.toHaveBeenCalled() expect(mockNotice).toHaveBeenCalledTimes(1) const [message, properties] = mockNotice.mock.calls[0] - expect(message).toContain('Gradle 8.14 is in maintenance-only support') + expect(message).toContain(`Gradle ${MAINTENANCE_VERSION} is in maintenance-only support`) + expect(message).toContain('receives critical bug fixes and security fixes only') expect(properties?.title).toBe('Gradle version in maintenance') }) - it('says nothing about a version recorded as active', () => { - reportSupportStatus([build('9.6.1', 'active')]) - - expect(mockWarning).not.toHaveBeenCalled() - expect(mockNotice).not.toHaveBeenCalled() - }) - - it('says nothing when the init script recorded no status', () => { - reportSupportStatus([build('7.6.4')]) + it('says nothing about a version in the latest release line', () => { + reportSupportStatus(['999.0.0']) expect(mockWarning).not.toHaveBeenCalled() expect(mockNotice).not.toHaveBeenCalled() }) it('annotates each distinct version once', () => { - reportSupportStatus([build('7.6.4', 'eol'), build('4.10.3', 'eol'), build('7.6.4', 'eol')]) + reportSupportStatus(['7.6.4', '4.10.3', '7.6.4']) expect(mockWarning).toHaveBeenCalledTimes(2) })