Skip to content
Closed
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
261 changes: 261 additions & 0 deletions .github/workflows/repair-pr-224.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
name: Repair PR 224 multi-hash lock evidence

on:
push:
branches:
- dependabot/pip/ruff-0.16.3
paths:
- .github/workflows/repair-pr-224.yml

permissions:
contents: write

concurrency:
group: repair-pr-224-${{ github.ref }}
cancel-in-progress: false

jobs:
repair:
if: >-
github.repository == 'ContextualWisdomLab/EgressWeave' &&
github.ref == 'refs/heads/dependabot/pip/ruff-0.16.3'
runs-on: ubuntu-24.04
timeout-minutes: 35
env:
EXPECTED_PARENT_SHA: 006161e9530ae081623f81b6cf4d32bce6a55bcc
TARGET_BRANCH: dependabot/pip/ruff-0.16.3
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 2
persist-credentials: false
- name: Verify exact repair boundary
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT_SHA"
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
test "$(git branch --show-current)" = "$TARGET_BRANCH"
- name: Apply minimal multi-hash parser correction and regressions
shell: bash
run: |
set -euo pipefail
python - <<'PY'
from pathlib import Path
from textwrap import dedent

script_path = Path("scripts/ci/generate_release_sbom.py")
script = script_path.read_text(encoding="utf-8")

replacements = [
(
"def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]:\n",
"def _load_runtime_lock(\n"
" path: Path,\n"
") -> dict[str, dict[str, str | tuple[str, ...] | None]]:\n",
),
(
' """Load exact package versions, markers, and hashes from the CI lock."""\n',
' """Load exact package versions, markers, and allowed hashes from the CI lock."""\n',
),
(
" entries: dict[str, dict[str, str | None]] = {}\n",
" entries: dict[\n"
" str, dict[str, str | tuple[str, ...] | None]\n"
" ] = {}\n",
),
(
''' if line.count("--hash=sha256:") != 1:
raise SystemExit("runtime lock entries require exactly one SHA-256 hash")
requirement_text, digest = line.split("--hash=sha256:", 1)
digest = digest.strip()
if SHA256.fullmatch(digest) is None:
raise SystemExit("runtime lock contains a noncanonical SHA-256 hash")
''',
''' segments = line.split("--hash=sha256:")
if len(segments) < 2:
raise SystemExit(
"runtime lock entries require at least one SHA-256 hash"
)
requirement_text = segments[0]
digests = tuple(segment.strip() for segment in segments[1:])
if any(SHA256.fullmatch(digest) is None for digest in digests):
raise SystemExit("runtime lock contains a noncanonical SHA-256 hash")
if len(set(digests)) != len(digests):
raise SystemExit("runtime lock contains a duplicate SHA-256 hash")
''',
),
(' "sha256": digest,\n', ' "sha256": digests,\n'),
(
''' for name, component in components.items():
locked = lock_entries.get(name)
expected = {
"version": component["version"],
"sha256": component["sha256"],
"marker": _canonical_marker(component["marker"], f"component {name}"),
}
if locked != expected:
raise SystemExit(
f"component {name!r} does not match the hash-locked runtime subset"
)
''',
''' for name, component in components.items():
locked = lock_entries.get(name)
expected_marker = _canonical_marker(
component["marker"], f"component {name}"
)
if (
locked is None
or locked["version"] != component["version"]
or locked["marker"] != expected_marker
or component["sha256"] not in locked["sha256"]
):
raise SystemExit(
f"component {name!r} does not match the hash-locked runtime subset"
)
''',
),
]
for old, new in replacements:
if script.count(old) != 1:
raise SystemExit(f"runtime-lock repair boundary drifted: {old[:60]!r}")
script = script.replace(old, new, 1)
script_path.write_text(script, encoding="utf-8")

Path("tests/test_release_lock_hash_sets.py").write_text(
dedent(
'''\
"""Regression contracts for portable multi-artifact hash locks."""

from __future__ import annotations

import importlib.util
from pathlib import Path

import pytest

REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py"
MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json"
LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt"


def _load_generator():
"""Load the repository SBOM generator without packaging it."""
specification = importlib.util.spec_from_file_location(
"egressweave_release_lock_hash_sets",
GENERATOR_PATH,
)
assert specification is not None and specification.loader is not None
module = importlib.util.module_from_spec(specification)
specification.loader.exec_module(module)
return module


def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None:
"""Preserve every exact artifact hash attached to one pinned version."""
generator = _load_generator()
first_digest = "a" * 64
second_digest = "b" * 64
lock_path = tmp_path / "requirements-ci.txt"
lock_path.write_text(
"tool==1.2.3 "
f"--hash=sha256:{first_digest} "
f"--hash=sha256:{second_digest}\\n",
encoding="utf-8",
)

assert generator._load_runtime_lock(lock_path) == {
"tool": {
"version": "1.2.3",
"sha256": (first_digest, second_digest),
"marker": None,
}
}


def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None:
"""Reject ambiguous duplicate evidence within one package entry."""
generator = _load_generator()
digest = "a" * 64
lock_path = tmp_path / "requirements-ci.txt"
lock_path.write_text(
f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\\n",
encoding="utf-8",
)

with pytest.raises(SystemExit, match="duplicate SHA-256"):
generator._load_runtime_lock(lock_path)


def test_runtime_manifest_accepts_its_digest_among_platform_hashes(
tmp_path: Path,
) -> None:
"""Bind runtime evidence to one reviewed digest in a portable hash set."""
generator = _load_generator()
lock_text = LOCK_PATH.read_text(encoding="utf-8")
needle = (
"anyio==4.14.2 \\\\n"
" --hash=sha256:"
"9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"
)
replacement = needle + " \\\\n --hash=sha256:" + ("f" * 64)
assert lock_text.count(needle) == 1
portable_lock = tmp_path / "requirements-ci.txt"
portable_lock.write_text(
lock_text.replace(needle, replacement, 1),
encoding="utf-8",
)

generator.validate_runtime_lock(MANIFEST_PATH, portable_lock)
'''
),
encoding="utf-8",
)

changelog_path = Path("CHANGELOG.md")
changelog = changelog_path.read_text(encoding="utf-8")
anchor = "### Fixed\n"
addition = (
"- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while "
"requiring every reviewed runtime SBOM digest to be present in that immutable set. "
"This preserves portable hash-locked tooling without weakening release evidence.\n"
)
if changelog.count(anchor) != 1 or addition in changelog:
raise SystemExit("changelog insertion boundary drifted")
changelog_path.write_text(
changelog.replace(anchor, anchor + addition, 1),
encoding="utf-8",
)
PY
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
cache: pip
cache-dependency-path: requirements-ci.txt
- name: Install immutable CI toolchain
run: python -m pip install --require-hashes -r requirements-ci.txt
- name: Verify focused and complete GREEN
shell: bash
run: |
set -euo pipefail
ruff check .
coverage erase
coverage run -m pytest -q
coverage report -m
python -m compileall -q src tests scripts
git diff --check
- name: Publish verified repair and remove temporary authority
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
rm .github/workflows/repair-pr-224.yml
git add -A
git config user.name "CWL One-shot Repair"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "fix(sbom): accept portable multi-hash lock entries"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push origin "HEAD:refs/heads/${TARGET_BRANCH}"
21 changes: 19 additions & 2 deletions requirements-ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,25 @@ pytest==9.1.1 \
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
pytest-asyncio==1.4.0 \
--hash=sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1
ruff==0.16.1 \
--hash=sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c
ruff==0.16.3 \
--hash=sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b \
--hash=sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7 \
--hash=sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb \
--hash=sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413 \
--hash=sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a \
--hash=sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474 \
--hash=sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82 \
--hash=sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84 \
--hash=sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da \
--hash=sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d \
--hash=sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870 \
--hash=sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a \
--hash=sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506 \
--hash=sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9 \
--hash=sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2 \
--hash=sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948 \
--hash=sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50 \
--hash=sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081
tomli==2.4.1 ; python_version < "3.11" \
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe
typing-extensions==4.16.0 ; python_version < "3.13" \
Expand Down
Loading