nilan-connect 0.2.0rc1__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.
Files changed (66) hide show
  1. nilan_connect-0.2.0rc1/.gitattributes +2 -0
  2. nilan_connect-0.2.0rc1/.github/dependabot.yml +17 -0
  3. nilan_connect-0.2.0rc1/.github/release-drafter.yml +63 -0
  4. nilan_connect-0.2.0rc1/.github/release.yml +27 -0
  5. nilan_connect-0.2.0rc1/.github/scripts/pyrightconfig.json +5 -0
  6. nilan_connect-0.2.0rc1/.github/scripts/release_version.py +164 -0
  7. nilan_connect-0.2.0rc1/.github/scripts/test_release_version.py +125 -0
  8. nilan_connect-0.2.0rc1/.github/scripts/test_workflows.py +25 -0
  9. nilan_connect-0.2.0rc1/.github/workflows/close-inactive-issues.yml +30 -0
  10. nilan_connect-0.2.0rc1/.github/workflows/release-drafter.yml +44 -0
  11. nilan_connect-0.2.0rc1/.github/workflows/release.yml +262 -0
  12. nilan_connect-0.2.0rc1/.github/workflows/tests.yml +79 -0
  13. nilan_connect-0.2.0rc1/.gitignore +168 -0
  14. nilan_connect-0.2.0rc1/.vscode/settings.json +28 -0
  15. nilan_connect-0.2.0rc1/CLAUDE.md +98 -0
  16. nilan_connect-0.2.0rc1/LICENSE +21 -0
  17. nilan_connect-0.2.0rc1/MANIFEST.in +2 -0
  18. nilan_connect-0.2.0rc1/PKG-INFO +193 -0
  19. nilan_connect-0.2.0rc1/README.md +164 -0
  20. nilan_connect-0.2.0rc1/docs/models/README.md +81 -0
  21. nilan_connect-0.2.0rc1/docs/models/extract.py +213 -0
  22. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-250-user-manual.pdf +0 -0
  23. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-250-user-manual.txt +1023 -0
  24. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-251-user-manual.pdf +0 -0
  25. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-251-user-manual.txt +1224 -0
  26. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-260-betjeningsvejledning-da.pdf +0 -0
  27. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-260-betjeningsvejledning-da.txt +1006 -0
  28. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-270-user-manual.pdf +0 -0
  29. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-270-user-manual.txt +1316 -0
  30. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-312-user-manual.pdf +0 -0
  31. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-312-user-manual.txt +1244 -0
  32. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-314-user-manual.pdf +0 -0
  33. nilan_connect-0.2.0rc1/docs/models/genvex-optima/optima-314-user-manual.txt +1511 -0
  34. nilan_connect-0.2.0rc1/docs/models/nilan-cts400/alarms.csv +22 -0
  35. nilan_connect-0.2.0rc1/docs/models/nilan-cts400/cts400-modbus.pdf +0 -0
  36. nilan_connect-0.2.0rc1/docs/models/nilan-cts400/cts400-software-instructions.pdf +0 -0
  37. nilan_connect-0.2.0rc1/docs/models/nilan-cts400/cts400-software-instructions.txt +1050 -0
  38. nilan_connect-0.2.0rc1/docs/models/nilan-cts400/registers.csv +89 -0
  39. nilan_connect-0.2.0rc1/docs/models/nilan-cts602/alarms.csv +68 -0
  40. nilan_connect-0.2.0rc1/docs/models/nilan-cts602/cts602-modbus.pdf +0 -0
  41. nilan_connect-0.2.0rc1/docs/models/nilan-cts602/registers.csv +166 -0
  42. nilan_connect-0.2.0rc1/docs/models/nilan-cts602-hmi350t/cts602-hmi350t-modbus.pdf +0 -0
  43. nilan_connect-0.2.0rc1/docs/models/nilan-cts602-hmi350t/registers.csv +315 -0
  44. nilan_connect-0.2.0rc1/docs/models/nilan-cts602-light/cts602-light-modbus.pdf +0 -0
  45. nilan_connect-0.2.0rc1/docs/models/nilan-cts602-light/registers.csv +183 -0
  46. nilan_connect-0.2.0rc1/pyproject.toml +79 -0
  47. nilan_connect-0.2.0rc1/requirements-test.txt +5 -0
  48. nilan_connect-0.2.0rc1/setup.cfg +4 -0
  49. nilan_connect-0.2.0rc1/src/nilan_connect/__init__.py +71 -0
  50. nilan_connect-0.2.0rc1/src/nilan_connect/_model.py +246 -0
  51. nilan_connect-0.2.0rc1/src/nilan_connect/_nilan.py +25 -0
  52. nilan_connect-0.2.0rc1/src/nilan_connect/py.typed +0 -0
  53. nilan_connect-0.2.0rc1/src/nilan_connect/testing.py +8 -0
  54. nilan_connect-0.2.0rc1/src/nilan_connect.egg-info/PKG-INFO +193 -0
  55. nilan_connect-0.2.0rc1/src/nilan_connect.egg-info/SOURCES.txt +64 -0
  56. nilan_connect-0.2.0rc1/src/nilan_connect.egg-info/dependency_links.txt +1 -0
  57. nilan_connect-0.2.0rc1/src/nilan_connect.egg-info/requires.txt +1 -0
  58. nilan_connect-0.2.0rc1/src/nilan_connect.egg-info/scm_file_list.json +62 -0
  59. nilan_connect-0.2.0rc1/src/nilan_connect.egg-info/scm_version.json +8 -0
  60. nilan_connect-0.2.0rc1/src/nilan_connect.egg-info/top_level.txt +1 -0
  61. nilan_connect-0.2.0rc1/tests/conftest.py +31 -0
  62. nilan_connect-0.2.0rc1/tests/cts400_points.json +636 -0
  63. nilan_connect-0.2.0rc1/tests/cts400_read_2026_09_27.json +98 -0
  64. nilan_connect-0.2.0rc1/tests/test_cts400.py +205 -0
  65. nilan_connect-0.2.0rc1/tests/test_live_nilan.py +68 -0
  66. nilan_connect-0.2.0rc1/tests/test_package.py +43 -0
@@ -0,0 +1,2 @@
1
+ # GitHub's source downloads carry the version setuptools-scm needs, from this file.
2
+ .git_archival.txt export-subst
@@ -0,0 +1,17 @@
1
+ # Proposes updates to the pinned actions and test tools as pull requests, which the tests then
2
+ # check before anything is merged.
3
+ version: 2
4
+ updates:
5
+ - package-ecosystem: github-actions
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ labels:
10
+ - dependencies
11
+
12
+ - package-ecosystem: pip
13
+ directory: /
14
+ schedule:
15
+ interval: weekly
16
+ labels:
17
+ - dependencies
@@ -0,0 +1,63 @@
1
+ name-template: 'v$RESOLVED_VERSION'
2
+ tag-template: 'v$RESOLVED_VERSION'
3
+ change-template: '- #$NUMBER $TITLE @$AUTHOR'
4
+ sort-direction: ascending
5
+ categories:
6
+ - type: 'pre-exclude'
7
+ when:
8
+ label: 'skip-changelog'
9
+
10
+ # Each pull request is listed once, under the first of these its labels match.
11
+ - title: '🛠 Breaking Changes'
12
+ exclusive: true
13
+ when:
14
+ label: 'breaking-change'
15
+
16
+ - title: '🚀 Features'
17
+ exclusive: true
18
+ when:
19
+ labels:
20
+ - 'feature request'
21
+ - 'enhancement'
22
+
23
+ - title: '🐛 Bug Fixes'
24
+ exclusive: true
25
+ when:
26
+ labels:
27
+ - 'fix'
28
+ - 'bugfix'
29
+ - 'bug'
30
+
31
+ - title: '🧰 Maintenance'
32
+ exclusive: true
33
+ when:
34
+ label: 'chore'
35
+
36
+ - title: '📦 Dependencies'
37
+ exclusive: true
38
+ when:
39
+ label: 'dependencies'
40
+
41
+ # The next version: a minor bump for a breaking change or by label, a patch otherwise. A major
42
+ # is never taken from a label, as one placed wrongly must not release a new major version.
43
+ - type: 'version-resolver'
44
+ semver-increment: 'minor'
45
+ when:
46
+ labels:
47
+ - 'breaking-change'
48
+ - 'minor'
49
+ - type: 'version-resolver'
50
+ semver-increment: 'patch'
51
+ template: |
52
+ ## Changes
53
+ $CHANGES
54
+ autolabeler:
55
+ - label: 'bug'
56
+ branch:
57
+ - '/fix\/.+/'
58
+ - label: 'feature request'
59
+ branch:
60
+ - '/feature\/.+/'
61
+ - label: 'chore'
62
+ branch:
63
+ - '/chore\/.+/'
@@ -0,0 +1,27 @@
1
+ # How GitHub groups a release's notes: each pull request by its labels.
2
+ changelog:
3
+ exclude:
4
+ labels:
5
+ - skip-changelog
6
+ categories:
7
+ - title: 🛠 Breaking Changes
8
+ labels:
9
+ - breaking-change
10
+ - title: 🚀 Features
11
+ labels:
12
+ - feature request
13
+ - enhancement
14
+ - title: 🐛 Bug Fixes
15
+ labels:
16
+ - bug
17
+ - fix
18
+ - bugfix
19
+ - title: 🧰 Maintenance
20
+ labels:
21
+ - chore
22
+ - title: 📦 Dependencies
23
+ labels:
24
+ - dependencies
25
+ - title: Other Changes
26
+ labels:
27
+ - "*"
@@ -0,0 +1,5 @@
1
+ {
2
+ // A project of its own, as pyright skips directories whose names begin with a dot.
3
+ "typeCheckingMode": "strict",
4
+ "reportMissingTypeStubs": "none"
5
+ }
@@ -0,0 +1,164 @@
1
+ """The release version: work it out, and write it into the package.
2
+
3
+ release_version.py next KIND DRAFT OVERRIDE TAGS [PUBLISHED]
4
+ prints version=, tag=, prerelease= and previous_tag=
5
+ lines for the next KIND ("release candidate" or
6
+ "final release"); DRAFT is the draft release's
7
+ version, OVERRIDE a version to take instead, either
8
+ may be empty; TAGS is a file of every tag, PUBLISHED
9
+ one of every version published, one per line
10
+ release_version.py changes SINCE MERGED refuses a release when MERGED, a file of the pull
11
+ requests merged since the release SINCE, is empty
12
+ release_version.py write VERSION FILE sets the one `__version__ = "..."` line in FILE
13
+ """
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from packaging.version import InvalidVersion, Version
19
+
20
+ _LINE = re.compile(r'^__version__ = "[^"\n]*"$', re.MULTILINE)
21
+
22
+ RELEASE_CANDIDATE = "release candidate"
23
+ FINAL_RELEASE = "final release"
24
+
25
+
26
+ def _canonical(text: str) -> Version:
27
+ """The version `text` names, refused unless `text` is already its canonical form."""
28
+ try:
29
+ version = Version(text)
30
+ except InvalidVersion:
31
+ sys.exit(f"::error::{text!r} is not a PEP 440 version, such as 1.2.3 or 1.2.3rc1")
32
+ if str(version) != text:
33
+ # A build writes the canonical form into the file names, so any other spelling would
34
+ # tag one version and publish another.
35
+ sys.exit(f"::error::write {text!r} as {str(version)!r}, its canonical PEP 440 form")
36
+ if len(version.release) != 3 or version.epoch or version.local:
37
+ # 1.2 and 1.2.0 are one version to PEP 440 but two tags; PyPI refuses a local version.
38
+ sys.exit(f"::error::{text!r} must be MAJOR.MINOR.PATCH, optionally with a pre-release, "
39
+ f"such as 1.2.3 or 1.2.3rc1")
40
+ return version
41
+
42
+
43
+ def known_versions(lines: list[str]) -> list[Version]:
44
+ """The versions `lines` name, skipping tags that name none."""
45
+ versions: list[Version] = []
46
+ for line in lines:
47
+ try:
48
+ versions.append(Version(line.strip().removeprefix("v")))
49
+ except InvalidVersion:
50
+ continue
51
+ return versions
52
+
53
+
54
+ def next_version(kind: str, draft: str, override: str, known: list[Version]) -> tuple[Version, str]:
55
+ """The version to release as `kind`, and how it was found.
56
+
57
+ Without an override, an open release-candidate series - the highest known version being a
58
+ pre-release - is continued, or ended by a final release, unless the draft names a higher
59
+ version; that starts a new series. A new major version is taken only as an override, so that
60
+ a label placed wrongly cannot release one. The result must be higher than every known version,
61
+ as pip installs the highest and PyPI never takes a version twice.
62
+ """
63
+ if kind not in (RELEASE_CANDIDATE, FINAL_RELEASE):
64
+ sys.exit(f"::error::{kind!r} is neither {RELEASE_CANDIDATE!r} nor {FINAL_RELEASE!r}")
65
+ top = max(known, default=None)
66
+ if override:
67
+ version = _canonical(override)
68
+ if version.is_prerelease != (kind == RELEASE_CANDIDATE):
69
+ sys.exit(f"::error::{version} is {'a pre-release' if version.is_prerelease else 'a final release'}, "
70
+ f"not a {kind}")
71
+ how = "given as the override"
72
+ else:
73
+ series = Version(top.base_version) if top is not None and top.is_prerelease else None
74
+ base = series
75
+ if draft:
76
+ proposed = _canonical(draft.removeprefix("v"))
77
+ if proposed.is_prerelease:
78
+ sys.exit(f"::error::the draft release names {proposed}, a pre-release, not the next version")
79
+ if base is None or proposed > base:
80
+ base = proposed
81
+ if base is None:
82
+ sys.exit("::error::no draft release names the next version, and no release-candidate "
83
+ "series is open; give the version as the override")
84
+ if kind == FINAL_RELEASE:
85
+ version = base
86
+ how = "ends the release-candidate series" if base == series else "the draft release's version"
87
+ elif base == series and top is not None and top.pre is not None and top.pre[0] == "rc":
88
+ version = Version(f"{base}rc{top.pre[1] + 1}")
89
+ how = f"the release candidate after {top}"
90
+ else:
91
+ version = Version(f"{base}rc1")
92
+ how = f"the first release candidate of {base}"
93
+ if top is not None and version.major > top.major:
94
+ sys.exit(f"::error::{version} is a new major version; type it as the override")
95
+ if top is not None and version <= top:
96
+ sys.exit(f"::error::{version} is not higher than {top}, the highest version already tagged "
97
+ f"or published")
98
+ return version, how
99
+
100
+
101
+ def previous_tag(version: Version, tags: list[str]) -> str | None:
102
+ """The tag the notes of `version` start from, None to start from the beginning.
103
+
104
+ That is the release candidate or final release before it; before a final release, the final
105
+ release before it, so that its notes hold every change its release candidates had. Other
106
+ pre-releases, such as test builds, are skipped.
107
+ """
108
+ found: list[tuple[Version, str]] = []
109
+ for tag in tags:
110
+ try:
111
+ earlier = Version(tag.strip().removeprefix("v"))
112
+ except InvalidVersion:
113
+ continue
114
+ if earlier >= version or earlier.dev is not None:
115
+ continue
116
+ if earlier.pre is not None and (earlier.pre[0] != "rc" or not version.is_prerelease):
117
+ continue
118
+ found.append((earlier, tag.strip()))
119
+ return max(found)[1] if found else None
120
+
121
+
122
+ def require_changes(since: str, pull_requests: list[str]) -> None:
123
+ """Refuse a release when no pull request has been merged since the release `since`.
124
+
125
+ What users get comes through pull requests; a change pushed straight to main - CI, tests,
126
+ the release itself - gives them nothing to release.
127
+ """
128
+ if not any(line.strip() for line in pull_requests):
129
+ sys.exit(f"::error::no pull request has been merged since {since}; there is nothing to release")
130
+
131
+
132
+ def _lines(path: str) -> list[str]:
133
+ return Path(path).read_text(encoding="utf-8").splitlines()
134
+
135
+
136
+ def _print(version: Version) -> None:
137
+ print(f"version={version}")
138
+ print(f"tag=v{version}")
139
+ print(f"prerelease={'true' if version.is_prerelease else 'false'}")
140
+
141
+
142
+ def write(text: str, path: Path) -> None:
143
+ version = _canonical(text)
144
+ source = path.read_text(encoding="utf-8")
145
+ found = _LINE.findall(source)
146
+ if len(found) != 1:
147
+ sys.exit(f"::error::{path} must hold exactly one `__version__ = \"...\"` line, found {len(found)}")
148
+ path.write_text(_LINE.sub(f'__version__ = "{version}"', source), encoding="utf-8")
149
+
150
+
151
+ if __name__ == "__main__":
152
+ match sys.argv[1:]:
153
+ case ["next", kind, draft, override, tags, *published] if len(published) <= 1:
154
+ found, how = next_version(kind, draft, override,
155
+ known_versions(_lines(tags) + [v for f in published for v in _lines(f)]))
156
+ print(f"{found}: {how}", file=sys.stderr)
157
+ _print(found)
158
+ print(f"previous_tag={previous_tag(found, _lines(tags)) or ''}")
159
+ case ["changes", since, merged]:
160
+ require_changes(since, _lines(merged))
161
+ case ["write", text, path]:
162
+ write(text, Path(path))
163
+ case _:
164
+ sys.exit(__doc__)
@@ -0,0 +1,125 @@
1
+ """Tests of how the release works out its version."""
2
+ import pytest
3
+ from packaging.version import Version
4
+
5
+ from release_version import FINAL_RELEASE, RELEASE_CANDIDATE, known_versions, next_version, previous_tag, require_changes
6
+
7
+
8
+ def _next(kind: str, draft: str, known: list[str], override: str = "") -> str:
9
+ return str(next_version(kind, draft, override, known_versions(known))[0])
10
+
11
+
12
+ def test_a_release_candidate_continues_the_open_series_below_the_draft() -> None:
13
+ assert _next(RELEASE_CANDIDATE, "v0.1.10", ["v0.1.8", "0.1.9", "v0.2.0rc1", "0.2.0rc1"]) == "0.2.0rc2"
14
+
15
+
16
+ def test_a_final_release_ends_the_open_series() -> None:
17
+ assert _next(FINAL_RELEASE, "v0.1.10", ["v0.1.8", "v0.2.0rc1", "v0.2.0rc2"]) == "0.2.0"
18
+
19
+
20
+ def test_a_draft_above_the_open_series_starts_a_new_one() -> None:
21
+ assert _next(RELEASE_CANDIDATE, "v0.3.0", ["v0.2.0rc2"]) == "0.3.0rc1"
22
+ assert _next(FINAL_RELEASE, "v0.3.0", ["v0.2.0rc2"]) == "0.3.0"
23
+
24
+
25
+ @pytest.mark.parametrize("kind", [RELEASE_CANDIDATE, FINAL_RELEASE])
26
+ def test_a_new_major_version_is_never_worked_out(kind: str) -> None:
27
+ with pytest.raises(SystemExit, match="1.0.0(rc1)? is a new major version"):
28
+ _next(kind, "v1.0.0", ["v0.2.0rc2"])
29
+
30
+
31
+ def test_a_new_major_version_is_taken_as_an_override() -> None:
32
+ assert _next(RELEASE_CANDIDATE, "v0.2.1", ["v0.2.0"], override="1.0.0rc1") == "1.0.0rc1"
33
+
34
+
35
+ def test_without_an_open_series_the_draft_names_the_version() -> None:
36
+ assert _next(RELEASE_CANDIDATE, "v0.3.0", ["v0.2.0rc1", "v0.2.0"]) == "0.3.0rc1"
37
+ assert _next(FINAL_RELEASE, "v0.3.0", ["v0.2.0"]) == "0.3.0"
38
+
39
+
40
+ def test_the_first_release_needs_nothing_known() -> None:
41
+ assert _next(FINAL_RELEASE, "v0.1.0", []) == "0.1.0"
42
+
43
+
44
+ def test_a_series_of_betas_continues_with_its_first_release_candidate() -> None:
45
+ assert _next(RELEASE_CANDIDATE, "v0.2.0", ["v0.1.0", "v0.2.0b3"]) == "0.2.0rc1"
46
+
47
+
48
+ def test_an_open_series_needs_no_draft() -> None:
49
+ assert _next(RELEASE_CANDIDATE, "", ["v0.2.0rc1"]) == "0.2.0rc2"
50
+ assert _next(FINAL_RELEASE, "", ["v0.2.0rc1"]) == "0.2.0"
51
+
52
+
53
+ def test_without_a_draft_or_an_open_series_the_version_must_be_given() -> None:
54
+ with pytest.raises(SystemExit, match="give the version as the override"):
55
+ _next(FINAL_RELEASE, "", ["v0.2.0"])
56
+
57
+
58
+ def test_a_draft_below_what_is_published_is_refused() -> None:
59
+ with pytest.raises(SystemExit, match="0.1.10 is not higher than 0.2.0"):
60
+ _next(FINAL_RELEASE, "v0.1.10", ["v0.2.0"])
61
+
62
+
63
+ def test_a_version_withdrawn_from_the_tags_still_counts_from_pypi() -> None:
64
+ with pytest.raises(SystemExit, match="0.1.9 is not higher than 0.1.9"):
65
+ _next(FINAL_RELEASE, "v0.1.9", ["v0.1.8", "0.1.9"])
66
+
67
+
68
+ def test_the_override_replaces_the_version_worked_out() -> None:
69
+ assert _next(RELEASE_CANDIDATE, "v0.1.10", ["v0.2.0rc1"], override="0.3.0rc1") == "0.3.0rc1"
70
+
71
+
72
+ def test_the_override_must_still_be_higher_than_every_known_version() -> None:
73
+ with pytest.raises(SystemExit, match="0.2.0rc1 is not higher than 0.2.0rc1"):
74
+ _next(RELEASE_CANDIDATE, "", ["v0.2.0rc1"], override="0.2.0rc1")
75
+
76
+
77
+ @pytest.mark.parametrize(("kind", "override"), [(FINAL_RELEASE, "0.2.0rc2"), (RELEASE_CANDIDATE, "0.2.0")])
78
+ def test_the_override_must_be_the_kind_chosen(kind: str, override: str) -> None:
79
+ with pytest.raises(SystemExit, match=f"not a {kind}"):
80
+ _next(kind, "", ["v0.2.0rc1"], override=override)
81
+
82
+
83
+ @pytest.mark.parametrize("override", ["v0.3.0", "0.3", "0.3.0-rc1", "0.3.0+local"])
84
+ def test_the_override_must_be_canonical_major_minor_patch(override: str) -> None:
85
+ with pytest.raises(SystemExit, match="::error::"):
86
+ _next(FINAL_RELEASE, "", [], override=override)
87
+
88
+
89
+ def test_an_unknown_kind_is_refused() -> None:
90
+ with pytest.raises(SystemExit, match="neither"):
91
+ _next("final", "v0.1.0", [])
92
+
93
+
94
+ def test_tags_that_name_no_version_are_skipped() -> None:
95
+ assert known_versions(["v0.1.0", "latest", "", "0.2.0rc1"]) == [Version("0.1.0"), Version("0.2.0rc1")]
96
+
97
+
98
+ def test_the_notes_of_a_release_candidate_start_at_the_one_before() -> None:
99
+ assert previous_tag(Version("0.2.0rc2"), ["v0.1.9", "v0.2.0rc1", "v0.2.0rc2"]) == "v0.2.0rc1"
100
+
101
+
102
+ def test_the_notes_of_a_final_release_start_at_the_final_release_before() -> None:
103
+ assert previous_tag(Version("0.2.0"), ["v0.1.9", "v0.2.0rc1", "v0.2.0rc2"]) == "v0.1.9"
104
+
105
+
106
+ def test_the_notes_of_a_new_series_start_at_the_final_release_before() -> None:
107
+ assert previous_tag(Version("0.3.0rc1"), ["v0.2.0rc2", "v0.2.0", "latest"]) == "v0.2.0"
108
+
109
+
110
+ def test_the_notes_skip_test_builds() -> None:
111
+ assert previous_tag(Version("0.2.0rc1"), ["v0.1.0", "v0.2.0b3"]) == "v0.1.0"
112
+
113
+
114
+ def test_the_first_release_notes_start_from_the_beginning() -> None:
115
+ assert previous_tag(Version("0.1.0"), []) is None
116
+ assert previous_tag(Version("0.2.0"), ["v0.2.0rc1", "v0.2.0rc2"]) is None
117
+
118
+
119
+ def test_a_release_without_a_merged_pull_request_is_refused() -> None:
120
+ with pytest.raises(SystemExit, match="no pull request has been merged since v0.3.0"):
121
+ require_changes("v0.3.0", ["", " "])
122
+
123
+
124
+ def test_a_release_with_a_merged_pull_request_goes_ahead() -> None:
125
+ require_changes("v0.3.0", ["10"])
@@ -0,0 +1,25 @@
1
+ """The repository's workflows."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ WORKFLOWS = sorted((Path(__file__).parents[1] / "workflows").glob("*.yml"))
8
+
9
+ BASH = "defaults:\n run:\n shell: bash\n"
10
+
11
+
12
+ def test_the_workflows_are_found() -> None:
13
+ assert WORKFLOWS
14
+
15
+
16
+ @pytest.mark.parametrize("workflow", WORKFLOWS, ids=[path.name for path in WORKFLOWS])
17
+ def test_a_step_fails_when_a_command_piped_into_another_fails(workflow: Path) -> None:
18
+ """GitHub runs a step as `bash -eo pipefail` only when the shell is given; without one it
19
+ runs `bash -e`, where a failing command piped into another fails nothing."""
20
+ assert BASH in workflow.read_text(encoding="utf-8")
21
+
22
+ @pytest.mark.parametrize("workflow", WORKFLOWS, ids=[path.name for path in WORKFLOWS])
23
+ def test_no_step_pipes_into_a_command_that_stops_reading(workflow: Path) -> None:
24
+ """With pipefail, the command before `head` fails when head stops reading its output."""
25
+ assert "| head" not in workflow.read_text(encoding="utf-8")
@@ -0,0 +1,30 @@
1
+ name: Close inactive issues
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ schedule:
6
+ - cron: "30 1 * * *"
7
+
8
+ permissions: {}
9
+
10
+ # The shell given explicitly, so GitHub runs every step with pipefail.
11
+ defaults:
12
+ run:
13
+ shell: bash
14
+
15
+ jobs:
16
+ close-issues:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ issues: write
20
+ pull-requests: write
21
+ steps:
22
+ - uses: actions/stale@v11
23
+ with:
24
+ days-before-issue-stale: 30
25
+ days-before-issue-close: 14
26
+ stale-issue-label: "stale"
27
+ stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
28
+ close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
29
+ days-before-pr-stale: -1
30
+ days-before-pr-close: -1
@@ -0,0 +1,44 @@
1
+ name: Release Drafter
2
+
3
+ # Keeps a draft release proposing the next version, from the labels of the pull requests merged
4
+ # since the last final release. The draft is never published by hand.
5
+ on:
6
+ push:
7
+ branches:
8
+ - main
9
+ pull_request:
10
+ types: [opened, reopened, synchronize]
11
+
12
+ permissions: {}
13
+
14
+ # The shell given explicitly, so GitHub runs every step with pipefail.
15
+ defaults:
16
+ run:
17
+ shell: bash
18
+
19
+ jobs:
20
+ update_release_draft:
21
+ name: Update the release draft
22
+ if: github.event_name == 'push'
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: write
26
+ pull-requests: read
27
+ steps:
28
+ - name: Draft the release
29
+ uses: release-drafter/release-drafter@v7
30
+ env:
31
+ GITHUB_TOKEN: ${{ github.token }}
32
+
33
+ autolabel_pr:
34
+ name: Label the pull request
35
+ if: github.event_name == 'pull_request'
36
+ runs-on: ubuntu-latest
37
+ permissions:
38
+ contents: read
39
+ pull-requests: write
40
+ steps:
41
+ - name: Apply labels
42
+ uses: release-drafter/release-drafter/autolabeler@v7
43
+ env:
44
+ GITHUB_TOKEN: ${{ github.token }}