Use Gradle 8.8 features for Gradle Home cleanup (#272)

Fixes #33
Fixes #24
Fixes #46 
Fixes #169
This commit is contained in:
Daz DeBoer 2024-06-28 13:46:10 -06:00 committed by GitHub
commit dad038d88d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 51 deletions

View file

@ -35,7 +35,7 @@ jobs:
cache-read-only: false # For testing, allow writing cache entries on non-default branches cache-read-only: false # For testing, allow writing cache entries on non-default branches
- name: Build with 3.1 - name: Build with 3.1
working-directory: sources/test/jest/resources/cache-cleanup 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 # Second build will use the cache from the first build, but cleanup should remove unused artifacts
assemble-build: assemble-build:
@ -58,7 +58,7 @@ jobs:
gradle-home-cache-cleanup: true gradle-home-cache-cleanup: true
- name: Build with 3.1.1 - name: Build with 3.1.1
working-directory: sources/test/jest/resources/cache-cleanup 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: check-clean-cache:
needs: assemble-build needs: assemble-build
@ -78,7 +78,9 @@ jobs:
with: with:
cache-read-only: true cache-read-only: true
- name: Report Gradle User Home - 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 - name: Verify cleaned cache
shell: bash shell: bash
run: | run: |
@ -90,3 +92,7 @@ jobs:
echo "::error ::Should NOT find commons-math3 3.1 in cache" echo "::error ::Should NOT find commons-math3 3.1 in cache"
exit 1 exit 1
fi 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

View file

@ -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` `./build act -W .github/workflows/integ-test-caching-config.yml -j cache-disabled-pre-existing-gradle-home`
Known issues: 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. - `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) - `act` is not yet compatible with `actions/upload-artifact@v4` (or related toolkit functions)
- See https://github.com/nektos/act/pull/2224 - See https://github.com/nektos/act/pull/2224

View file

@ -1,8 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as glob from '@actions/glob'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import {provisionAndMaybeExecute} from '../execution/gradle'
export class CacheCleaner { export class CacheCleaner {
private readonly gradleUserHome: string private readonly gradleUserHome: string
@ -13,25 +12,20 @@ export class CacheCleaner {
this.tmpDir = tmpDir this.tmpDir = tmpDir
} }
async prepare(): Promise<void> { async prepare(): Promise<string> {
// Reset the file-access journal so that files appear not to have been used recently // Save the current timestamp
fs.rmSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true, force: true}) const timestamp = Date.now().toString()
fs.mkdirSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true}) core.saveState('clean-timestamp', timestamp)
fs.writeFileSync( return timestamp
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 forceCleanup(): Promise<void> { async forceCleanup(): Promise<void> {
// Age all 'gc' files so that cache cleanup will run immediately. const cleanTimestamp = core.getState('clean-timestamp')
await this.ageAllFiles('gc.properties') await this.forceCleanupFilesOlderThan(cleanTimestamp)
}
async forceCleanupFilesOlderThan(cleanTimestamp: string): Promise<void> {
core.info(`Cleaning up caches before ${cleanTimestamp}`)
// Run a dummy Gradle build to trigger cache cleanup // Run a dummy Gradle build to trigger cache cleanup
const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project') const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project')
@ -40,30 +34,37 @@ export class CacheCleaner {
path.resolve(cleanupProjectDir, 'settings.gradle'), path.resolve(cleanupProjectDir, 'settings.gradle'),
'rootProject.name = "dummy-cleanup-project"' '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") {}') 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 provisionAndMaybeExecute('current', cleanupProjectDir, [
await exec.exec(gradleCommand, [], { '-g',
cwd: cleanupProjectDir this.gradleUserHome,
}) '-I',
} 'init.gradle',
'--info',
private async ageAllFiles(fileName = '*'): Promise<void> { '--no-daemon',
core.debug(`Aging all files in Gradle User Home with name ${fileName}`) '--no-scan',
await this.setUtimes(`${this.gradleUserHome}/**/${fileName}`, new Date(0)) '--build-cache',
} '-DGITHUB_DEPENDENCY_GRAPH_ENABLED=false',
'noop'
private async touchAllFiles(fileName = '*'): Promise<void> { ])
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<void> {
const globber = await glob.create(pattern, {
implicitDescendants: false
})
for await (const file of globber.globGenerator()) {
fs.utimesSync(file, timestamp, timestamp)
}
} }
} }

View file

@ -1,5 +1,6 @@
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import {CacheCleaner} from '../../src/caching/cache-cleaner' 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 runGradleBuild(projectRoot, 'build', '3.1')
await cacheCleaner.prepare() const timestamp = await cacheCleaner.prepare()
await runGradleBuild(projectRoot, 'build', '3.1.1') 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.existsSync(commonsMath311)).toBe(true)
expect(fs.readdirSync(buildCacheDir).length).toBe(4) // gc.properties, build-cache-1.lock, and 2 task entries 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(commonsMath31)).toBe(false)
expect(fs.existsSync(commonsMath311)).toBe(true) expect(fs.existsSync(commonsMath311)).toBe(true)
@ -42,25 +43,39 @@ test('will cleanup unused gradle versions', async () => {
// Initialize HOME with 2 different Gradle versions // Initialize HOME with 2 different Gradle versions
await runGradleWrapperBuild(projectRoot, 'build') await runGradleWrapperBuild(projectRoot, 'build')
await runGradleBuild(projectRoot, 'build') await runGradleBuild(projectRoot, 'build')
await cacheCleaner.prepare() const timestamp = await cacheCleaner.prepare()
// Run with only one of these versions // Run with only one of these versions
await runGradleBuild(projectRoot, 'build') await runGradleBuild(projectRoot, 'build')
const gradle802 = path.resolve(gradleUserHome, "caches/8.0.2") 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 wrapper802 = path.resolve(gradleUserHome, "wrapper/dists/gradle-8.0.2-bin")
const gradleCurrent = path.resolve(gradleUserHome, "caches/8.8") 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(gradle802)).toBe(true)
expect(fs.existsSync(transforms3)).toBe(true)
expect(fs.existsSync(metadata100)).toBe(true)
expect(fs.existsSync(wrapper802)).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(gradle802)).toBe(false)
expect(fs.existsSync(transforms3)).toBe(false)
expect(fs.existsSync(metadata100)).toBe(false)
expect(fs.existsSync(wrapper802)).toBe(false) expect(fs.existsSync(wrapper802)).toBe(false)
expect(fs.existsSync(gradleCurrent)).toBe(true) expect(fs.existsSync(gradleCurrent)).toBe(true)
expect(fs.existsSync(metadataCurrent)).toBe(true)
}) })
async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise<void> { async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise<void> {
@ -86,3 +101,9 @@ function prepareTestProject(): string {
return projectRoot return projectRoot
} }
async function setUtimes(pattern: string, timestamp: Date): Promise<void> {
const globber = await glob.create(pattern)
for await (const file of globber.globGenerator()) {
fs.utimesSync(file, timestamp, timestamp)
}
}