Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/release-audio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ jobs:
name: audio-prep-${{ inputs.version }}
path: dist/
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
8 changes: 4 additions & 4 deletions .github/workflows/release-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
name: docker-server-${{ matrix.platform }}-${{ matrix.bundle }}-${{ inputs.version }}
path: artifact/
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
compression-level: 0
- id: restore-evidence
name: Reuse original image verification evidence on retry
Expand All @@ -112,7 +112,7 @@ jobs:
name: docker-evidence-server-${{ matrix.platform }}-${{ matrix.bundle }}-${{ inputs.version }}
path: evidence/
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}

build-service:
needs: matrix
Expand Down Expand Up @@ -159,7 +159,7 @@ jobs:
name: docker-service-${{ matrix.service }}-${{ inputs.version }}
path: artifact/
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
compression-level: 0
- id: restore-evidence
name: Reuse original image verification evidence on retry
Expand All @@ -183,4 +183,4 @@ jobs:
name: docker-evidence-service-${{ matrix.service }}-${{ inputs.version }}
path: evidence/
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
2 changes: 1 addition & 1 deletion .github/workflows/release-helm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ jobs:
name: helm-sie-cluster-${{ inputs.version }}
path: artifact/
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
2 changes: 1 addition & 1 deletion .github/workflows/release-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ jobs:
name: native-sidecar-${{ inputs.version }}
path: artifact/
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
2 changes: 1 addition & 1 deletion .github/workflows/release-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ jobs:
name: npm-distributions
path: artifacts/*
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
2 changes: 1 addition & 1 deletion .github/workflows/release-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ jobs:
name: python-distributions
path: artifacts/*
if-no-files-found: error
retention-days: 30
retention-days: ${{ github.event_name == 'release' && 30 || 5 }}
7 changes: 7 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ native sidecar from its tested image, as well as building Python, npm, audio,
and the packaged Helm chart. Require its final `Release candidate / Complete`
check on the final release PR head before merging that PR.

Non-release build artifacts, including CI package archives and PR or manually
dispatched rehearsals, expire after five days. This includes Docker archives
and verification evidence; the native sidecar still
consumes its tested image within the same run. After expiry, start a new
rehearsal instead of relying on its old artifacts. Published-release builds
and their original-run retries retain artifacts for 30 days for recovery.

On a stable release, every builder must pass the `artifacts-ready` gate before
any publisher can start. Publishers consume the retained archives. This keeps
a failed image or chart build from leaving packages partially published.
Expand Down
20 changes: 17 additions & 3 deletions tools/ci/check_release_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@
ACTION_PIN = re.compile(r"^[^@\s]+@[0-9a-f]{40}$")
JOB_FIELD_INDENT = 4
ARTIFACT_RETENTION_DAYS = 30
REHEARSAL_ARTIFACT_RETENTION_DAYS = 5
ARTIFACT_RETENTION_EXPRESSION = (
f"${{{{ github.event_name == 'release' && {ARTIFACT_RETENTION_DAYS} || {REHEARSAL_ARTIFACT_RETENTION_DAYS} }}}}"
)
REQUIRED_OCI_LABEL_OCCURRENCES = 2
RELEASE_BOOTSTRAP_SHA = "60996d9c30168e0f8e85b680295f147fdee87f61"
MPL_LICENSE_EXCEPTION_NAMES = frozenset(
Expand Down Expand Up @@ -535,6 +539,18 @@ def release_queue_errors(top: str, ci: str) -> list[str]:
return errors


def artifact_retention_errors(name: str, text: str) -> list[str]:
errors = []
for retention in re.findall(r"^\s*retention-days:\s*([^\n]*)$", text, re.MULTILINE):
retention = re.split(r"[ \t]+#", retention, maxsplit=1)[0].strip()
if retention == ARTIFACT_RETENTION_EXPRESSION:
continue
if retention.isdecimal() and int(retention) >= ARTIFACT_RETENTION_DAYS:
continue
errors.append(f"{name} must retain published-release retry artifacts for at least 30 days")
return errors


Comment thread
coderabbitai[bot] marked this conversation as resolved.
def release_workflow_errors() -> list[str]:
errors: list[str] = []
top = (ROOT / ".github/workflows/release.yml").read_text()
Expand All @@ -557,9 +573,7 @@ def release_workflow_errors() -> list[str]:
for token_name in ("PYPI_TOKEN", "NPM_TOKEN", "NODE_AUTH_TOKEN"):
if token_name in text:
errors.append(f"{path.name} references forbidden publication token {token_name}")
for retention in re.findall(r"retention-days:\s*(\d+)", text):
if int(retention) < ARTIFACT_RETENTION_DAYS:
errors.append(f"{path.name} expires retry artifacts before 30 days")
errors.extend(artifact_retention_errors(path.name, text))
if path.name not in {"release.yml", "release-candidate.yml"} and "workflow_dispatch" in text:
errors.append(f"{path.name} must not expose a separate recovery writer")
blocks = workflow_job_blocks(".github/workflows/release.yml")
Expand Down
30 changes: 30 additions & 0 deletions tools/ci/tests/test_release_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,36 @@ def test_release_workflows_are_pinned_and_fail_closed() -> None:
assert contract.publisher_job_errors() == []


@pytest.mark.parametrize(
"retention",
[
"5",
"5 # too short for release recovery",
"29",
"${{ github.event_name == 'release' && 5 || 30 }}",
"${{ github.event_name == 'release' && 29 || 5 }}",
"${{ github.event_name == 'workflow_dispatch' && 30 || 5 }}",
"${{ inputs.retention_days }}",
],
)
def test_artifact_retention_rejects_short_or_unverified_release_recovery(retention):
assert contract.artifact_retention_errors("release-docker.yml", f" retention-days: {retention}\n")


@pytest.mark.parametrize(
"retention",
[
"30",
"30 # retain release recovery artifacts",
"90",
"${{ github.event_name == 'release' && 30 || 5 }}",
"${{ github.event_name == 'release' && 30 || 5 }} # shorter rehearsals",
],
)
def test_artifact_retention_allows_full_release_recovery(retention):
assert contract.artifact_retention_errors("release-docker.yml", f" retention-days: {retention}\n") == []


@pytest.mark.parametrize("replacement", [" queue: single", " queue: arbitrary", "", " queue: max\n queue: single"])
def test_release_queue_must_retain_pending_runs(replacement):
top = (contract.ROOT / ".github/workflows/release.yml").read_text()
Expand Down
Loading