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 RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ python3 scripts/release/notes.py docs X.Y.Z --ref vX.Y.Z --mdx ../tablepro-docs/

A `v*` tag runs `release.yml` and nothing else. A run is never cancelled, and every job reads the version, build and channel from `verify`.

- `verify`: `version.py check` refuses a tag that breaks the grammar or the bounds; that `origin/release/<X.Y>` does not contain; whose previous tag on the line is not its ancestor; a stable X.Y.0 with no published beta, or a beta after its X.Y.0; a `CHANGELOG.md` missing the stable's heading, or a published stable of this or the previous line below it; a stable whose tag still holds fragments in `changelog.d/`. Then it needs macOS Tests, iOS and Package Tests and Release Build green on the tag commit or the nearest first-parent commit that ran them, waiting on a pending run. Last, it writes the notes.
- `verify`: `version.py check` refuses a tag that breaks the grammar or the bounds; that `origin/release/<X.Y>` does not contain; whose previous tag on the line is not its ancestor; a stable X.Y.0 with no published beta, or a beta after its X.Y.0; a `CHANGELOG.md` missing the stable's heading, or a published stable of this or the previous line below it; a stable whose tag still holds fragments in `changelog.d/`. Then it needs macOS Tests, iOS and Package Tests and Release Build green on the tag commit or the nearest first-parent commit that ran them, with no change to the files they read after it, waiting on a pending run. Last, it writes the notes.
- `app` (`signing`): a GA Xcode for a stable (`setup-xcode` `require-release`), the CloudKit Production schema check, `build.sh app` (archive, export with `scripts/release/ExportOptions.plist`, universal and plugin load checks), `notarize.sh` on the app, `package.sh app`, `notarize.sh` on the DMG.
- `plugins` (`signing`): the Dameng and HANA bridges, `build.sh plugins` (each target `registry.py targets` prints, universal, signed with Developer ID, the hardened runtime and a timestamp), one notary submission for all, `package.sh plugins` (the zips, described as the manifest will read them).
- `github-release`: `actions/attest` attests `dist/*`; `gh release create` drafts, uploads and publishes; `gh release verify` and `verify-asset` check the published release.
Expand Down
28 changes: 27 additions & 1 deletion scripts/release/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,15 @@ def setUp(self):
self.logged = []
self.diffs = []
self.changed_suites = set()
self.changed_bases = set()
self.base = self.repo.commit("0.81.0", MAIN_CHANGELOG)
self.repo.git("tag", "v0.81.0")
self.cut = self.repo.commit("feat: something")
self.repo.line("0.82", self.cut)

def changed(self, base, suite):
self.diffs.append((base, suite))
return suite in self.changed_suites
return suite in self.changed_suites or (base, suite) in self.changed_bases

def check(self, tag, github):
if self.git.exists(f"refs/tags/{tag}"):
Expand Down Expand Up @@ -229,6 +230,31 @@ def test_a_success_on_another_branch_run_of_the_same_commit_counts(self):
github = FakeGitHub(releases=[("v0.81.0", False)], runs=runs, jobs=jobs)
self.assertEqual(self.check("v0.82.0-beta.1", github), [])

def test_skipped_commits_past_fifty_still_borrow_the_last_green_run(self):
tail = self.cut
for index in range(60):
tail = self.repo.commit(f"feat(linux): change {index}")
self.repo.line("0.82", tail)
self.repo.git("tag", "v0.82.0-beta.1", tail)
runs, jobs = green_on(self.cut)
github = FakeGitHub(releases=[("v0.81.0", False)], runs=runs, jobs=jobs)
self.assertEqual(self.check("v0.82.0-beta.1", github), [])
self.assertIn(f"macOS Tests: green on {self.cut}", self.logged)

def test_the_walk_stops_at_the_newest_change_to_the_files_a_job_reads(self):
between = self.repo.commit("feat: before the change")
touch = self.repo.commit("feat: changes the app")
tail = self.repo.commit("docs: after the change")
self.repo.line("0.82", tail)
self.repo.git("tag", "v0.82.0-beta.1", tail)
self.changed_bases = {(between, "macos")}
runs, jobs = green_on(self.cut)
github = FakeGitHub(releases=[("v0.81.0", False)], runs=runs, jobs=jobs)
self.assertEqual(self.check("v0.82.0-beta.1", github),
[f"no macOS Tests run that was not skipped on {touch}, which changed files it reads, "
f"or after it"])
self.assertNotIn(("macos-tests.yml", self.cut), github.run_queries)

def test_no_run_in_the_walk_refuses_the_tag(self):
self.repo.git("tag", "v0.82.0-beta.1", self.cut)
runs, jobs = green_on(self.cut)
Expand Down
41 changes: 26 additions & 15 deletions scripts/release/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
)
DETECTOR = "scripts/ci/detect-changed-paths.sh"
FRAGMENTS = "changelog.d/"
WALK_DEPTH = 50
POLL_SECONDS = 60
PENDING_STATUSES = {"queued", "in_progress", "waiting", "requested", "pending"}

Expand Down Expand Up @@ -131,8 +130,8 @@ def show(self, ref: str, path: str) -> Optional[str]:
def list_tree(self, ref: str, path: str) -> List[str]:
return self._git("ls-tree", "--name-only", ref, "--", path).stdout.splitlines()

def first_parents(self, ref: str, limit: int) -> List[str]:
return self._git("rev-list", "--first-parent", f"--max-count={limit}", f"{ref}^{{commit}}").stdout.split()
def first_parents(self, ref: str) -> List[str]:
return self._git("rev-list", "--first-parent", f"{ref}^{{commit}}").stdout.split()


class GitHub:
Expand Down Expand Up @@ -278,25 +277,37 @@ def ci_errors(git: Git, github: GitHub, version: Version, changed: Callable[[str
tag_commit = git.commit(version.tag)
if git.commit("HEAD") != tag_commit:
return [f"check out {version.tag} first: the coverage check diffs against HEAD"]
shas = git.first_parents(version.tag, WALK_DEPTH)
shas = git.first_parents(version.tag)
errors = []
for workflow, job, suite in REQUIRED_RUNS:
try:
green = next((sha for sha in shas if is_green(github, workflow, job, sha, sleep, log)), None)
error = coverage_error(github, workflow, job, suite, shas, version.tag, changed, sleep, log)
except CheckFailure as failure:
errors.append(str(failure))
continue
if green is None:
errors.append(f"no {job} run that was not skipped among the {len(shas)} first-parent commits "
f"of {version.tag}")
# A commit with no run of its own would otherwise borrow a verdict that never covered it.
elif green != tag_commit and changed(green, suite):
errors.append(f"{job} passed on {green}, but files it reads changed before {version.tag}")
else:
log(f"{job}: green on {green}")
error = str(failure)
if error is not None:
errors.append(error)
return errors


def coverage_error(github: GitHub, workflow: str, job: str, suite: str, shas: List[str], tag: str,
changed: Callable[[str, str], bool], sleep: Callable[[float], None],
log: Callable[[str], None]) -> Optional[str]:
# The walk ends at the newest commit that changed the files the job reads: a run on any
# commit before it never covered that change, however many skipped commits follow it.
newer = None
for sha in shas:
is_tag = newer is None
if is_green(github, workflow, job, sha, sleep, log):
if not is_tag and changed(sha, suite):
return f"{job} passed on {sha}, but files it reads changed before {tag}"
log(f"{job}: green on {sha}")
return None
if not is_tag and changed(sha, suite):
return f"no {job} run that was not skipped on {newer}, which changed files it reads, or after it"
newer = sha
return f"no {job} run that was not skipped among the {len(shas)} first-parent commits of {tag}"


def check(tag: str, git: Git, github: GitHub, changed: Optional[Callable[[str, str], bool]] = None,
sleep: Callable[[float], None] = time.sleep, log: Callable[[str], None] = print) -> List[str]:
try:
Expand Down
Loading