Skip to content

Commit a836805

Browse files
committed
Prepare Python 0.2.0 release
1 parent 95a6865 commit a836805

7 files changed

Lines changed: 215 additions & 14 deletions

File tree

‎.github/verify_release_ref.py‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Fail closed unless the requested release tag names HEAD and the package version."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from pathlib import Path
7+
import subprocess
8+
import sys
9+
import tomllib
10+
11+
12+
def _git(*arguments: str) -> str:
13+
completed = subprocess.run(
14+
["git", *arguments],
15+
check=False,
16+
stdout=subprocess.PIPE,
17+
stderr=subprocess.PIPE,
18+
text=True,
19+
)
20+
if completed.returncode:
21+
detail = completed.stderr.strip() or completed.stdout.strip()
22+
raise RuntimeError(detail or f"git {' '.join(arguments)} failed")
23+
return completed.stdout.strip()
24+
25+
26+
def verify_release_ref() -> None:
27+
release_tag = os.environ.get("RELEASE_TAG", "")
28+
version = str(
29+
tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
30+
)
31+
expected_tag = f"v{version}"
32+
if release_tag != expected_tag:
33+
raise RuntimeError(
34+
f"release tag {release_tag!r} does not match package version {version!r}"
35+
)
36+
37+
tag_ref = f"refs/tags/{release_tag}"
38+
exists = subprocess.run(
39+
["git", "show-ref", "--verify", "--quiet", tag_ref],
40+
check=False,
41+
)
42+
if exists.returncode:
43+
raise RuntimeError(f"release tag ref {tag_ref!r} does not exist")
44+
45+
tag_commit = _git("rev-parse", "--verify", f"{tag_ref}^{{commit}}")
46+
head_commit = _git("rev-parse", "--verify", "HEAD^{commit}")
47+
if tag_commit != head_commit:
48+
raise RuntimeError(
49+
f"release tag {release_tag!r} resolves to {tag_commit}, not HEAD {head_commit}"
50+
)
51+
52+
53+
def main() -> int:
54+
try:
55+
verify_release_ref()
56+
except (KeyError, OSError, RuntimeError, tomllib.TOMLDecodeError) as error:
57+
print(f"release ref verification failed: {error}", file=sys.stderr)
58+
return 1
59+
return 0
60+
61+
62+
if __name__ == "__main__":
63+
raise SystemExit(main())

‎.github/workflows/release.yml‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,18 @@ jobs:
2323
steps:
2424
- uses: actions/checkout@v7
2525
with:
26-
ref: ${{ inputs.release_ref || github.ref }}
26+
ref: refs/tags/${{ inputs.release_ref || github.ref_name }}
27+
fetch-depth: 0
2728
- uses: actions/setup-python@v7
2829
with:
2930
python-version: "3.13"
31+
- name: Verify release tag exists and targets this package version
32+
env:
33+
RELEASE_TAG: ${{ inputs.release_ref || github.ref_name }}
34+
run: |
35+
set -eu
36+
git fetch --force --no-tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
37+
python .github/verify_release_ref.py
3038
- run: python -m pip install --upgrade pip build
3139
- name: Verify required SPSS engine is publicly installable
3240
run: >-

‎CHANGELOG.md‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,37 @@
22

33
All notable changes to this reference implementation are documented here.
44

5-
## 0.1.0 — unreleased
5+
## 0.2.0 — 2026-07-30
6+
7+
This release extends the reference adapter while keeping imported source
8+
datasets immutable and all unsupported paths fail-closed.
9+
10+
### Added
11+
12+
- An optional SQLite-only Transformation Workflow for immutable, versioned,
13+
parameterized SELECT definitions; materialized derived datasets; lineage and
14+
weights; public catalog APIs; and CLI operations.
15+
- An independent Dolt core SQL profile for exact product version 2.2.2, with
16+
service-backed coverage, conservative adapter limits, complete compensating
17+
cleanup, and pre-DDL rejection of unsupported values. Transformation Workflow
18+
support on Dolt is not claimed.
19+
20+
### Changed
21+
22+
- SAV and ZSAV validation now rewrites legacy compatible names consistently
23+
across type-2 records and subtype-13/subtype-14 long-name metadata.
24+
- Transformation publication, interrupted-run reconciliation, retirement, and
25+
physical-removal recovery now preserve auditable state and fail closed on
26+
catalog or relation drift.
27+
- Release automation builds distributions before test-only source checkouts and
28+
supports idempotent rebuilds of an existing release tag.
29+
30+
### Specification basis
31+
32+
- CI and release validation are pinned to OpenStatSpec specification commit
33+
`34141dda023d9e0217c37c232e39f436edfb0746`; no specification tag is claimed.
34+
35+
## 0.1.0 — 2026-07-29
636

737
First public reference implementation of the OpenStatSpec strict wide-table
838
SPSS profile.
@@ -12,11 +42,8 @@ SPSS profile.
1242
- Import of unencrypted SAV and ZSAV sources into one dedicated wide SQL table
1343
and its metadata catalog.
1444
- Export of supported dataset semantics to SAV and ZSAV.
15-
- Consistent legacy compatible-name rewriting across type-2, subtype-13, and
16-
VLS subtype-14 records, with fail-closed SAV/ZSAV validation.
17-
- SQLite, PostgreSQL, MySQL, MariaDB, and independent Dolt profiles, including
18-
service-backed PostgreSQL 17/18, MySQL 8.4/9.7, MariaDB 11.4/11.8/12.3, and
19-
pinned Dolt 2.2.2 CI coverage. Dolt Transformation Workflow support is not claimed.
45+
- SQLite, PostgreSQL, MySQL, and MariaDB profiles, including service-backed
46+
PostgreSQL 17/18, MySQL 8.4/9.7, and MariaDB 11.4/11.8/12.3 CI coverage.
2047
- Preflight checks for target profile limits, atomic imports, validation, a
2148
command-line interface, and machine-readable capability and loss reports.
2249

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ See [the SQL transformation workflow](docs/sql-transformation-workflow.md) for
5555
Python and CLI examples, migration behavior, hashing, atomicity, and the exact
5656
implemented capability boundary.
5757

58-
## 0.1.0 support status
58+
## 0.2.0 support status
5959

6060
The adapter requires `openstatspec-pyspssio==0.5.1.post2` as its sole SPSS
6161
engine. Its import module remains `pyspssio`; the exact source commit is recorded

‎docs/release-readiness.md‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 0.1.0 release readiness
1+
# 0.2.0 release readiness
22

33
This page records the expected release contract, not a publication event.
44
Creating a version tag remains a separate maintainer action.
@@ -20,9 +20,12 @@ database contract.
2020
| PostgreSQL | `postgresql+psycopg://…` | PostgreSQL 17 and 18 service jobs |
2121
| MySQL | `mysql+pymysql://…` | MySQL 8.4 and 9.7 service jobs |
2222
| MariaDB | `mysql+pymysql://…` | MariaDB 11.4, 11.8, and 12.3 service jobs |
23+
| Dolt | `mysql+pymysql://…` | Exact Dolt 2.2.2 service job |
2324

2425
Separate service matrices test the shared MySQL/MariaDB profile contract while
25-
retaining distinct active-server identities and version claims. This does not
26+
retaining distinct active-server identities and version claims. Dolt is an
27+
independent core profile that fails closed unless the product version is exactly
28+
2.2.2; the optional Transformation Workflow remains SQLite-only. This does not
2629
claim coverage for every server configuration. The machine-readable capability
2730
declaration reports both the theoretical profile boundaries and effective
2831
limits observed from the active connection.
@@ -49,13 +52,16 @@ machine-readable loss report with the export result.
4952
1. Publish the pinned `openstatspec-pyspssio==0.5.1.post2` engine distribution
5053
first and confirm that a clean environment can download it from PyPI. The
5154
main package has no fallback SPSS engine.
52-
2. Run `python -m pytest`.
55+
2. Run `python -m pytest -m "not services"`.
5356
3. Confirm the GitHub Actions matrix is green for PostgreSQL 17/18, MySQL
54-
8.4/9.7, and MariaDB 11.4/11.8/12.3.
57+
8.4/9.7, MariaDB 11.4/11.8/12.3, and exact Dolt 2.2.2.
5558
4. Build with `python -m build` and install the generated wheel in a clean
5659
environment.
5760
5. Confirm `openstatspec capabilities` reflects the intended support boundary.
58-
6. Review this document, the README, and CHANGELOG for accurate scope.
61+
6. Confirm the `v0.2.0` tag matches package version `0.2.0`, and that CI and
62+
release fixtures use specification commit
63+
`34141dda023d9e0217c37c232e39f436edfb0746`.
64+
7. Review this document, the README, and CHANGELOG for accurate scope.
5965

6066
The tag-triggered release workflow repeats the non-service test suite, builds
6167
the distributions, and installs the wheel with the exact required SPSS engine

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "openstatspec"
7-
version = "0.1.0"
7+
version = "0.2.0"
88
description = "Reference adapter for the OpenStatSpec relational contract"
99
readme = "README.md"
1010
requires-python = ">=3.11"

‎tests/test_release_ref.py‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from pathlib import Path
5+
import subprocess
6+
import sys
7+
8+
9+
VERIFY_RELEASE_REF = Path(__file__).parents[1] / ".github" / "verify_release_ref.py"
10+
11+
12+
def _git(repository: Path, *arguments: str) -> str:
13+
completed = subprocess.run(
14+
["git", *arguments],
15+
cwd=repository,
16+
check=True,
17+
stdout=subprocess.PIPE,
18+
text=True,
19+
)
20+
return completed.stdout.strip()
21+
22+
23+
def _repository(tmp_path: Path) -> Path:
24+
repository = tmp_path / "repository"
25+
repository.mkdir()
26+
_git(repository, "init", "--quiet")
27+
_git(repository, "config", "user.name", "Release Test")
28+
_git(repository, "config", "user.email", "release@example.invalid")
29+
(repository / "pyproject.toml").write_text(
30+
'[project]\nname = "openstatspec"\nversion = "0.2.0"\n',
31+
encoding="utf-8",
32+
)
33+
_git(repository, "add", "pyproject.toml")
34+
_git(repository, "commit", "--quiet", "-m", "fixture")
35+
return repository
36+
37+
38+
def _verify(repository: Path, release_tag: str) -> subprocess.CompletedProcess[str]:
39+
environment = os.environ.copy()
40+
environment["RELEASE_TAG"] = release_tag
41+
return subprocess.run(
42+
[sys.executable, str(VERIFY_RELEASE_REF)],
43+
cwd=repository,
44+
env=environment,
45+
check=False,
46+
stdout=subprocess.PIPE,
47+
stderr=subprocess.PIPE,
48+
text=True,
49+
)
50+
51+
52+
def test_release_ref_accepts_annotated_version_tag_at_head(tmp_path: Path) -> None:
53+
repository = _repository(tmp_path)
54+
_git(repository, "tag", "-a", "v0.2.0", "-m", "release")
55+
56+
assert _verify(repository, "v0.2.0").returncode == 0
57+
58+
59+
def test_release_ref_rejects_branch_with_version_name(tmp_path: Path) -> None:
60+
repository = _repository(tmp_path)
61+
_git(repository, "branch", "v0.2.0")
62+
63+
result = _verify(repository, "v0.2.0")
64+
65+
assert result.returncode == 1
66+
assert "does not exist" in result.stderr
67+
68+
69+
def test_release_ref_rejects_commit_sha_input(tmp_path: Path) -> None:
70+
repository = _repository(tmp_path)
71+
72+
result = _verify(repository, _git(repository, "rev-parse", "HEAD"))
73+
74+
assert result.returncode == 1
75+
assert "does not match package version" in result.stderr
76+
77+
78+
def test_release_ref_rejects_missing_version_tag(tmp_path: Path) -> None:
79+
repository = _repository(tmp_path)
80+
81+
result = _verify(repository, "v0.2.0")
82+
83+
assert result.returncode == 1
84+
assert "does not exist" in result.stderr
85+
86+
87+
def test_release_ref_rejects_tag_targeting_another_commit(tmp_path: Path) -> None:
88+
repository = _repository(tmp_path)
89+
_git(repository, "tag", "-a", "v0.2.0", "-m", "release")
90+
(repository / "README.md").write_text("later commit\n", encoding="utf-8")
91+
_git(repository, "add", "README.md")
92+
_git(repository, "commit", "--quiet", "-m", "later")
93+
94+
result = _verify(repository, "v0.2.0")
95+
96+
assert result.returncode == 1
97+
assert "not HEAD" in result.stderr

0 commit comments

Comments
 (0)