diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffa9247..4afab0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1087,6 +1087,25 @@ jobs: env: GH_TOKEN: ${{ github.token }} + test-workflow-run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install act + run: gh extension install https://github.com/nektos/gh-act + env: + GH_TOKEN: ${{ github.token }} + - name: Verify workflow_run disables automatic caching with act + run: | + gh act workflow_run \ + -W __tests__/workflows/workflow-run.yml \ + -P ubuntu-latest=catthehacker/ubuntu:act-latest \ + --env RUNNER_ENVIRONMENT=github-hosted + env: + GH_TOKEN: ${{ github.token }} + validate-typings: runs-on: "ubuntu-latest" steps: @@ -1143,6 +1162,7 @@ jobs: - test-restore-python-installs - test-python-install-dir - test-act + - test-workflow-run - validate-typings if: always() steps: diff --git a/README.md b/README.md index 0c03444..70ba4e2 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed # Used when downloading uv from GitHub releases github-token: ${{ github.token }} - # Enable uploading of the uv cache: true, false, or auto (enabled on GitHub-hosted runners, disabled on self-hosted runners) + # Enable the GitHub Actions cache for uv: true, false, or auto (enabled on GitHub-hosted runners except for release, tag push, pull_request_target, and workflow_run events; disabled on self-hosted runners) enable-cache: "auto" # Glob pattern to match files relative to the repository root to control the cache diff --git a/__tests__/utils/inputs.test.ts b/__tests__/utils/inputs.test.ts index e16ffa6..47706e8 100644 --- a/__tests__/utils/inputs.test.ts +++ b/__tests__/utils/inputs.test.ts @@ -12,6 +12,8 @@ import { let mockInputs: Record = {}; const tempDirs: string[] = []; +const ORIGINAL_GITHUB_EVENT_NAME = process.env.GITHUB_EVENT_NAME; +const ORIGINAL_GITHUB_REF = process.env.GITHUB_REF; const ORIGINAL_HOME = process.env.HOME; const ORIGINAL_RUNNER_ENVIRONMENT = process.env.RUNNER_ENVIRONMENT; const ORIGINAL_RUNNER_TEMP = process.env.RUNNER_TEMP; @@ -52,6 +54,8 @@ function createTempProject(files: Record = {}): string { function resetEnvironment(): void { jest.clearAllMocks(); mockInputs = {}; + delete process.env.GITHUB_EVENT_NAME; + delete process.env.GITHUB_REF; process.env.HOME = "/home/testuser"; delete process.env.RUNNER_ENVIRONMENT; delete process.env.RUNNER_TEMP; @@ -64,6 +68,8 @@ function restoreEnvironment(): void { fs.rmSync(dir, { force: true, recursive: true }); } + process.env.GITHUB_EVENT_NAME = ORIGINAL_GITHUB_EVENT_NAME; + process.env.GITHUB_REF = ORIGINAL_GITHUB_REF; process.env.HOME = ORIGINAL_HOME; process.env.RUNNER_ENVIRONMENT = ORIGINAL_RUNNER_ENVIRONMENT; process.env.RUNNER_TEMP = ORIGINAL_RUNNER_TEMP; @@ -94,6 +100,64 @@ describe("loadInputs", () => { expect(inputs.resolutionStrategy).toBe("highest"); }); + it.each([ + "pull_request_target", + "workflow_run", + "release", + ])("disables automatic caching for the %s event", (eventName) => { + mockInputs["working-directory"] = "/workspace"; + mockInputs["enable-cache"] = "auto"; + process.env.RUNNER_ENVIRONMENT = "github-hosted"; + process.env.RUNNER_TEMP = "/runner-temp"; + process.env.GITHUB_EVENT_NAME = eventName; + + const inputs = loadInputs(); + + expect(inputs.enableCache).toBe(false); + expect(mockInfo).toHaveBeenCalledWith( + `Caching is disabled for the ${eventName} event`, + ); + }); + + it("disables automatic caching for tag pushes", () => { + mockInputs["working-directory"] = "/workspace"; + mockInputs["enable-cache"] = "auto"; + process.env.RUNNER_ENVIRONMENT = "github-hosted"; + process.env.RUNNER_TEMP = "/runner-temp"; + process.env.GITHUB_EVENT_NAME = "push"; + process.env.GITHUB_REF = "refs/tags/v1.0.0"; + + const inputs = loadInputs(); + + expect(inputs.enableCache).toBe(false); + expect(mockInfo).toHaveBeenCalledWith("Caching is disabled for tag pushes"); + }); + + it("enables automatic caching for branch pushes", () => { + mockInputs["working-directory"] = "/workspace"; + mockInputs["enable-cache"] = "auto"; + process.env.RUNNER_ENVIRONMENT = "github-hosted"; + process.env.RUNNER_TEMP = "/runner-temp"; + process.env.GITHUB_EVENT_NAME = "push"; + process.env.GITHUB_REF = "refs/heads/main"; + + const inputs = loadInputs(); + + expect(inputs.enableCache).toBe(true); + }); + + it("honors explicitly enabled caching for sensitive events", () => { + mockInputs["working-directory"] = "/workspace"; + mockInputs["enable-cache"] = "true"; + process.env.RUNNER_ENVIRONMENT = "github-hosted"; + process.env.RUNNER_TEMP = "/runner-temp"; + process.env.GITHUB_EVENT_NAME = "release"; + + const inputs = loadInputs(); + + expect(inputs.enableCache).toBe(true); + }); + it("uses cache-dir from pyproject.toml when present", () => { mockInputs["working-directory"] = createTempProject({ "pyproject.toml": `[project] diff --git a/__tests__/workflows/workflow-run.yml b/__tests__/workflows/workflow-run.yml new file mode 100644 index 0000000..22e25a6 --- /dev/null +++ b/__tests__/workflows/workflow-run.yml @@ -0,0 +1,42 @@ +name: "test workflow_run caching" + +on: # zizmor: ignore[dangerous-triggers] this workflow is a test fixture executed by act only + workflow_run: + workflows: + - test + types: + - completed + +permissions: + contents: read + +jobs: + test-cache-disabled: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Setup uv with automatic caching + id: setup-uv + uses: ./ + - name: Verify automatic caching is disabled + env: + CACHE_KEY: ${{ steps.setup-uv.outputs.cache-key }} + run: | + if [ "$GITHUB_EVENT_NAME" != "workflow_run" ]; then + echo "Expected workflow_run event, got: $GITHUB_EVENT_NAME" + exit 1 + fi + if [ "$RUNNER_ENVIRONMENT" != "github-hosted" ]; then + echo "Expected a simulated GitHub-hosted runner, got: $RUNNER_ENVIRONMENT" + exit 1 + fi + if [ -n "$CACHE_KEY" ]; then + echo "Cache key should not be set for a workflow_run event: $CACHE_KEY" + exit 1 + fi + if [ -n "$UV_CACHE_DIR" ]; then + echo "UV_CACHE_DIR should not be set for a workflow_run event: $UV_CACHE_DIR" + exit 1 + fi diff --git a/action.yml b/action.yml index ecacacb..cdcd51d 100644 --- a/action.yml +++ b/action.yml @@ -33,7 +33,7 @@ inputs: required: false default: ${{ github.token }} enable-cache: - description: "Enable uploading of the uv cache" + description: "Enable the GitHub Actions cache for uv. 'auto' enables caching on GitHub-hosted runners except for release, tag push, pull_request_target, and workflow_run events." default: "auto" cache-dependency-glob: description: diff --git a/dist/save-cache/index.cjs b/dist/save-cache/index.cjs index 87d6f7a..9ad6c06 100644 --- a/dist/save-cache/index.cjs +++ b/dist/save-cache/index.cjs @@ -62624,7 +62624,20 @@ function getVenvPath(workingDirectory, activateEnvironment) { function getEnableCache() { const enableCacheInput = getInput("enable-cache"); if (enableCacheInput === "auto") { - return process.env.RUNNER_ENVIRONMENT === "github-hosted"; + if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") { + return false; + } + const eventName = process.env.GITHUB_EVENT_NAME; + const isTagPush = eventName === "push" && process.env.GITHUB_REF?.startsWith("refs/tags/"); + if (isTagPush) { + info2("Caching is disabled for tag pushes"); + return false; + } + if (eventName === "pull_request_target" || eventName === "workflow_run" || eventName === "release") { + info2(`Caching is disabled for the ${eventName} event`); + return false; + } + return true; } return enableCacheInput === "true"; } diff --git a/dist/setup/index.cjs b/dist/setup/index.cjs index c2a5fd3..4258575 100644 --- a/dist/setup/index.cjs +++ b/dist/setup/index.cjs @@ -98255,7 +98255,20 @@ function getVenvPath(workingDirectory, activateEnvironment2) { function getEnableCache() { const enableCacheInput = getInput("enable-cache"); if (enableCacheInput === "auto") { - return process.env.RUNNER_ENVIRONMENT === "github-hosted"; + if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") { + return false; + } + const eventName = process.env.GITHUB_EVENT_NAME; + const isTagPush = eventName === "push" && process.env.GITHUB_REF?.startsWith("refs/tags/"); + if (isTagPush) { + info2("Caching is disabled for tag pushes"); + return false; + } + if (eventName === "pull_request_target" || eventName === "workflow_run" || eventName === "release") { + info2(`Caching is disabled for the ${eventName} event`); + return false; + } + return true; } return enableCacheInput === "true"; } diff --git a/docs/caching.md b/docs/caching.md index 4903d47..b5c734e 100644 --- a/docs/caching.md +++ b/docs/caching.md @@ -38,7 +38,10 @@ The computed cache key is available as the `cache-key` output: If you enable caching, the [uv cache](https://docs.astral.sh/uv/concepts/cache/) will be uploaded to the GitHub Actions cache. This can speed up runs that reuse the cache by several minutes. -Caching is enabled by default on GitHub-hosted runners. +With the default `enable-cache: auto`, caching is enabled on GitHub-hosted runners except for +`release`, tag push, `pull_request_target`, and `workflow_run` events. Caching is disabled for these +events to prevent insecure or release-sensitive jobs from restoring potentially poisoned caches. +Set `enable-cache: true` to explicitly enable caching for any event. > [!TIP] > diff --git a/src/utils/inputs.ts b/src/utils/inputs.ts index c314d37..efe4b5c 100644 --- a/src/utils/inputs.ts +++ b/src/utils/inputs.ts @@ -140,7 +140,27 @@ function getVenvPath( function getEnableCache(): boolean { const enableCacheInput = core.getInput("enable-cache"); if (enableCacheInput === "auto") { - return process.env.RUNNER_ENVIRONMENT === "github-hosted"; + if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") { + return false; + } + + const eventName = process.env.GITHUB_EVENT_NAME; + const isTagPush = + eventName === "push" && process.env.GITHUB_REF?.startsWith("refs/tags/"); + if (isTagPush) { + log.info("Caching is disabled for tag pushes"); + return false; + } + if ( + eventName === "pull_request_target" || + eventName === "workflow_run" || + eventName === "release" + ) { + log.info(`Caching is disabled for the ${eventName} event`); + return false; + } + + return true; } return enableCacheInput === "true"; }