diff --git a/.agents/skills/release-captain/SKILL.md b/.agents/skills/release-captain/SKILL.md index 90ddc1286f..6ad534404f 100644 --- a/.agents/skills/release-captain/SKILL.md +++ b/.agents/skills/release-captain/SKILL.md @@ -42,21 +42,6 @@ tag, or start a second `Pre-release` run for a tag; it has no concurrency guard. picks the highest `-pre.N` on `main` whose whole `Pre-release` run passed, creates the stable tag on that commit, and publishes without requalifying. -### Patch-train protobuf exception - -All `v0.1.6-pre.N` candidates have a release-captain-approved protobuf compatibility exception -in `release-qualification.yml`. The check still compiles both protobuf snapshots -and runs Buf; only detected breaking changes are allowed. Compilation and tooling -errors still fail. Other versions retain normal version-based enforcement. -The run summary and qualification JSON record the exception and reason. Review -all protobuf findings for each candidate and include migration guidance in the -release notes; the exception does not accept other compatibility-review findings. -Squash-merge PR #4976 and, after verifying v0.1.6 publication, prepare a cleanup PR -reverting its entire squash-merge commit. Follow "Remove the v0.1.6 exception" in -`CI.md`; remove the checker support and metadata along with the workflow exception. -A new prerelease number -within this patch train retains the exception; other patch or minor trains do not. - ## Setup ```shell diff --git a/.agents/skills/watch-github-actions/SKILL.md b/.agents/skills/watch-github-actions/SKILL.md index f80ef25b12..3443407d38 100644 --- a/.agents/skills/watch-github-actions/SKILL.md +++ b/.agents/skills/watch-github-actions/SKILL.md @@ -169,8 +169,6 @@ the shared `check-protobuf-compatibility` action with `nix run .#check-protobuf- During `0.x`, a minor train permits compatibility findings as warnings; a patch train or no active train rejects them. Compare the current train's version with the latest stable release; commit messages are irrelevant. -For the patch-train `v0.1.6-pre.N` exception, inspect the run summary and the -qualification JSON's `exceptions` array; see `release-captain` for disposition. Compilation, baseline, and tool errors remain fatal. The `protobuf_compatibility` suite participates in the `release-tag-v1` qualification profile. Pre-release calls the reusable `release-qualification.yml` workflow after building, in parallel with publication. Release diff --git a/.github/workflows/release-qualification.yml b/.github/workflows/release-qualification.yml index b65a34b636..3dc0ca3c9e 100644 --- a/.github/workflows/release-qualification.yml +++ b/.github/workflows/release-qualification.yml @@ -30,10 +30,6 @@ permissions: env: RELEASE_TAG: ${{ inputs.release-tag }} - # TEMPORARY (#4976): squash-merge and revert after v0.1.6 publication is verified. - # See CI.md's "Remove the v0.1.6 exception" procedure. - PROTO_COMPATIBILITY_EXCEPTION_TAG: ${{ inputs.is-prerelease && startsWith(inputs.release-tag, 'v0.1.6-pre.') && inputs.release-tag || '' }} - PROTO_COMPATIBILITY_EXCEPTION_REASON: Release captain accepted protobuf breaking changes for the v0.1.6 prerelease train; migration guidance must accompany the release. defaults: run: @@ -216,11 +212,6 @@ jobs: echo "| Docker E2E | ${DOCKER_E2E_RESULT} |" echo "| VM E2E | ${VM_E2E_RESULT} |" echo - if [[ -n "${PROTO_COMPATIBILITY_EXCEPTION_TAG}" ]]; then - echo "- Protobuf compatibility exception: \`${PROTO_COMPATIBILITY_EXCEPTION_TAG}\`" - echo "- Reason: ${PROTO_COMPATIBILITY_EXCEPTION_REASON}" - echo - fi echo "Advisory compatibility findings do not affect the qualification result." echo "See the Compatibility Review job and compatibility-review-run-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT} artifact in [this run](${run_url})." } >> "${GITHUB_STEP_SUMMARY}" diff --git a/CI.md b/CI.md index 5252a4f330..df66dc0ad3 100644 --- a/CI.md +++ b/CI.md @@ -48,14 +48,6 @@ remains in Actions. This checks protobuf compatibility; SDK/configuration compatibility and migration review remain separate qualification work. -The release qualification workflow has a release-captain-approved exception -for all `v0.1.6-pre.N` candidates, including replacement candidates after a failed -run. It still runs Buf and fails on compilation or tooling -errors, but accepts detected protobuf breaking changes for that tag. The run -summary and qualification JSON's `exceptions` array record the scope and reason. -Other versions retain the normal version policy. Remove the workflow exception -after promotion; release notes must describe the accepted changes and migration. - Fetch the target and tags to check committed branch changes locally, replacing `origin/main` for another target: @@ -75,23 +67,6 @@ For an intentional minor-train incompatibility, record the Buf finding, linked issue, consumer impact, and migration plan in the PR. Keep the finding visible; do not disable the job or add a broad Buf ignore rule. -### Remove the v0.1.6 exception - -Squash-merge [PR #4976](https://github.com/NVIDIA/OpenShell/pull/4976) so the -exception, checker support, qualification metadata, tests, and temporary guidance -can be removed together with one revert. After verifying stable v0.1.6 publication, -open a cleanup PR from current `main` and revert that PR's squash-merge commit: - -```shell -git revert --no-commit -git commit --signoff -m "revert(ci): remove temporary v0.1.6 protobuf exception" -``` - -Use the final squash-merge SHA shown on the merged PR, not a branch commit SHA. -Validate the reverted workflow with Actionlint and run the normal protobuf check -before merging the cleanup. Reverting the whole PR restores normal enforcement -and removes the exception mechanism; do not leave the bypass support behind. - ### Windows validation Native Windows implementation and releases use the diff --git a/tasks/scripts/check_proto_compatibility.py b/tasks/scripts/check_proto_compatibility.py index d2ca37ba0a..8a2ba37134 100644 --- a/tasks/scripts/check_proto_compatibility.py +++ b/tasks/scripts/check_proto_compatibility.py @@ -103,8 +103,7 @@ def compare(candidate: str, baseline: str, allows_breaks: bool) -> int: if result.returncode == 100 and allows_breaks: print(diagnostics.replace("::error ", "::warning "), end="") print( - "Breaking changes allowed by the version policy or an explicit " - "maintainer exception; review migration guidance." + "Breaking changes allowed by the version increment; review migration guidance." ) return 0 print(diagnostics, end="") @@ -123,15 +122,6 @@ def main() -> int: ) release = ref.removeprefix("refs/tags/") if ref.startswith("refs/tags/") else None stable, train, allows_breaks = train_policy(candidate, release) - exception_tag = os.environ.get("PROTO_COMPATIBILITY_EXCEPTION_TAG", "") - if exception_tag: - if release != exception_tag or not _parse_prerelease_tag(exception_tag): - raise ValueError("Protobuf exception must match the exact prerelease tag.") - reason = os.environ.get("PROTO_COMPATIBILITY_EXCEPTION_REASON", "").strip() - if not reason: - raise ValueError("Protobuf exception requires a reason.") - print(f"Protobuf compatibility exception for {release}: {reason}", flush=True) - allows_breaks = True baseline = f"refs/tags/{stable}" if release else candidate if not release: # merge-tree writes only Git objects, leaving HEAD and the worktree diff --git a/tasks/scripts/check_proto_compatibility_test.py b/tasks/scripts/check_proto_compatibility_test.py deleted file mode 100644 index ce80e06c13..0000000000 --- a/tasks/scripts/check_proto_compatibility_test.py +++ /dev/null @@ -1,163 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import json -import os -import shutil -import subprocess -import sys -from pathlib import Path -from unittest.mock import patch - -import pytest - -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import check_proto_compatibility as compatibility - -TAG = "v0.1.6-pre.7" -REASON = "Release captain accepted the protobuf changes." - - -@pytest.mark.parametrize( - ("ref", "exception_tag", "reason", "expected"), - [ - (f"refs/tags/{TAG}", TAG, REASON, True), - ("refs/tags/v0.1.6-pre.8", "v0.1.6-pre.8", REASON, True), - ("refs/tags/v0.1.6-pre.12", "v0.1.6-pre.12", REASON, True), - ("refs/tags/v0.1.6-pre.8", "", "", False), - ("refs/tags/v0.1.7-pre.1", "", "", False), - ("refs/tags/v0.2.0-pre.1", "", "", False), - (f"refs/tags/{TAG}", "", "", False), - ("refs/tags/v0.1.6-pre.8", TAG, REASON, ValueError), - ("refs/tags/v0.1.6", "v0.1.6", REASON, ValueError), - ("refs/heads/main", TAG, REASON, ValueError), - (f"refs/tags/{TAG}", TAG, " ", ValueError), - ], -) -def test_exception_scope(monkeypatch, ref, exception_tag, reason, expected): - monkeypatch.setenv("PROTO_COMPATIBILITY_EXCEPTION_TAG", exception_tag) - monkeypatch.setenv("PROTO_COMPATIBILITY_EXCEPTION_REASON", reason) - monkeypatch.setattr(sys, "argv", ["check", ref]) - with ( - patch.object(compatibility, "git", side_effect=[ref, "candidate-sha"]), - patch.object( - compatibility, "train_policy", return_value=("v0.1.5", "v0.1.6", False) - ), - patch.object(compatibility, "compare", return_value=0) as compare, - ): - if expected is ValueError: - with pytest.raises(ValueError): - compatibility.main() - compare.assert_not_called() - else: - assert compatibility.main() == 0 - assert compare.call_args.args[2] is expected - - -@pytest.mark.parametrize( - ("allows_breaks", "exit_code", "expected"), - [(False, 100, 100), (True, 100, 0), (True, 1, 1), (True, 0, 0)], -) -def test_exception_only_accepts_breaking_diagnostics( - allows_breaks, exit_code, expected -): - with ( - patch.object(compatibility, "export_proto"), - patch.object( - compatibility.subprocess, - "run", - side_effect=[ - subprocess.CompletedProcess([], 0), - subprocess.CompletedProcess([], 0), - subprocess.CompletedProcess([], exit_code, stdout="", stderr=""), - ], - ) as run, - ): - assert compatibility.compare("candidate", "baseline", allows_breaks) == expected - assert [call.args[0][1] for call in run.call_args_list] == [ - "build", - "build", - "breaking", - ] - - -@pytest.mark.parametrize("failed_snapshot", [0, 1]) -def test_exception_does_not_accept_invalid_protobufs(failed_snapshot): - results = [subprocess.CompletedProcess([], 0)] * failed_snapshot - results.append(subprocess.CalledProcessError(100, ["buf", "build"])) - with ( - patch.object(compatibility, "export_proto"), - patch.object(compatibility.subprocess, "run", side_effect=results) as run, - pytest.raises(subprocess.CalledProcessError), - ): - compatibility.compare("candidate", "baseline", True) - assert run.call_count == failed_snapshot + 1 - - -@pytest.mark.skipif(shutil.which("buf") is None, reason="Buf is not installed") -@pytest.mark.parametrize("allows_breaks", [False, True]) -def test_real_buf_field_rename(monkeypatch, allows_breaks): - def export_fixture(ref, destination): - (destination / "proto").mkdir(parents=True) - (destination / "buf.yaml").write_text( - "version: v2\nmodules:\n - path: proto\nbreaking:\n use:\n - FILE\n" - ) - field = "old_name" if ref == "baseline" else "new_name" - (destination / "proto" / "test.proto").write_text( - 'syntax = "proto3";\npackage test.v1;\n' - f"message Request {{ bool {field} = 3; }}\n" - ) - - monkeypatch.setattr(compatibility, "export_proto", export_fixture) - assert compatibility.compare("candidate", "baseline", allows_breaks) == ( - 0 if allows_breaks else 100 - ) - - -@pytest.mark.parametrize("exception", [False, True]) -@pytest.mark.parametrize("proto_result", ["success", "failure", "skipped"]) -@pytest.mark.parametrize("tag", [TAG, "v0.1.6-pre.8"]) -def test_qualification_records_exception_without_bypassing_results( - tmp_path, exception, proto_result, tag -): - output = tmp_path / "qualification.json" - env = { - **os.environ, - "RELEASE_TAG": tag, - "SOURCE_SHA": "candidate-sha", - "IS_PRERELEASE": "true", - "GITHUB_RUN_ID": "123", - "GITHUB_RUN_ATTEMPT": "1", - "GITHUB_SERVER_URL": "https://github.com", - "GITHUB_REPOSITORY": "NVIDIA/OpenShell", - "PROTO_COMPATIBILITY_RESULT": proto_result, - "PROTO_COMPATIBILITY_EXCEPTION_TAG": tag if exception else "", - "PROTO_COMPATIBILITY_EXCEPTION_REASON": REASON, - } - for suite in ( - "SECURITY", - "CONFORMANCE", - "FEATURE_INTEGRATION", - "DOCKER_E2E", - "VM_E2E", - ): - env[f"{suite}_RESULT"] = "success" - subprocess.run( - [ - "bash", - str(Path(__file__).with_name("generate-qualification-summary.sh")), - str(output), - ], - env=env, - check=True, - capture_output=True, - text=True, - ) - summary = json.loads(output.read_text()) - assert summary["current_profile_passed"] is (proto_result == "success") - assert summary["exceptions"] == ( - [{"suite": "protobuf_compatibility", "tag": tag, "reason": REASON}] - if exception - else [] - ) diff --git a/tasks/scripts/generate-qualification-summary.sh b/tasks/scripts/generate-qualification-summary.sh index 0e229cf85d..78f55c239e 100755 --- a/tasks/scripts/generate-qualification-summary.sh +++ b/tasks/scripts/generate-qualification-summary.sh @@ -22,20 +22,12 @@ require_env() { [[ $# -eq 1 ]] || die "usage: $0 OUTPUT" output=$1 current_profile_passed=true -proto_exception_tag=${PROTO_COMPATIBILITY_EXCEPTION_TAG:-} -proto_exception_reason=${PROTO_COMPATIBILITY_EXCEPTION_REASON:-} require_env \ RELEASE_TAG SOURCE_SHA IS_PRERELEASE GITHUB_RUN_ID GITHUB_RUN_ATTEMPT \ GITHUB_SERVER_URL GITHUB_REPOSITORY SECURITY_RESULT CONFORMANCE_RESULT \ FEATURE_INTEGRATION_RESULT DOCKER_E2E_RESULT VM_E2E_RESULT PROTO_COMPATIBILITY_RESULT -if [[ -n "${proto_exception_tag}" ]]; then - [[ "${IS_PRERELEASE}" == true && "${proto_exception_tag}" == "${RELEASE_TAG}" ]] || - die "protobuf exception must match the prerelease tag" - [[ -n "${proto_exception_reason//[[:space:]]/}" ]] || die "protobuf exception requires a reason" -fi - for result in \ "${PROTO_COMPATIBILITY_RESULT}" \ "${SECURITY_RESULT}" \ @@ -61,8 +53,6 @@ jq -n \ --arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --arg security "${SECURITY_RESULT}" \ --arg protobuf_compatibility "${PROTO_COMPATIBILITY_RESULT}" \ - --arg proto_exception_tag "${proto_exception_tag}" \ - --arg proto_exception_reason "${proto_exception_reason}" \ --arg conformance "${CONFORMANCE_RESULT}" \ --arg feature_integration "${FEATURE_INTEGRATION_RESULT}" \ --arg docker_e2e "${DOCKER_E2E_RESULT}" \ @@ -82,11 +72,6 @@ jq -n \ ] }, current_profile_passed: $current_profile_passed, - exceptions: (if $proto_exception_tag == "" then [] else [{ - suite: "protobuf_compatibility", - tag: $proto_exception_tag, - reason: $proto_exception_reason - }] end), run: { id: $run_id, attempt: $run_attempt,