mirror of
https://github.com/gradle/actions
synced 2024-11-27 11:52:24 +00:00
Use Gradle 8.8 features for Gradle Home cleanup (#272)
Fixes #33 Fixes #24 Fixes #46 Fixes #169
This commit is contained in:
commit
dad038d88d
4 changed files with 78 additions and 51 deletions
12
.github/workflows/integ-test-cache-cleanup.yml
vendored
12
.github/workflows/integ-test-cache-cleanup.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue