Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1d2d636
Add dependency cooldowns (#1021)
github-actions[bot] Oct 5, 2026
b54ffe5
chore: update known checksums for 0.12.5 (#1020)
github-actions[bot] Oct 5, 2026
f15e5f7
Use JSON + a typed wrapper instead of TS codegen (#1025)
github-actions[bot] Oct 5, 2026
a989306
Use JSON + a typed wrapper instead of TS codegen (#1025)
github-actions[bot] Oct 5, 2026
12e62f2
Harden npm install defaults (#1026)
github-actions[bot] Oct 5, 2026
b9630c1
Harden npm install defaults (#1026)
github-actions[bot] Oct 5, 2026
75c320a
Verify downloads with astral-sh/versions checksums (#1033)
github-actions[bot] Oct 5, 2026
fe22e18
Verify downloads with astral-sh/versions checksums (#1033)
github-actions[bot] Oct 5, 2026
ad9b512
Verify downloads with astral-sh/versions checksums (#1033)
github-actions[bot] Oct 5, 2026
a005646
Verify downloads with astral-sh/versions checksums (#1033)
github-actions[bot] Oct 5, 2026
cde9cc2
Verify downloads with astral-sh/versions checksums (#1033)
github-actions[bot] Oct 5, 2026
c9b03ce
Expose a Python "identity" output (#1036)
github-actions[bot] Oct 5, 2026
4c8b911
Expose a Python "identity" output (#1036)
github-actions[bot] Oct 5, 2026
3266937
Expose a Python "identity" output (#1036)
github-actions[bot] Oct 5, 2026
d231dc0
Expose a Python "identity" output (#1036)
github-actions[bot] Oct 5, 2026
0f945cf
fix: respect no proxy directive (#1037)
github-actions[bot] Oct 5, 2026
94d6d39
chore(deps-dev): roll up Dependabot updates (#1043)
github-actions[bot] Oct 5, 2026
69dcf87
fix: apply code build script
github-actions[bot] Oct 5, 2026
d9394a0
fix: apply code build script
github-actions[bot] Oct 5, 2026
25ddea0
conflicted commits cherry-picked
Raj-StepSecurity Oct 5, 2026
e3d5e6e
comments addressed
Raj-StepSecurity Oct 7, 2026
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
39 changes: 16 additions & 23 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ jobs:
persist-credentials: false
- name: Install default version
uses: ./
- run: uvx ruff --version
- run: uvx ruff@0.14.10 --version

test-tool-install:
runs-on: ${{ matrix.os }}
Expand All @@ -389,7 +389,7 @@ jobs:
persist-credentials: false
- name: Install default version
uses: ./
- run: uv tool install ruff
- run: uv tool install ruff==0.14.10
- run: ruff --version

test-python-version:
Expand Down Expand Up @@ -423,9 +423,14 @@ jobs:
if [ "$PYTHON_VERSION" != "3.13.1t" ]; then
exit 1
fi
if [ -n "$PYTHON_RUNTIME_ID" ]; then
echo "python-runtime-id should be empty without environment activation"
exit 1
fi
shell: bash
env:
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}
- run: uv sync
working-directory: __tests__/fixtures/uv-project

Expand Down Expand Up @@ -579,6 +584,15 @@ jobs:
raise SystemExit(f"Python is not running from custom venv: {sys.executable}")
PY
shell: bash
- name: Verify Python runtime ID from custom venv
run: |
case "$PYTHON_RUNTIME_ID" in
cpython-3.13.1+freethreaded-*) ;;
*) echo "Wrong Python runtime ID: $PYTHON_RUNTIME_ID"; exit 1 ;;
esac
shell: bash
env:
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}

test-debian-unstable:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -1309,26 +1323,6 @@ jobs:
- name: Install python works
run: uv python install

test-act:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit

- 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: Run test-uvx with act
run: gh act -j test-uvx -P ubuntu-latest=catthehacker/ubuntu:act-latest
env:
GH_TOKEN: ${{ github.token }}

test-workflow-run:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -1414,7 +1408,6 @@ jobs:
- test-cache-python-installs
- test-restore-python-installs
- test-python-install-dir
- test-act
- test-workflow-run
- validate-typings
if: always()
Expand Down
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
engine-strict = true
ignore-scripts = true
min-release-age=3
Comment thread
Raj-StepSecurity marked this conversation as resolved.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24
24.19.0
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
- `cache-hit`: A boolean value to indicate a cache entry was found.
- `venv`: Path to the activated venv if activate-environment is true.
- `python-version`: The Python version that was set.
- `python-runtime-id`: An opaque identifier reported by uv for the activated venv's Python runtime. Empty when `activate-environment` is false.
- `python-cache-hit`: A boolean value to indicate the Python cache entry was found.

### Python version
Expand Down
40 changes: 40 additions & 0 deletions __tests__/download/checksum/checksum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,49 @@ test("provided checksum beats known checksums", async () => {
"x86_64",
"unknown-linux-gnu",
"0.3.0",
"incorrect-manifest-checksum",
);
});

test("known checksums beat manifest checksums", async () => {
await expect(
validateChecksum(
undefined,
filePath,
"x86_64",
"unknown-linux-gnu",
"0.3.0",
validChecksum,
),
).rejects.toThrow("did not match");
});

test("manifest checksums are used when no known checksum exists", async () => {
await expect(
validateChecksum(
undefined,
filePath,
"aarch64",
"pc-windows-msvc",
"1.2.3",
"incorrect-manifest-checksum",
),
).rejects.toThrow("did not match");
});

test("empty manifest checksums are rejected", async () => {
await expect(
validateChecksum(
undefined,
filePath,
"aarch64",
"pc-windows-msvc",
"1.2.3",
"",
),
).rejects.toThrow("No checksum found");
});

type KnownVersionFixture = { version: string; known: boolean };

it.each<KnownVersionFixture>([
Expand Down
27 changes: 27 additions & 0 deletions __tests__/download/checksum/update-known-checksums.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { expect, test } from "@jest/globals";
import { updateChecksums } from "../../../src/download/checksum/update-known-checksums";

test("serializes checksum entries as JSON data", async () => {
const tempDirectory = await fs.mkdtemp(
path.join(os.tmpdir(), "setup-uv-checksums-test-"),
);
const outputPath = path.join(tempDirectory, "known-checksums.json");
const key = 'platform-1.0.0"\n};\nglobalThis.compromised = true;';
const checksum = 'checksum"\\\nvalue';

try {
await updateChecksums(outputPath, [
{ checksum, key },
{ checksum: "duplicate", key },
]);

const content = await fs.readFile(outputPath, "utf8");
expect(JSON.parse(content)).toEqual({ [key]: checksum });
expect(content.endsWith("\n")).toBe(true);
} finally {
await fs.rm(tempDirectory, { force: true, recursive: true });
}
});
7 changes: 5 additions & 2 deletions __tests__/download/download-version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,10 @@ describe("download-version", () => {
expect(mockValidateChecksum).not.toHaveBeenCalled();
});

it("uses built-in checksums for default manifest downloads", async () => {
it("uses the default manifest checksum as a fallback", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum-that-should-be-ignored",
checksum: "manifest-checksum",
downloadUrl: "https://example.com/uv.tar.gz",
});

Expand All @@ -248,6 +248,7 @@ describe("download-version", () => {
"x86_64",
"unknown-linux-gnu",
"0.9.26",
"manifest-checksum",
);
});

Expand Down Expand Up @@ -400,6 +401,7 @@ describe("download-version", () => {
"x86_64",
"unknown-linux-gnu",
"0.9.26",
"manifest-checksum",
);
});

Expand All @@ -425,6 +427,7 @@ describe("download-version", () => {
"x86_64",
"unknown-linux-gnu",
"0.9.26",
"manifest-checksum",
);
});

Expand Down
86 changes: 86 additions & 0 deletions __tests__/utils/python-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { promisify } from "node:util";
import { beforeEach, expect, it, jest } from "@jest/globals";
import { createSetupInputs } from "../helpers/setup-inputs";

const mockExecFile =
jest.fn<
(...args: unknown[]) => Promise<{ stdout: string; stderr: string }>
>();
const inputs = createSetupInputs({
activateEnvironment: true,
pythonVersion: "3.15t",
});

jest.unstable_mockModule("node:child_process", () => ({
// execFile's custom promisifier returns both stdout and stderr.
execFile: Object.assign(mockExecFile, { [promisify.custom]: mockExecFile }),
}));

const { getPythonRuntimeId } = await import("../../src/utils/python-runtime");

beforeEach(() => {
mockExecFile.mockReset();
mockExecFile.mockResolvedValue({
stderr: "",
stdout: '[{"key":"cpython-3.13.1-linux-x86_64-gnu"}]\r\n',
});
});

it("does not query uv without environment activation", async () => {
expect(
await getPythonRuntimeId({ ...inputs, activateEnvironment: false }),
).toBe("");
expect(mockExecFile).not.toHaveBeenCalled();
});

it.each([
"cpython-3.13.1-linux-x86_64-gnu",
"cpython-3.15.0rc1+freethreaded-macos-aarch64-none",
"cpython-3.15.0rc2+freethreaded-windows-x86_64-none",
"pypy-3.11.15-linux-x86_64-gnu",
])("returns uv's opaque runtime key unchanged: %s", async (key) => {
mockExecFile.mockResolvedValue({
stderr: "",
stdout: `${JSON.stringify([{ key }])}\r\n`,
});
expect(await getPythonRuntimeId(inputs)).toBe(key);
});

it.each(['/runner temp/a "quoted" venv', "C:\\runner temp\\custom venv"])(
"queries the exact venv directory: %s",
async (venvPath) => {
await getPythonRuntimeId({ ...inputs, venvPath });
expect(mockExecFile).toHaveBeenCalledWith(
"uv",
[
"python",
"list",
venvPath,
"--only-installed",
"--output-format",
"json",
],
{ encoding: "utf8" },
);
},
);

it.each([
new Error("uv failed"),
"not JSON",
"null",
"{}",
"[]",
'[{"key":""}]',
'[{"key":123}]',
'[{"key":"first"},{"key":"second"}]',
])("rejects uv failure or invalid results: %s", async (result) => {
if (result instanceof Error) {
mockExecFile.mockRejectedValue(result);
} else {
mockExecFile.mockResolvedValue({ stderr: "", stdout: result });
}
await expect(getPythonRuntimeId(inputs)).rejects.toThrow(
"Failed to identify the activated environment's Python runtime:",
);
});
2 changes: 2 additions & 0 deletions action-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,7 @@ outputs:
type: string
python-version:
type: string
python-runtime-id:
type: string
python-cache-hit:
type: boolean
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ outputs:
description: "Path to the activated venv if activate-environment is true"
python-version:
description: "The Python version that was set."
python-runtime-id:
description: "An opaque identifier reported by uv for the activated venv's Python runtime. Empty when activate-environment is false."
python-cache-hit:
description: "A boolean value to indicate the Python cache entry was found"
runs:
Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.8/schema.json",
"assist": {
"actions": {
"source": {
Expand Down
Loading
Loading