diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1abd52..9485fa4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,9 @@ on: required: false type: string +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} diff --git a/changelog.d/20260722_issue_33_release_permissions.md b/changelog.d/20260722_issue_33_release_permissions.md new file mode 100644 index 0000000..fdc6355 --- /dev/null +++ b/changelog.d/20260722_issue_33_release_permissions.md @@ -0,0 +1,4 @@ +### Security + +- Default the release workflow token to read-only repository access, with write + access limited to the publishing jobs that require it. diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 8917fd3..122edf2 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -71,6 +71,17 @@ def test_release_workflow_keeps_main_releases_running() -> None: assert "cancel-in-progress: true" not in workflow +def test_release_workflow_uses_least_privilege_permissions() -> None: + """Only publishing jobs should receive write-capable tokens.""" + workflow = read_workflow("release.yml") + + assert "\npermissions:\n contents: read\n" in workflow + + for job_name in ("auto-release", "manual-release"): + block = workflow_job_block(workflow, job_name) + assert "permissions:\n contents: write\n id-token: write" in block + + def test_release_workflow_jobs_have_explicit_timeouts() -> None: """Release workflow jobs should fail fast instead of using the six-hour default.""" workflow = read_workflow("release.yml")