Skip to content

Commit ac7f615

Browse files
authored
fix(ci): allow accepted protobuf breaks for v0.1.6 prereleases (#4976)
* fix(ci): allow accepted protobuf breaks for one prerelease Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * fix(ci): cover all v0.1.6 prerelease candidates Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * docs(ci): document full rollback of temporary protobuf exception Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * fix(ci): complete protobuf test copyright header Signed-off-by: Taylor Mutch <taylormutch@gmail.com> --------- Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
1 parent dedb089 commit ac7f615

7 files changed

Lines changed: 240 additions & 1 deletion

File tree

‎.agents/skills/release-captain/SKILL.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ tag, or start a second `Pre-release` run for a tag; it has no concurrency guard.
4242
picks the highest `-pre.N` on `main` whose whole `Pre-release` run passed,
4343
creates the stable tag on that commit, and publishes without requalifying.
4444

45+
### Patch-train protobuf exception
46+
47+
All `v0.1.6-pre.N` candidates have a release-captain-approved protobuf compatibility exception
48+
in `release-qualification.yml`. The check still compiles both protobuf snapshots
49+
and runs Buf; only detected breaking changes are allowed. Compilation and tooling
50+
errors still fail. Other versions retain normal version-based enforcement.
51+
The run summary and qualification JSON record the exception and reason. Review
52+
all protobuf findings for each candidate and include migration guidance in the
53+
release notes; the exception does not accept other compatibility-review findings.
54+
Squash-merge PR #4976 and, after verifying v0.1.6 publication, prepare a cleanup PR
55+
reverting its entire squash-merge commit. Follow "Remove the v0.1.6 exception" in
56+
`CI.md`; remove the checker support and metadata along with the workflow exception.
57+
A new prerelease number
58+
within this patch train retains the exception; other patch or minor trains do not.
59+
4560
## Setup
4661

4762
```shell

‎.agents/skills/watch-github-actions/SKILL.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ the shared `check-protobuf-compatibility` action with `nix run .#check-protobuf-
169169
During `0.x`, a minor train permits compatibility findings
170170
as warnings; a patch train or no active train rejects them. Compare the current
171171
train's version with the latest stable release; commit messages are irrelevant.
172+
For the patch-train `v0.1.6-pre.N` exception, inspect the run summary and the
173+
qualification JSON's `exceptions` array; see `release-captain` for disposition.
172174
Compilation, baseline, and tool errors remain fatal. The `protobuf_compatibility` suite participates in
173175
the `release-tag-v1` qualification profile. Pre-release calls the reusable
174176
`release-qualification.yml` workflow after building, in parallel with publication. Release

‎.github/workflows/release-qualification.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ permissions:
3030

3131
env:
3232
RELEASE_TAG: ${{ inputs.release-tag }}
33+
# TEMPORARY (#4976): squash-merge and revert after v0.1.6 publication is verified.
34+
# See CI.md's "Remove the v0.1.6 exception" procedure.
35+
PROTO_COMPATIBILITY_EXCEPTION_TAG: ${{ inputs.is-prerelease && startsWith(inputs.release-tag, 'v0.1.6-pre.') && inputs.release-tag || '' }}
36+
PROTO_COMPATIBILITY_EXCEPTION_REASON: Release captain accepted protobuf breaking changes for the v0.1.6 prerelease train; migration guidance must accompany the release.
3337

3438
defaults:
3539
run:
@@ -212,6 +216,11 @@ jobs:
212216
echo "| Docker E2E | ${DOCKER_E2E_RESULT} |"
213217
echo "| VM E2E | ${VM_E2E_RESULT} |"
214218
echo
219+
if [[ -n "${PROTO_COMPATIBILITY_EXCEPTION_TAG}" ]]; then
220+
echo "- Protobuf compatibility exception: \`${PROTO_COMPATIBILITY_EXCEPTION_TAG}\`"
221+
echo "- Reason: ${PROTO_COMPATIBILITY_EXCEPTION_REASON}"
222+
echo
223+
fi
215224
echo "Advisory compatibility findings do not affect the qualification result."
216225
echo "See the Compatibility Review job and compatibility-review-run-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT} artifact in [this run](${run_url})."
217226
} >> "${GITHUB_STEP_SUMMARY}"

‎CI.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ remains in Actions. This checks
4848
protobuf compatibility; SDK/configuration compatibility and migration review
4949
remain separate qualification work.
5050

51+
The release qualification workflow has a release-captain-approved exception
52+
for all `v0.1.6-pre.N` candidates, including replacement candidates after a failed
53+
run. It still runs Buf and fails on compilation or tooling
54+
errors, but accepts detected protobuf breaking changes for that tag. The run
55+
summary and qualification JSON's `exceptions` array record the scope and reason.
56+
Other versions retain the normal version policy. Remove the workflow exception
57+
after promotion; release notes must describe the accepted changes and migration.
58+
5159
Fetch the target and tags to check committed branch changes locally, replacing
5260
`origin/main` for another target:
5361

@@ -67,6 +75,23 @@ For an intentional minor-train incompatibility, record the Buf finding,
6775
linked issue, consumer impact, and migration plan in the PR. Keep the finding
6876
visible; do not disable the job or add a broad Buf ignore rule.
6977

78+
### Remove the v0.1.6 exception
79+
80+
Squash-merge [PR #4976](https://github.com/NVIDIA/OpenShell/pull/4976) so the
81+
exception, checker support, qualification metadata, tests, and temporary guidance
82+
can be removed together with one revert. After verifying stable v0.1.6 publication,
83+
open a cleanup PR from current `main` and revert that PR's squash-merge commit:
84+
85+
```shell
86+
git revert --no-commit <PR-4976-squash-merge-sha>
87+
git commit --signoff -m "revert(ci): remove temporary v0.1.6 protobuf exception"
88+
```
89+
90+
Use the final squash-merge SHA shown on the merged PR, not a branch commit SHA.
91+
Validate the reverted workflow with Actionlint and run the normal protobuf check
92+
before merging the cleanup. Reverting the whole PR restores normal enforcement
93+
and removes the exception mechanism; do not leave the bypass support behind.
94+
7095
### Windows validation
7196

7297
Native Windows implementation and releases use the

‎tasks/scripts/check_proto_compatibility.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ def compare(candidate: str, baseline: str, allows_breaks: bool) -> int:
103103
if result.returncode == 100 and allows_breaks:
104104
print(diagnostics.replace("::error ", "::warning "), end="")
105105
print(
106-
"Breaking changes allowed by the version increment; review migration guidance."
106+
"Breaking changes allowed by the version policy or an explicit "
107+
"maintainer exception; review migration guidance."
107108
)
108109
return 0
109110
print(diagnostics, end="")
@@ -122,6 +123,15 @@ def main() -> int:
122123
)
123124
release = ref.removeprefix("refs/tags/") if ref.startswith("refs/tags/") else None
124125
stable, train, allows_breaks = train_policy(candidate, release)
126+
exception_tag = os.environ.get("PROTO_COMPATIBILITY_EXCEPTION_TAG", "")
127+
if exception_tag:
128+
if release != exception_tag or not _parse_prerelease_tag(exception_tag):
129+
raise ValueError("Protobuf exception must match the exact prerelease tag.")
130+
reason = os.environ.get("PROTO_COMPATIBILITY_EXCEPTION_REASON", "").strip()
131+
if not reason:
132+
raise ValueError("Protobuf exception requires a reason.")
133+
print(f"Protobuf compatibility exception for {release}: {reason}", flush=True)
134+
allows_breaks = True
125135
baseline = f"refs/tags/{stable}" if release else candidate
126136
if not release:
127137
# merge-tree writes only Git objects, leaving HEAD and the worktree
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import json
5+
import os
6+
import shutil
7+
import subprocess
8+
import sys
9+
from pathlib import Path
10+
from unittest.mock import patch
11+
12+
import pytest
13+
14+
sys.path.insert(0, str(Path(__file__).resolve().parent))
15+
16+
import check_proto_compatibility as compatibility
17+
18+
TAG = "v0.1.6-pre.7"
19+
REASON = "Release captain accepted the protobuf changes."
20+
21+
22+
@pytest.mark.parametrize(
23+
("ref", "exception_tag", "reason", "expected"),
24+
[
25+
(f"refs/tags/{TAG}", TAG, REASON, True),
26+
("refs/tags/v0.1.6-pre.8", "v0.1.6-pre.8", REASON, True),
27+
("refs/tags/v0.1.6-pre.12", "v0.1.6-pre.12", REASON, True),
28+
("refs/tags/v0.1.6-pre.8", "", "", False),
29+
("refs/tags/v0.1.7-pre.1", "", "", False),
30+
("refs/tags/v0.2.0-pre.1", "", "", False),
31+
(f"refs/tags/{TAG}", "", "", False),
32+
("refs/tags/v0.1.6-pre.8", TAG, REASON, ValueError),
33+
("refs/tags/v0.1.6", "v0.1.6", REASON, ValueError),
34+
("refs/heads/main", TAG, REASON, ValueError),
35+
(f"refs/tags/{TAG}", TAG, " ", ValueError),
36+
],
37+
)
38+
def test_exception_scope(monkeypatch, ref, exception_tag, reason, expected):
39+
monkeypatch.setenv("PROTO_COMPATIBILITY_EXCEPTION_TAG", exception_tag)
40+
monkeypatch.setenv("PROTO_COMPATIBILITY_EXCEPTION_REASON", reason)
41+
monkeypatch.setattr(sys, "argv", ["check", ref])
42+
with (
43+
patch.object(compatibility, "git", side_effect=[ref, "candidate-sha"]),
44+
patch.object(
45+
compatibility, "train_policy", return_value=("v0.1.5", "v0.1.6", False)
46+
),
47+
patch.object(compatibility, "compare", return_value=0) as compare,
48+
):
49+
if expected is ValueError:
50+
with pytest.raises(ValueError):
51+
compatibility.main()
52+
compare.assert_not_called()
53+
else:
54+
assert compatibility.main() == 0
55+
assert compare.call_args.args[2] is expected
56+
57+
58+
@pytest.mark.parametrize(
59+
("allows_breaks", "exit_code", "expected"),
60+
[(False, 100, 100), (True, 100, 0), (True, 1, 1), (True, 0, 0)],
61+
)
62+
def test_exception_only_accepts_breaking_diagnostics(
63+
allows_breaks, exit_code, expected
64+
):
65+
with (
66+
patch.object(compatibility, "export_proto"),
67+
patch.object(
68+
compatibility.subprocess,
69+
"run",
70+
side_effect=[
71+
subprocess.CompletedProcess([], 0),
72+
subprocess.CompletedProcess([], 0),
73+
subprocess.CompletedProcess([], exit_code, stdout="", stderr=""),
74+
],
75+
) as run,
76+
):
77+
assert compatibility.compare("candidate", "baseline", allows_breaks) == expected
78+
assert [call.args[0][1] for call in run.call_args_list] == [
79+
"build",
80+
"build",
81+
"breaking",
82+
]
83+
84+
85+
@pytest.mark.parametrize("failed_snapshot", [0, 1])
86+
def test_exception_does_not_accept_invalid_protobufs(failed_snapshot):
87+
results = [subprocess.CompletedProcess([], 0)] * failed_snapshot
88+
results.append(subprocess.CalledProcessError(100, ["buf", "build"]))
89+
with (
90+
patch.object(compatibility, "export_proto"),
91+
patch.object(compatibility.subprocess, "run", side_effect=results) as run,
92+
pytest.raises(subprocess.CalledProcessError),
93+
):
94+
compatibility.compare("candidate", "baseline", True)
95+
assert run.call_count == failed_snapshot + 1
96+
97+
98+
@pytest.mark.skipif(shutil.which("buf") is None, reason="Buf is not installed")
99+
@pytest.mark.parametrize("allows_breaks", [False, True])
100+
def test_real_buf_field_rename(monkeypatch, allows_breaks):
101+
def export_fixture(ref, destination):
102+
(destination / "proto").mkdir(parents=True)
103+
(destination / "buf.yaml").write_text(
104+
"version: v2\nmodules:\n - path: proto\nbreaking:\n use:\n - FILE\n"
105+
)
106+
field = "old_name" if ref == "baseline" else "new_name"
107+
(destination / "proto" / "test.proto").write_text(
108+
'syntax = "proto3";\npackage test.v1;\n'
109+
f"message Request {{ bool {field} = 3; }}\n"
110+
)
111+
112+
monkeypatch.setattr(compatibility, "export_proto", export_fixture)
113+
assert compatibility.compare("candidate", "baseline", allows_breaks) == (
114+
0 if allows_breaks else 100
115+
)
116+
117+
118+
@pytest.mark.parametrize("exception", [False, True])
119+
@pytest.mark.parametrize("proto_result", ["success", "failure", "skipped"])
120+
@pytest.mark.parametrize("tag", [TAG, "v0.1.6-pre.8"])
121+
def test_qualification_records_exception_without_bypassing_results(
122+
tmp_path, exception, proto_result, tag
123+
):
124+
output = tmp_path / "qualification.json"
125+
env = {
126+
**os.environ,
127+
"RELEASE_TAG": tag,
128+
"SOURCE_SHA": "candidate-sha",
129+
"IS_PRERELEASE": "true",
130+
"GITHUB_RUN_ID": "123",
131+
"GITHUB_RUN_ATTEMPT": "1",
132+
"GITHUB_SERVER_URL": "https://github.com",
133+
"GITHUB_REPOSITORY": "NVIDIA/OpenShell",
134+
"PROTO_COMPATIBILITY_RESULT": proto_result,
135+
"PROTO_COMPATIBILITY_EXCEPTION_TAG": tag if exception else "",
136+
"PROTO_COMPATIBILITY_EXCEPTION_REASON": REASON,
137+
}
138+
for suite in (
139+
"SECURITY",
140+
"CONFORMANCE",
141+
"FEATURE_INTEGRATION",
142+
"DOCKER_E2E",
143+
"VM_E2E",
144+
):
145+
env[f"{suite}_RESULT"] = "success"
146+
subprocess.run(
147+
[
148+
"bash",
149+
str(Path(__file__).with_name("generate-qualification-summary.sh")),
150+
str(output),
151+
],
152+
env=env,
153+
check=True,
154+
capture_output=True,
155+
text=True,
156+
)
157+
summary = json.loads(output.read_text())
158+
assert summary["current_profile_passed"] is (proto_result == "success")
159+
assert summary["exceptions"] == (
160+
[{"suite": "protobuf_compatibility", "tag": tag, "reason": REASON}]
161+
if exception
162+
else []
163+
)

‎tasks/scripts/generate-qualification-summary.sh‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,20 @@ require_env() {
2222
[[ $# -eq 1 ]] || die "usage: $0 OUTPUT"
2323
output=$1
2424
current_profile_passed=true
25+
proto_exception_tag=${PROTO_COMPATIBILITY_EXCEPTION_TAG:-}
26+
proto_exception_reason=${PROTO_COMPATIBILITY_EXCEPTION_REASON:-}
2527

2628
require_env \
2729
RELEASE_TAG SOURCE_SHA IS_PRERELEASE GITHUB_RUN_ID GITHUB_RUN_ATTEMPT \
2830
GITHUB_SERVER_URL GITHUB_REPOSITORY SECURITY_RESULT CONFORMANCE_RESULT \
2931
FEATURE_INTEGRATION_RESULT DOCKER_E2E_RESULT VM_E2E_RESULT PROTO_COMPATIBILITY_RESULT
3032

33+
if [[ -n "${proto_exception_tag}" ]]; then
34+
[[ "${IS_PRERELEASE}" == true && "${proto_exception_tag}" == "${RELEASE_TAG}" ]] ||
35+
die "protobuf exception must match the prerelease tag"
36+
[[ -n "${proto_exception_reason//[[:space:]]/}" ]] || die "protobuf exception requires a reason"
37+
fi
38+
3139
for result in \
3240
"${PROTO_COMPATIBILITY_RESULT}" \
3341
"${SECURITY_RESULT}" \
@@ -53,6 +61,8 @@ jq -n \
5361
--arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
5462
--arg security "${SECURITY_RESULT}" \
5563
--arg protobuf_compatibility "${PROTO_COMPATIBILITY_RESULT}" \
64+
--arg proto_exception_tag "${proto_exception_tag}" \
65+
--arg proto_exception_reason "${proto_exception_reason}" \
5666
--arg conformance "${CONFORMANCE_RESULT}" \
5767
--arg feature_integration "${FEATURE_INTEGRATION_RESULT}" \
5868
--arg docker_e2e "${DOCKER_E2E_RESULT}" \
@@ -72,6 +82,11 @@ jq -n \
7282
]
7383
},
7484
current_profile_passed: $current_profile_passed,
85+
exceptions: (if $proto_exception_tag == "" then [] else [{
86+
suite: "protobuf_compatibility",
87+
tag: $proto_exception_tag,
88+
reason: $proto_exception_reason
89+
}] end),
7590
run: {
7691
id: $run_id,
7792
attempt: $run_attempt,

0 commit comments

Comments
 (0)