diff --git a/.github/workflows/integ-test-cache-cleanup.yml b/.github/workflows/integ-test-cache-cleanup.yml index c6d4405..060547d 100644 --- a/.github/workflows/integ-test-cache-cleanup.yml +++ b/.github/workflows/integ-test-cache-cleanup.yml @@ -35,7 +35,7 @@ jobs: cache-read-only: false # For testing, allow writing cache entries on non-default branches - name: Build with 3.1 working-directory: sources/test/jest/resources/cache-cleanup - run: gradle --no-daemon --build-cache -Dcommons_math3_version="3.1" build + run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1" build # Second build will use the cache from the first build, but cleanup should remove unused artifacts assemble-build: @@ -58,7 +58,7 @@ jobs: gradle-home-cache-cleanup: true - name: Build with 3.1.1 working-directory: sources/test/jest/resources/cache-cleanup - run: gradle --no-daemon --build-cache -Dcommons_math3_version="3.1.1" build + run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1.1" build check-clean-cache: needs: assemble-build @@ -78,7 +78,9 @@ jobs: with: cache-read-only: true - name: Report Gradle User Home - run: du -hc ~/.gradle/caches/modules-2 + run: | + du -hc ~/.gradle/caches/modules-2 + du -hc ~/.gradle/wrapper/dists - name: Verify cleaned cache shell: bash run: | @@ -90,3 +92,7 @@ jobs: echo "::error ::Should NOT find commons-math3 3.1 in cache" exit 1 fi + if [ ! -e ~/.gradle/wrapper/dists/gradle-8.0.2-bin ]; then + echo "::error ::Should find gradle-8.0.2 in wrapper/dists" + exit 1 + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c3313d..2fed3d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,6 @@ Example running a single job: `./build act -W .github/workflows/integ-test-caching-config.yml -j cache-disabled-pre-existing-gradle-home` Known issues: -- `integ-test-cache-cleanup.yml` fails because `gradle` is not installed on the runner. Should be fixed by #33. - `integ-test-detect-java-toolchains.yml` fails when running on a `linux/amd64` container, since the expected pre-installed JDKs are not present. Should be fixed by #89. - `act` is not yet compatible with `actions/upload-artifact@v4` (or related toolkit functions) - See https://github.com/nektos/act/pull/2224 diff --git a/sources/src/caching/cache-cleaner.ts b/sources/src/caching/cache-cleaner.ts index 26c64a7..12e3be5 100644 --- a/sources/src/caching/cache-cleaner.ts +++ b/sources/src/caching/cache-cleaner.ts @@ -1,8 +1,7 @@ import * as core from '@actions/core' -import * as exec from '@actions/exec' -import * as glob from '@actions/glob' import fs from 'fs' import path from 'path' +import {provisionAndMaybeExecute} from '../execution/gradle' export class CacheCleaner { private readonly gradleUserHome: string @@ -13,25 +12,20 @@ export class CacheCleaner { this.tmpDir = tmpDir } - async prepare(): Promise { - // Reset the file-access journal so that files appear not to have been used recently - fs.rmSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true, force: true}) - fs.mkdirSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true}) - fs.writeFileSync( - path.resolve(this.gradleUserHome, 'caches/journal-1/file-access.properties'), - 'inceptionTimestamp=0' - ) - - // Set the modification time of all files to the past: this timestamp is used when there is no matching entry in the journal - await this.ageAllFiles() - - // Touch all 'gc' files so that cache cleanup won't run immediately. - await this.touchAllFiles('gc.properties') + async prepare(): Promise { + // Save the current timestamp + const timestamp = Date.now().toString() + core.saveState('clean-timestamp', timestamp) + return timestamp } async forceCleanup(): Promise { - // Age all 'gc' files so that cache cleanup will run immediately. - await this.ageAllFiles('gc.properties') + const cleanTimestamp = core.getState('clean-timestamp') + await this.forceCleanupFilesOlderThan(cleanTimestamp) + } + + async forceCleanupFilesOlderThan(cleanTimestamp: string): Promise { + core.info(`Cleaning up caches before ${cleanTimestamp}`) // Run a dummy Gradle build to trigger cache cleanup const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project') @@ -40,30 +34,37 @@ export class CacheCleaner { path.resolve(cleanupProjectDir, 'settings.gradle'), 'rootProject.name = "dummy-cleanup-project"' ) + fs.writeFileSync( + path.resolve(cleanupProjectDir, 'init.gradle'), + ` + beforeSettings { settings -> + def cleanupTime = ${cleanTimestamp} + + settings.caches { + cleanup = Cleanup.ALWAYS + + releasedWrappers.removeUnusedEntriesOlderThan.set(cleanupTime) + snapshotWrappers.removeUnusedEntriesOlderThan.set(cleanupTime) + downloadedResources.removeUnusedEntriesOlderThan.set(cleanupTime) + createdResources.removeUnusedEntriesOlderThan.set(cleanupTime) + buildCache.removeUnusedEntriesOlderThan.set(cleanupTime) + } + } + ` + ) fs.writeFileSync(path.resolve(cleanupProjectDir, 'build.gradle'), 'task("noop") {}') - const gradleCommand = `gradle -g ${this.gradleUserHome} --no-daemon --build-cache --no-scan --quiet -DGITHUB_DEPENDENCY_GRAPH_ENABLED=false noop` - await exec.exec(gradleCommand, [], { - cwd: cleanupProjectDir - }) - } - - private async ageAllFiles(fileName = '*'): Promise { - core.debug(`Aging all files in Gradle User Home with name ${fileName}`) - await this.setUtimes(`${this.gradleUserHome}/**/${fileName}`, new Date(0)) - } - - private async touchAllFiles(fileName = '*'): Promise { - core.debug(`Touching all files in Gradle User Home with name ${fileName}`) - await this.setUtimes(`${this.gradleUserHome}/**/${fileName}`, new Date()) - } - - private async setUtimes(pattern: string, timestamp: Date): Promise { - const globber = await glob.create(pattern, { - implicitDescendants: false - }) - for await (const file of globber.globGenerator()) { - fs.utimesSync(file, timestamp, timestamp) - } + await provisionAndMaybeExecute('current', cleanupProjectDir, [ + '-g', + this.gradleUserHome, + '-I', + 'init.gradle', + '--info', + '--no-daemon', + '--no-scan', + '--build-cache', + '-DGITHUB_DEPENDENCY_GRAPH_ENABLED=false', + 'noop' + ]) } } diff --git a/sources/test/jest/cache-cleanup.test.ts b/sources/test/jest/cache-cleanup.test.ts index f97dbd8..2eed9a1 100644 --- a/sources/test/jest/cache-cleanup.test.ts +++ b/sources/test/jest/cache-cleanup.test.ts @@ -1,5 +1,6 @@ import * as exec from '@actions/exec' import * as core from '@actions/core' +import * as glob from '@actions/glob' import fs from 'fs' import path from 'path' import {CacheCleaner} from '../../src/caching/cache-cleaner' @@ -14,7 +15,7 @@ test('will cleanup unused dependency jars and build-cache entries', async () => await runGradleBuild(projectRoot, 'build', '3.1') - await cacheCleaner.prepare() + const timestamp = await cacheCleaner.prepare() await runGradleBuild(projectRoot, 'build', '3.1.1') @@ -26,7 +27,7 @@ test('will cleanup unused dependency jars and build-cache entries', async () => expect(fs.existsSync(commonsMath311)).toBe(true) expect(fs.readdirSync(buildCacheDir).length).toBe(4) // gc.properties, build-cache-1.lock, and 2 task entries - await cacheCleaner.forceCleanup() + await cacheCleaner.forceCleanupFilesOlderThan(timestamp) expect(fs.existsSync(commonsMath31)).toBe(false) expect(fs.existsSync(commonsMath311)).toBe(true) @@ -42,25 +43,39 @@ test('will cleanup unused gradle versions', async () => { // Initialize HOME with 2 different Gradle versions await runGradleWrapperBuild(projectRoot, 'build') await runGradleBuild(projectRoot, 'build') - - await cacheCleaner.prepare() + + const timestamp = await cacheCleaner.prepare() // Run with only one of these versions await runGradleBuild(projectRoot, 'build') const gradle802 = path.resolve(gradleUserHome, "caches/8.0.2") + const transforms3 = path.resolve(gradleUserHome, "caches/transforms-3") + const metadata100 = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.100") const wrapper802 = path.resolve(gradleUserHome, "wrapper/dists/gradle-8.0.2-bin") const gradleCurrent = path.resolve(gradleUserHome, "caches/8.8") + const metadataCurrent = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.106") expect(fs.existsSync(gradle802)).toBe(true) + expect(fs.existsSync(transforms3)).toBe(true) + expect(fs.existsSync(metadata100)).toBe(true) expect(fs.existsSync(wrapper802)).toBe(true) - expect(fs.existsSync(gradleCurrent)).toBe(true) - await cacheCleaner.forceCleanup() + expect(fs.existsSync(gradleCurrent)).toBe(true) + expect(fs.existsSync(metadataCurrent)).toBe(true) + + // The wrapper won't be removed if it was recently downloaded. Age it. + setUtimes(wrapper802, new Date(Date.now() - 48 * 60 * 60 * 1000)) + + await cacheCleaner.forceCleanupFilesOlderThan(timestamp) expect(fs.existsSync(gradle802)).toBe(false) + expect(fs.existsSync(transforms3)).toBe(false) + expect(fs.existsSync(metadata100)).toBe(false) expect(fs.existsSync(wrapper802)).toBe(false) + expect(fs.existsSync(gradleCurrent)).toBe(true) + expect(fs.existsSync(metadataCurrent)).toBe(true) }) async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise { @@ -86,3 +101,9 @@ function prepareTestProject(): string { return projectRoot } +async function setUtimes(pattern: string, timestamp: Date): Promise { + const globber = await glob.create(pattern) + for await (const file of globber.globGenerator()) { + fs.utimesSync(file, timestamp, timestamp) + } +}