diff --git a/.github/workflows/release-bundle.yml b/.github/workflows/release-bundle.yml index b3cdc7d..dc8f9ab 100644 --- a/.github/workflows/release-bundle.yml +++ b/.github/workflows/release-bundle.yml @@ -87,7 +87,9 @@ jobs: - name: Run release gates shell: bash - run: scripts/validate_release.sh + run: | + python3 -m pip install --quiet --require-hashes -r requirements/test.txt + scripts/validate_release.sh - name: Materialize bounded signed harness evidence input id: harness-evidence diff --git a/core/releasebuilder/workflow_contract_test.go b/core/releasebuilder/workflow_contract_test.go index 1ae6e68..193dafb 100644 --- a/core/releasebuilder/workflow_contract_test.go +++ b/core/releasebuilder/workflow_contract_test.go @@ -154,6 +154,26 @@ func TestHostedReleaseWorkflowPinsAllActionsBySHA(t *testing.T) { } } +func TestHostedReleaseWorkflowInstallsLockedPythonDependenciesBeforeReleaseGate(t *testing.T) { + t.Parallel() + _, currentFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(currentFile), "..", "..")) + workflowPath := filepath.Join(repositoryRoot, ".github", "workflows", "release-bundle.yml") + workflow, err := os.ReadFile(workflowPath) + if err != nil { + t.Fatal(err) + } + content := string(workflow) + install := strings.Index(content, "python3 -m pip install --quiet --require-hashes -r requirements/test.txt") + validate := strings.Index(content, "scripts/validate_release.sh") + if install < 0 || validate < 0 || install > validate { + t.Fatal("release workflow must install hash-locked Python dependencies before validation") + } +} + // The release chain runs on the estate's own fleet. What matters is that all // three jobs agree on one provider: provenance describes the environment of the // run that produced it, so a chain split across providers would attest an