Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ jobs:
cache-provider: basic
cache-read-only: ${{ github.ref_name != github.event.repository.default_branch }}

# The basic Gradle cache is keyed only by build files: a faster job can save it before the
# SDK compiles and GitHub caches cannot be overwritten. Overlay the last successful main
# build's content-addressed compilation cache. Gradle still checks every task's own inputs.
- name: Restore SDK compilation cache
id: sdk-compilation-cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.gradle/caches/build-cache-1
key: java-sdk-compile-v1-${{ runner.os }}-${{ runner.arch }}-${{ github.sha }}
restore-keys: java-sdk-compile-v1-${{ runner.os }}-${{ runner.arch }}-

- name: Build SDK
env:
GRADLE_OPTS: -Dkotlin.compiler.execution.strategy=in-process
Expand All @@ -137,6 +148,18 @@ jobs:
retention-days: 1
compression-level: 0

# Only a successful default-branch push may publish compilation outputs for future runs.
# PRs/merge groups continue sharing only their immutable, exact-run artifact downstream.
- name: Save SDK compilation cache
if: >-
github.event_name == 'push' &&
github.ref_name == github.event.repository.default_branch &&
steps.sdk-compilation-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.gradle/caches/build-cache-1
key: ${{ steps.sdk-compilation-cache.outputs.cache-primary-key }}

jackson_compatibility:
name: CI / Jackson compatibility
needs: build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ class GradleCacheTrustPolicyTest {
assertPullRequestCachePolicy(workflow)
}

@Test
fun `SDK compilation cache cannot be saved by a PR or an earlier job`() {
val workflow = Path.of("../.github/workflows/ci.yml").readText()
val trustedSave =
"github.event_name == 'push' &&\n" +
" github.ref_name == github.event.repository.default_branch &&\n" +
" steps.sdk-compilation-cache.outputs.cache-hit != 'true'"

for (unsafeSave in listOf("true", "github.event_name == 'pull_request'", "always()")) {
val poisonedWorkflow = workflow.replace(trustedSave, unsafeSave)
assertTrue(poisonedWorkflow != workflow)
assertFailsWith<AssertionError> { assertPullRequestCachePolicy(poisonedWorkflow) }
}

val earlierSave =
workflow.replace(
" - name: Run lints\n",
" - name: Save incomplete compilation cache\n" +
" uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25\n" +
" with:\n" +
" path: ~/.gradle/caches/build-cache-1\n" +
" key: java-sdk-compile-v1-incomplete\n\n" +
" - name: Run lints\n",
)
assertTrue(earlierSave != workflow)
assertFailsWith<AssertionError> { assertPullRequestCachePolicy(earlierSave) }
}

@Test
fun `untrusted CI cannot move outside GitHub enforced pull request cache scope`() {
val workflow = Path.of("../.github/workflows/ci.yml").readText()
Expand Down Expand Up @@ -872,6 +900,55 @@ class GradleCacheTrustPolicyTest {
"Gradle cache for job ${action.job} must remain effectively read-only on pull requests and merge groups.",
)
}

val compilationCacheActions =
parsedWorkflow.jobs.values.flatMap { job ->
job.steps.filter { step ->
step.action?.repository == "actions/cache" ||
step.action?.repository?.startsWith("actions/cache/") == true
}
}
assertEquals(
2,
compilationCacheActions.size,
"Only the SDK build owns compilation caching.",
)
val restore = compilationCacheActions[0]
val save = compilationCacheActions[1]
assertEquals("build", restore.action?.job)
assertEquals("build", save.action?.job)
assertEquals("actions/cache/restore", restore.action?.repository)
assertEquals("actions/cache/save", save.action?.repository)
for (step in compilationCacheActions) {
assertTrue(
step.action!!.reference.substringAfter('@').matches(Regex("[0-9a-fA-F]{40}"))
)
assertEquals("~/.gradle/caches/build-cache-1", step.action.inputs["path"])
}
assertEquals(
"java-sdk-compile-v1-\${{ runner.os }}-\${{ runner.arch }}-\${{ github.sha }}",
restore.action?.inputs?.get("key"),
"Each successful main revision must be able to refresh the compilation cache.",
)
assertEquals(
"java-sdk-compile-v1-\${{ runner.os }}-\${{ runner.arch }}-",
restore.action?.inputs?.get("restore-keys"),
)
assertEquals(
"\${{ steps.sdk-compilation-cache.outputs.cache-primary-key }}",
save.action?.inputs?.get("key"),
)
assertEquals(
"github.event_name == 'push' && " +
"github.ref_name == github.event.repository.default_branch && " +
"steps.sdk-compilation-cache.outputs.cache-hit != 'true'",
save.condition?.trim(),
"PRs, merge groups and workflow dispatch must never publish compilation cache entries.",
)
val buildSteps = parsedWorkflow.job("build").steps
val buildIndex = buildSteps.indexOfFirst { it.name == "Build SDK" }
assertTrue(buildSteps.indexOf(restore) < buildIndex)
assertTrue(buildSteps.indexOf(save) > buildIndex)
}

private fun assertPublishingCachePolicy(workflow: String) {
Expand Down Expand Up @@ -1161,6 +1238,7 @@ class GradleCacheTrustPolicyTest {
action,
fields["run"]?.workflowScalar("step script"),
fields["env"].workflowScalars("step environment"),
fields["if"]?.workflowScalar("step condition"),
)
} ?: emptyList()

Expand Down Expand Up @@ -1235,6 +1313,7 @@ class GradleCacheTrustPolicyTest {
val action: WorkflowAction?,
val run: String?,
val environment: Map<String, String>,
val condition: String?,
)

private data class WorkflowAction(
Expand Down
Loading