divejson 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- divejson-0.4.0/.github/scripts/release_bump.py +276 -0
- divejson-0.4.0/.github/workflows/ci.yml +119 -0
- divejson-0.4.0/.github/workflows/pr-title.yml +65 -0
- divejson-0.4.0/.github/workflows/release-bump.yml +281 -0
- divejson-0.4.0/.github/workflows/release.yml +65 -0
- divejson-0.4.0/.gitignore +9 -0
- divejson-0.4.0/CHANGELOG.md +243 -0
- divejson-0.4.0/CONTRIBUTING.md +181 -0
- divejson-0.4.0/LICENSE +21 -0
- divejson-0.4.0/PKG-INFO +293 -0
- divejson-0.4.0/README.md +274 -0
- divejson-0.4.0/SPEC_REF +1 -0
- divejson-0.4.0/divejson/__init__.py +120 -0
- divejson-0.4.0/divejson/cli.py +414 -0
- divejson-0.4.0/divejson/conform.py +544 -0
- divejson-0.4.0/divejson/converter.py +600 -0
- divejson-0.4.0/divejson/fit.py +1319 -0
- divejson-0.4.0/divejson/py.typed +0 -0
- divejson-0.4.0/divejson/registry.py +474 -0
- divejson-0.4.0/divejson/series.py +195 -0
- divejson-0.4.0/divejson/ssrf.py +698 -0
- divejson-0.4.0/divejson/suunto_json.py +1199 -0
- divejson-0.4.0/divejson/suunto_xml.py +1003 -0
- divejson-0.4.0/divejson/uddf.py +1163 -0
- divejson-0.4.0/divejson/uddf_write.py +1127 -0
- divejson-0.4.0/divejson/validate.py +410 -0
- divejson-0.4.0/divejson/xmlsource.py +201 -0
- divejson-0.4.0/docs/converting.md +334 -0
- divejson-0.4.0/docs/fit-mapping.md +485 -0
- divejson-0.4.0/docs/ssrf-mapping.md +336 -0
- divejson-0.4.0/docs/suunto-json-mapping.md +492 -0
- divejson-0.4.0/docs/suunto-xml-mapping.md +495 -0
- divejson-0.4.0/docs/uddf-mapping.md +365 -0
- divejson-0.4.0/docs/uddf-writing.md +381 -0
- divejson-0.4.0/fixtures/README.md +147 -0
- divejson-0.4.0/fixtures/fit/suunto-d5.divejson +850 -0
- divejson-0.4.0/fixtures/fit/suunto-d5.fit +0 -0
- divejson-0.4.0/fixtures/fit/suunto-ocean.divejson +9508 -0
- divejson-0.4.0/fixtures/fit/suunto-ocean.fit +0 -0
- divejson-0.4.0/fixtures/invalid/agency-other-missing.divejson +459 -0
- divejson-0.4.0/fixtures/invalid/avg-depth-exceeds-max.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/bad-version.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/bbox-missing-corner.divejson +459 -0
- divejson-0.4.0/fixtures/invalid/bbox-south-exceeds-north.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/bbox-without-position.divejson +456 -0
- divejson-0.4.0/fixtures/invalid/channel-length-mismatch.divejson +461 -0
- divejson-0.4.0/fixtures/invalid/course-dates-reversed.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/dangling-reference.divejson +461 -0
- divejson-0.4.0/fixtures/invalid/duplicate-json-member.divejson +6 -0
- divejson-0.4.0/fixtures/invalid/duplicate-uuid.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/event-other-without-label.divejson +459 -0
- divejson-0.4.0/fixtures/invalid/missing-format.divejson +459 -0
- divejson-0.4.0/fixtures/invalid/naive-exported-at.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/non-increasing-samples.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/null-member.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/oxygen-helium-sum.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/position-incomplete.divejson +459 -0
- divejson-0.4.0/fixtures/invalid/pressure-order.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/profile-duration-short.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/species-no-identity.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/trailing-newline-datetime.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/trip-dates-reversed.divejson +460 -0
- divejson-0.4.0/fixtures/invalid/undefined-member.divejson +461 -0
- divejson-0.4.0/fixtures/invalid/version-before-format.divejson +5 -0
- divejson-0.4.0/fixtures/invalid/version-not-second.divejson +5 -0
- divejson-0.4.0/fixtures/ssrf/refusals.divejson +94 -0
- divejson-0.4.0/fixtures/ssrf/refusals.ssrf +39 -0
- divejson-0.4.0/fixtures/ssrf/subsurface.divejson +131 -0
- divejson-0.4.0/fixtures/ssrf/subsurface.ssrf +42 -0
- divejson-0.4.0/fixtures/ssrf/trip-grouping.divejson +80 -0
- divejson-0.4.0/fixtures/ssrf/trip-grouping.ssrf +33 -0
- divejson-0.4.0/fixtures/suunto_json/header-only.divejson +27 -0
- divejson-0.4.0/fixtures/suunto_json/header-only.json +15 -0
- divejson-0.4.0/fixtures/suunto_json/not-a-dive.divejson +18 -0
- divejson-0.4.0/fixtures/suunto_json/not-a-dive.json +17 -0
- divejson-0.4.0/fixtures/suunto_json/purged-regulator.divejson +73 -0
- divejson-0.4.0/fixtures/suunto_json/purged-regulator.json +50 -0
- divejson-0.4.0/fixtures/suunto_json/suunto-d5.divejson +123 -0
- divejson-0.4.0/fixtures/suunto_json/suunto-d5.json +76 -0
- divejson-0.4.0/fixtures/suunto_json/suunto-ocean.divejson +114 -0
- divejson-0.4.0/fixtures/suunto_json/suunto-ocean.json +111 -0
- divejson-0.4.0/fixtures/suunto_xml/freedive.divejson +18 -0
- divejson-0.4.0/fixtures/suunto_xml/freedive.xml +128 -0
- divejson-0.4.0/fixtures/suunto_xml/nitrox-deco.divejson +145 -0
- divejson-0.4.0/fixtures/suunto_xml/nitrox-deco.xml +198 -0
- divejson-0.4.0/fixtures/suunto_xml/refusals.divejson +80 -0
- divejson-0.4.0/fixtures/suunto_xml/refusals.xml +154 -0
- divejson-0.4.0/fixtures/suunto_xml/suunto-d5.divejson +92 -0
- divejson-0.4.0/fixtures/suunto_xml/suunto-d5.xml +169 -0
- divejson-0.4.0/fixtures/suunto_xml/unlabelled-pressure.divejson +97 -0
- divejson-0.4.0/fixtures/suunto_xml/unlabelled-pressure.xml +119 -0
- divejson-0.4.0/fixtures/uddf/divelogs.divejson +95 -0
- divejson-0.4.0/fixtures/uddf/divelogs.uddf +112 -0
- divejson-0.4.0/fixtures/uddf/legacy-writer.divejson +64 -0
- divejson-0.4.0/fixtures/uddf/legacy-writer.uddf +66 -0
- divejson-0.4.0/fixtures/uddf/mix-only-cylinder.divejson +67 -0
- divejson-0.4.0/fixtures/uddf/mix-only-cylinder.uddf +70 -0
- divejson-0.4.0/fixtures/uddf/opendiving.divejson +197 -0
- divejson-0.4.0/fixtures/uddf/opendiving.uddf +161 -0
- divejson-0.4.0/fixtures/uddf/subsurface.divejson +129 -0
- divejson-0.4.0/fixtures/uddf/subsurface.uddf +163 -0
- divejson-0.4.0/fixtures/valid/demo-logbook.divejson +4164 -0
- divejson-0.4.0/fixtures/valid/minimal.divejson +5 -0
- divejson-0.4.0/fixtures/valid/technical-dive.divejson +465 -0
- divejson-0.4.0/fixtures/write/uddf/opendiving.divejson +197 -0
- divejson-0.4.0/fixtures/write/uddf/opendiving.uddf +161 -0
- divejson-0.4.0/fixtures/write/uddf/technical-dive.divejson +465 -0
- divejson-0.4.0/fixtures/write/uddf/technical-dive.uddf +250 -0
- divejson-0.4.0/pyproject.toml +67 -0
- divejson-0.4.0/schema/1.0/divejson.schema.json +450 -0
- divejson-0.4.0/tests/fitbuild.py +312 -0
- divejson-0.4.0/tests/fixtures/README.md +33 -0
- divejson-0.4.0/tests/fixtures/uddf_3.2.2.xsd +1888 -0
- divejson-0.4.0/tests/helpers.py +179 -0
- divejson-0.4.0/tests/test_cli.py +274 -0
- divejson-0.4.0/tests/test_conform.py +427 -0
- divejson-0.4.0/tests/test_converter.py +187 -0
- divejson-0.4.0/tests/test_fit_fixtures.py +203 -0
- divejson-0.4.0/tests/test_fit_parsing.py +838 -0
- divejson-0.4.0/tests/test_fit_units.py +293 -0
- divejson-0.4.0/tests/test_package.py +89 -0
- divejson-0.4.0/tests/test_registry.py +462 -0
- divejson-0.4.0/tests/test_release_bump.py +339 -0
- divejson-0.4.0/tests/test_series.py +205 -0
- divejson-0.4.0/tests/test_ssrf_fixtures.py +205 -0
- divejson-0.4.0/tests/test_ssrf_parsing.py +380 -0
- divejson-0.4.0/tests/test_ssrf_units.py +198 -0
- divejson-0.4.0/tests/test_suunto_json_fixtures.py +326 -0
- divejson-0.4.0/tests/test_suunto_json_parsing.py +571 -0
- divejson-0.4.0/tests/test_suunto_json_units.py +235 -0
- divejson-0.4.0/tests/test_suunto_xml_fixtures.py +131 -0
- divejson-0.4.0/tests/test_suunto_xml_parsing.py +628 -0
- divejson-0.4.0/tests/test_suunto_xml_units.py +230 -0
- divejson-0.4.0/tests/test_uddf_fixtures.py +125 -0
- divejson-0.4.0/tests/test_uddf_parsing.py +528 -0
- divejson-0.4.0/tests/test_uddf_units.py +177 -0
- divejson-0.4.0/tests/test_uddf_write_fixtures.py +344 -0
- divejson-0.4.0/tests/test_uddf_writing.py +524 -0
- divejson-0.4.0/tests/test_xmlsource.py +123 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Work out what version a release is, and write it into the two files that carry it.
|
|
3
|
+
|
|
4
|
+
`.github/workflows/release-bump.yml` runs this over a checkout of `main`; the two
|
|
5
|
+
rewrites then land through a pull request and the tag goes on the commit that lands. It
|
|
6
|
+
is a script beside the workflow rather than part of the package, because nothing a `pip
|
|
7
|
+
install divejson` gets has any business knowing how this repository cuts a release.
|
|
8
|
+
|
|
9
|
+
The version is computed from the conventional-commit subjects since the last `v*` tag,
|
|
10
|
+
which — the repository being squash-only with `PR_TITLE` as the subject — are the titles
|
|
11
|
+
of the pull requests in the window. **Below 1.0 a breaking change moves the minor**: 0.x
|
|
12
|
+
has no major to spend, and the promise 0.x makes is precisely that a minor may break.
|
|
13
|
+
From 1.0.0 the rule is ordinary semver.
|
|
14
|
+
|
|
15
|
+
Everything it will not do is a refusal rather than a guess, because every one of them is
|
|
16
|
+
a release that is wrong in a way nobody would notice until PyPI had it:
|
|
17
|
+
|
|
18
|
+
- an empty `## Unreleased` section publishes a version whose notes say nothing;
|
|
19
|
+
- a `__version__` that disagrees with the newest tag means a bump landed and its tag
|
|
20
|
+
never did, where the answer is to tag what is already there rather than to bump past
|
|
21
|
+
it, silently burning a version number;
|
|
22
|
+
- a version that is not ahead of the current one, or one whose tag already exists, is a
|
|
23
|
+
release PyPI would refuse after `main` had already moved.
|
|
24
|
+
|
|
25
|
+
Run it by hand to see what a release would be — it only touches the working tree, and
|
|
26
|
+
`git diff` is the whole report:
|
|
27
|
+
|
|
28
|
+
python3 .github/scripts/release_bump.py
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import subprocess
|
|
37
|
+
import sys
|
|
38
|
+
from collections.abc import Sequence
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
# The line hatchling reads the built version out of (`[tool.hatch.version]`), matched
|
|
42
|
+
# whole so that a `__version__` mentioned in a docstring or a comment cannot be the one
|
|
43
|
+
# that moves. `[^"\n]` and not `[^"]`: a class that admits a newline would match a
|
|
44
|
+
# `__version__ = "0.3.0` whose closing quote is on the next line, which is the shape a
|
|
45
|
+
# version string carrying a newline writes and the shape the read-back below has to catch.
|
|
46
|
+
VERSION_LINE = re.compile(r'^__version__ = "(?P<version>[^"\n]*)"$', re.MULTILINE)
|
|
47
|
+
|
|
48
|
+
# Three plain integers and nothing else. A pre-release or build-metadata suffix would
|
|
49
|
+
# make `bump` ambiguous and `release.yml`'s `dist/divejson-$version.tar.gz` check depend
|
|
50
|
+
# on how the build normalises it, so it is refused rather than half-supported.
|
|
51
|
+
#
|
|
52
|
+
# Used with `fullmatch` rather than anchored with `$`, because Python's `$` also matches
|
|
53
|
+
# *before* a trailing newline: `"0.3.0\n"` would pass an anchored `match`, be written into
|
|
54
|
+
# the version line as two physical lines, and leave a package that does not import.
|
|
55
|
+
SEMVER = re.compile(r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)")
|
|
56
|
+
|
|
57
|
+
# The same shape `.github/workflows/pr-title.yml` enforces on every title that becomes a
|
|
58
|
+
# subject here, with the type left open: that workflow owns the list of types, and a
|
|
59
|
+
# second copy of it in this file is a second place for it to go stale. What this needs
|
|
60
|
+
# from a subject is only whether it carries `!`, and whether its type is `feat` or `fix`.
|
|
61
|
+
SUBJECT = re.compile(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]*)\))?(?P<breaking>!)?: ")
|
|
62
|
+
|
|
63
|
+
UNRELEASED = "## Unreleased"
|
|
64
|
+
|
|
65
|
+
INIT_PATH = Path("divejson") / "__init__.py"
|
|
66
|
+
CHANGELOG_PATH = Path("CHANGELOG.md")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ReleaseError(Exception):
|
|
70
|
+
"""Something about the tree or the request means there is no release to cut."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_version(text: str) -> tuple[int, int, int]:
|
|
74
|
+
match = SEMVER.fullmatch(text)
|
|
75
|
+
if match is None:
|
|
76
|
+
raise ReleaseError(f"{text!r} is not a major.minor.patch version")
|
|
77
|
+
return int(match["major"]), int(match["minor"]), int(match["patch"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_version(init_text: str) -> str:
|
|
81
|
+
"""The version the package currently declares."""
|
|
82
|
+
found = VERSION_LINE.findall(init_text)
|
|
83
|
+
if len(found) != 1:
|
|
84
|
+
raise ReleaseError(f"{INIT_PATH} declares __version__ {len(found)} times, not once")
|
|
85
|
+
return found[0]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def bump_kind(subjects: Sequence[str]) -> str:
|
|
89
|
+
"""What the window as a whole asks for: `breaking`, `feature`, `fix` or `none`.
|
|
90
|
+
|
|
91
|
+
Subjects that are not conventional subjects contribute nothing. They exist — the
|
|
92
|
+
handful of commits that predate `pr-title.yml` — and one of them is not evidence
|
|
93
|
+
either way.
|
|
94
|
+
"""
|
|
95
|
+
types: set[str] = set()
|
|
96
|
+
for subject in subjects:
|
|
97
|
+
match = SUBJECT.match(subject)
|
|
98
|
+
if match is None:
|
|
99
|
+
continue
|
|
100
|
+
if match["breaking"]:
|
|
101
|
+
return "breaking"
|
|
102
|
+
types.add(match["type"])
|
|
103
|
+
if "feat" in types:
|
|
104
|
+
return "feature"
|
|
105
|
+
if "fix" in types:
|
|
106
|
+
return "fix"
|
|
107
|
+
return "none"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def next_version(current: str, subjects: Sequence[str]) -> str:
|
|
111
|
+
major, minor, patch = parse_version(current)
|
|
112
|
+
kind = bump_kind(subjects)
|
|
113
|
+
if major == 0:
|
|
114
|
+
# Below 1.0 a break has no major to move, and a minor is what 0.x already warns
|
|
115
|
+
# about — so breaking and feature land on the same answer, and only a window of
|
|
116
|
+
# fixes (or of changes that claim nothing at all) is a patch.
|
|
117
|
+
if kind in ("breaking", "feature"):
|
|
118
|
+
return f"0.{minor + 1}.0"
|
|
119
|
+
return f"0.{minor}.{patch + 1}"
|
|
120
|
+
if kind == "breaking":
|
|
121
|
+
return f"{major + 1}.0.0"
|
|
122
|
+
if kind == "feature":
|
|
123
|
+
return f"{major}.{minor + 1}.0"
|
|
124
|
+
return f"{major}.{minor}.{patch + 1}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def rewrite_version(init_text: str, version: str) -> str:
|
|
128
|
+
read_version(init_text) # exactly one line to move, or nothing is rewritten
|
|
129
|
+
return VERSION_LINE.sub(f'__version__ = "{version}"', init_text, count=1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def rewrite_changelog(changelog_text: str, version: str) -> str:
|
|
133
|
+
"""`## Unreleased` becomes `## <version>`, with a fresh empty `## Unreleased` above."""
|
|
134
|
+
lines = changelog_text.split("\n")
|
|
135
|
+
headings = [i for i, line in enumerate(lines) if line.rstrip() == UNRELEASED]
|
|
136
|
+
if len(headings) != 1:
|
|
137
|
+
raise ReleaseError(
|
|
138
|
+
f"{CHANGELOG_PATH} has {len(headings)} `{UNRELEASED}` headings, not one"
|
|
139
|
+
)
|
|
140
|
+
start = headings[0]
|
|
141
|
+
end = next(
|
|
142
|
+
(i for i in range(start + 1, len(lines)) if lines[i].startswith("## ")),
|
|
143
|
+
len(lines),
|
|
144
|
+
)
|
|
145
|
+
if not "\n".join(lines[start + 1 : end]).strip():
|
|
146
|
+
raise ReleaseError(
|
|
147
|
+
f"the `{UNRELEASED}` section of {CHANGELOG_PATH} is empty — a release with no "
|
|
148
|
+
"notes is not one anybody can read, so write the entries first"
|
|
149
|
+
)
|
|
150
|
+
lines[start : start + 1] = [UNRELEASED, "", f"## {version}"]
|
|
151
|
+
return "\n".join(lines)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def git(root: Path, *args: str) -> str:
|
|
155
|
+
result = subprocess.run(
|
|
156
|
+
["git", *args], cwd=root, capture_output=True, text=True, check=True
|
|
157
|
+
)
|
|
158
|
+
return result.stdout
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def last_tag(root: Path) -> str | None:
|
|
162
|
+
"""The newest `v*` tag reachable from HEAD, or None in a repository with no release.
|
|
163
|
+
|
|
164
|
+
Reachability rather than a sort over every tag: a tag on some other branch is not
|
|
165
|
+
what this history was released as, and would silently truncate the window.
|
|
166
|
+
"""
|
|
167
|
+
result = subprocess.run(
|
|
168
|
+
["git", "describe", "--tags", "--abbrev=0", "--match", "v*"],
|
|
169
|
+
cwd=root,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
)
|
|
173
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def subjects_since(root: Path, tag: str | None) -> list[str]:
|
|
177
|
+
span = f"{tag}..HEAD" if tag else "HEAD"
|
|
178
|
+
return [line for line in git(root, "log", "--format=%s", span).splitlines() if line]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def plan(root: Path, init_text: str, requested: str | None) -> tuple[str, str, str | None, list[str]]:
|
|
182
|
+
"""The whole decision, before a byte of the tree is touched."""
|
|
183
|
+
current = read_version(init_text)
|
|
184
|
+
parse_version(current)
|
|
185
|
+
|
|
186
|
+
tag = last_tag(root)
|
|
187
|
+
if tag is not None and tag != f"v{current}":
|
|
188
|
+
raise ReleaseError(
|
|
189
|
+
f"the tree declares {current} and the newest tag reachable is {tag}. A bump "
|
|
190
|
+
f"has landed whose tag never did — push v{current} at the commit that moved "
|
|
191
|
+
"it rather than bumping past a version nobody released"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
subjects = subjects_since(root, tag)
|
|
195
|
+
if not subjects:
|
|
196
|
+
raise ReleaseError(f"nothing has landed since {tag or 'the start of the history'}")
|
|
197
|
+
|
|
198
|
+
if requested:
|
|
199
|
+
version = requested
|
|
200
|
+
if parse_version(version) <= parse_version(current):
|
|
201
|
+
raise ReleaseError(f"{version} is not ahead of {current}")
|
|
202
|
+
else:
|
|
203
|
+
version = next_version(current, subjects)
|
|
204
|
+
|
|
205
|
+
if f"v{version}" in git(root, "tag", "--list").split():
|
|
206
|
+
raise ReleaseError(f"v{version} already exists — PyPI would refuse it as a duplicate")
|
|
207
|
+
|
|
208
|
+
return current, version, tag, subjects
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def run(root: Path, requested: str | None) -> None:
|
|
212
|
+
"""Decide, rewrite, report. Every refusal leaves this by raising `ReleaseError`."""
|
|
213
|
+
init_text = (root / INIT_PATH).read_text(encoding="utf-8")
|
|
214
|
+
changelog_text = (root / CHANGELOG_PATH).read_text(encoding="utf-8")
|
|
215
|
+
|
|
216
|
+
current, version, tag, subjects = plan(root, init_text, requested)
|
|
217
|
+
# Both rewrites are computed before either is written, so a refusal from the second
|
|
218
|
+
# one does not leave the first one on disk.
|
|
219
|
+
new_init = rewrite_version(init_text, version)
|
|
220
|
+
new_changelog = rewrite_changelog(changelog_text, version)
|
|
221
|
+
|
|
222
|
+
(root / INIT_PATH).write_text(new_init, encoding="utf-8")
|
|
223
|
+
(root / CHANGELOG_PATH).write_text(new_changelog, encoding="utf-8")
|
|
224
|
+
|
|
225
|
+
# Read back rather than trust the substitution: this string is what `release.yml` will
|
|
226
|
+
# look for as `dist/divejson-$version.tar.gz`, and the tag is pushed before anything
|
|
227
|
+
# builds. `read_version` raises here if the line it wrote is no longer one line.
|
|
228
|
+
written = read_version((root / INIT_PATH).read_text(encoding="utf-8"))
|
|
229
|
+
if written != version:
|
|
230
|
+
raise ReleaseError(f"wrote {written!r}, meant {version!r}")
|
|
231
|
+
|
|
232
|
+
print(f"released so far: {current} (tag {tag or 'none'})")
|
|
233
|
+
print(f"releasing: {version} ({bump_kind(subjects)})")
|
|
234
|
+
print(f"window: {len(subjects)} commits")
|
|
235
|
+
for subject in subjects:
|
|
236
|
+
print(f" {subject}")
|
|
237
|
+
|
|
238
|
+
# Only once everything above held: the workflow names the branch, the pull request and
|
|
239
|
+
# the tag from this, so a version here is a commitment to push it.
|
|
240
|
+
output = os.environ.get("GITHUB_OUTPUT")
|
|
241
|
+
if output:
|
|
242
|
+
with open(output, "a", encoding="utf-8") as handle:
|
|
243
|
+
handle.write(f"version={version}\n")
|
|
244
|
+
handle.write(f"previous={current}\n")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
248
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
"--version",
|
|
251
|
+
default="",
|
|
252
|
+
help="the version to release. Empty — which is what the workflow passes when its "
|
|
253
|
+
"own input was left blank — computes it from the commit subjects.",
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--root",
|
|
257
|
+
type=Path,
|
|
258
|
+
default=Path.cwd(),
|
|
259
|
+
help="the checkout to work in (default: the working directory)",
|
|
260
|
+
)
|
|
261
|
+
args = parser.parse_args(argv)
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
# Stripped once, here: whitespace around a version typed into the dispatch form is
|
|
265
|
+
# a typo rather than an intention, and every check below is on the value that is
|
|
266
|
+
# actually written.
|
|
267
|
+
run(args.root.resolve(), args.version.strip() or None)
|
|
268
|
+
except ReleaseError as error:
|
|
269
|
+
prefix = "::error::" if os.environ.get("GITHUB_ACTIONS") else "error: "
|
|
270
|
+
print(f"{prefix}{error}", file=sys.stderr)
|
|
271
|
+
return 1
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
if __name__ == "__main__":
|
|
276
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
tests:
|
|
10
|
+
name: Tests and the vendored corpus
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
# The floor, a current version, and the one the applications that install this
|
|
15
|
+
# package run. 3.10's stricter datetime.fromisoformat is exactly the kind of
|
|
16
|
+
# thing only running the floor catches, and until 3.14 was in this matrix the
|
|
17
|
+
# converter had never been run on the interpreter its first consumer uses.
|
|
18
|
+
python-version: ["3.10", "3.12", "3.14"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
# Editable, which is the install that has no `divejson/_schema/`: a `force-include`
|
|
26
|
+
# reaches a built wheel and not an editable install, so this leg is what exercises
|
|
27
|
+
# the vendored corpus as the schema's source. The `wheel` job below covers the other.
|
|
28
|
+
- name: Install the package
|
|
29
|
+
run: pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: The tests pass
|
|
32
|
+
run: pytest
|
|
33
|
+
|
|
34
|
+
# The conformance runner over this repository's own copy of the corpus, exactly as
|
|
35
|
+
# the specification repository runs it over the original against a pinned release.
|
|
36
|
+
# `--strict`, so that a format this implementation reads and the corpus has no
|
|
37
|
+
# pairs for fails the build rather than warning into a log nobody reads.
|
|
38
|
+
- name: The vendored corpus conforms
|
|
39
|
+
run: divejson conform fixtures --strict
|
|
40
|
+
|
|
41
|
+
specification:
|
|
42
|
+
name: The vendored tree is the specification at SPEC_REF
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- name: Check the specification out at SPEC_REF
|
|
48
|
+
run: |
|
|
49
|
+
ref="$(tr -d '[:space:]' < SPEC_REF)"
|
|
50
|
+
echo "SPEC_REF=$ref" >> "$GITHUB_ENV"
|
|
51
|
+
git clone --quiet https://github.com/divejson/divejson.git "$RUNNER_TEMP/spec"
|
|
52
|
+
git -C "$RUNNER_TEMP/spec" checkout --quiet "$ref"
|
|
53
|
+
|
|
54
|
+
# A subset check, not an equality one: this repository may carry pairs and mapping
|
|
55
|
+
# documents the specification has not adopted yet, and that is how an adapter lands
|
|
56
|
+
# self-contained. What it may never do is *change* a file the specification owns —
|
|
57
|
+
# that always goes to the specification first, and comes back on the next pin bump.
|
|
58
|
+
- name: Every file the specification owns is byte-identical here
|
|
59
|
+
run: |
|
|
60
|
+
differing=0
|
|
61
|
+
while IFS= read -r file; do
|
|
62
|
+
if ! cmp -s "$RUNNER_TEMP/spec/$file" "$file"; then
|
|
63
|
+
echo "::error file=$file::differs from the specification at $SPEC_REF"
|
|
64
|
+
differing=1
|
|
65
|
+
fi
|
|
66
|
+
done < <(git -C "$RUNNER_TEMP/spec" ls-files -- schema fixtures docs)
|
|
67
|
+
exit $differing
|
|
68
|
+
|
|
69
|
+
# A pin on a commit that never landed — a branch someone force-pushed away, a PR
|
|
70
|
+
# head that was rewritten — would leave this implementing a specification nobody
|
|
71
|
+
# else has, and the byte check above would pass happily.
|
|
72
|
+
- name: SPEC_REF is an ancestor of the specification's main
|
|
73
|
+
run: git -C "$RUNNER_TEMP/spec" merge-base --is-ancestor "$SPEC_REF" origin/main
|
|
74
|
+
|
|
75
|
+
wheel:
|
|
76
|
+
name: An installed wheel carries its own schema
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v4
|
|
80
|
+
- uses: actions/setup-python@v5
|
|
81
|
+
with:
|
|
82
|
+
python-version: "3.12"
|
|
83
|
+
- run: pip install build hatchling
|
|
84
|
+
|
|
85
|
+
# `python -m build` builds the sdist and then builds the wheel *from the sdist*,
|
|
86
|
+
# which is the artefact question worth asking: a wheel built from the source tree
|
|
87
|
+
# can carry files the sdist forgot. `--no-isolation`, with the backend installed
|
|
88
|
+
# above, so nothing is fetched while building — an sdist that needed the network
|
|
89
|
+
# would make that fetch part of every `pip install` from source.
|
|
90
|
+
- name: Build the sdist, and the wheel from the sdist
|
|
91
|
+
run: python -m build --no-isolation
|
|
92
|
+
|
|
93
|
+
- name: Install the wheel into a virtualenv of its own
|
|
94
|
+
run: |
|
|
95
|
+
python -m venv "$RUNNER_TEMP/venv"
|
|
96
|
+
"$RUNNER_TEMP/venv/bin/pip" install dist/*.whl
|
|
97
|
+
|
|
98
|
+
# Run from `$RUNNER_TEMP`, where there is no checkout to fall back to: this is the
|
|
99
|
+
# position every application that installs the package is in.
|
|
100
|
+
- name: The installed package resolves the schema it carries, with no checkout near
|
|
101
|
+
working-directory: ${{ runner.temp }}
|
|
102
|
+
run: |
|
|
103
|
+
cp "$GITHUB_WORKSPACE/fixtures/valid/technical-dive.divejson" .
|
|
104
|
+
"$RUNNER_TEMP/venv/bin/divejson" validate technical-dive.divejson
|
|
105
|
+
"$RUNNER_TEMP/venv/bin/divejson" conform "$GITHUB_WORKSPACE/fixtures" --strict
|
|
106
|
+
"$RUNNER_TEMP/venv/bin/python" - <<'EOF'
|
|
107
|
+
import os, pathlib
|
|
108
|
+
import divejson
|
|
109
|
+
|
|
110
|
+
installed = pathlib.Path(divejson.__file__).resolve().parent
|
|
111
|
+
assert "site-packages" in str(installed), installed
|
|
112
|
+
vendored = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) / "schema"
|
|
113
|
+
sources = sorted(vendored.glob("*/divejson.schema.json"))
|
|
114
|
+
assert sources, "the vendored tree carries no schema at all"
|
|
115
|
+
for source in sources:
|
|
116
|
+
packaged = installed / "_schema" / source.parent.name / "divejson.schema.json"
|
|
117
|
+
assert packaged.read_bytes() == source.read_bytes(), packaged
|
|
118
|
+
print(f"schemas checked against the vendored tree: {len(sources)}")
|
|
119
|
+
EOF
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: PR Title
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
# `edited` is the one that matters: it re-runs the check when a title is
|
|
6
|
+
# corrected, so a failing PR goes green without an empty commit to push.
|
|
7
|
+
#
|
|
8
|
+
# `synchronize` is not about the title at all - it is what would let this be
|
|
9
|
+
# a required check. A required check has to have passed on the PR's *latest*
|
|
10
|
+
# head SHA, so without a run per push, a PR that gains a second commit would
|
|
11
|
+
# sit at "Expected - waiting for status" for good. The title is read from the
|
|
12
|
+
# event payload rather than from the tree, so the extra runs reach the same
|
|
13
|
+
# verdict. Nothing is required on `main` today; this keeps the option open at
|
|
14
|
+
# no cost.
|
|
15
|
+
types: [opened, edited, reopened, synchronize]
|
|
16
|
+
|
|
17
|
+
# The one list of types. CONTRIBUTING.md spells the same list out in prose -
|
|
18
|
+
# keep the two in step.
|
|
19
|
+
env:
|
|
20
|
+
TYPES: feat|fix|refactor|docs|test|chore|perf|ci|build|revert
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
semantic-title:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
|
|
26
|
+
# Nothing is checked out and no API call is made - the title arrives in the
|
|
27
|
+
# event payload - so this job needs no token at all.
|
|
28
|
+
permissions: {}
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
# `main`'s ruleset allows only squash merges, and the repository's squash
|
|
32
|
+
# subject source is PR_TITLE rather than COMMIT_OR_PR_TITLE, so this title
|
|
33
|
+
# *is* the commit subject that lands. It is the only part of a branch that
|
|
34
|
+
# outlives the branch, which is why it is checked and the individual
|
|
35
|
+
# commits are not. That setting was corrected by hand after a non-semantic
|
|
36
|
+
# subject reached a default branch in this org; this check is the half that
|
|
37
|
+
# stops the bad title from being written in the first place.
|
|
38
|
+
#
|
|
39
|
+
# The title is passed through `env` rather than interpolated into the
|
|
40
|
+
# script. `${{ github.event.pull_request.title }}` written inside `run:`
|
|
41
|
+
# is substituted before bash parses the line, so a title containing
|
|
42
|
+
# backticks or `$(...)` would execute as shell.
|
|
43
|
+
- name: Check title is a conventional commit subject
|
|
44
|
+
env:
|
|
45
|
+
TITLE: ${{ github.event.pull_request.title }}
|
|
46
|
+
run: |
|
|
47
|
+
PATTERN="^($TYPES)(\([a-z0-9._-]+\))?!?: .+"
|
|
48
|
+
|
|
49
|
+
if printf '%s' "$TITLE" | grep -Eq "$PATTERN"; then
|
|
50
|
+
echo "OK: $TITLE"
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
echo "PR title is not a semantic commit subject:"
|
|
55
|
+
echo " $TITLE"
|
|
56
|
+
echo
|
|
57
|
+
echo "Expected: <type>[(scope)][!]: <description>"
|
|
58
|
+
echo "Types: ${TYPES//|/, }"
|
|
59
|
+
echo
|
|
60
|
+
echo "Examples: feat(schema): a cylinder may record a mix without a vessel"
|
|
61
|
+
echo " fix(spec): profile.duration spans the samples, not the events"
|
|
62
|
+
echo " docs(spec)!: the draft becomes 1.0"
|
|
63
|
+
echo
|
|
64
|
+
echo "Edit the title - this check re-runs on edit, no push needed."
|
|
65
|
+
exit 1
|