portlearn 0.0.1.dev0__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.
- portlearn-0.0.1.dev0/.github/workflows/ci.yml +50 -0
- portlearn-0.0.1.dev0/.github/workflows/release.yml +425 -0
- portlearn-0.0.1.dev0/.gitignore +20 -0
- portlearn-0.0.1.dev0/CHANGELOG.md +23 -0
- portlearn-0.0.1.dev0/LICENSE +202 -0
- portlearn-0.0.1.dev0/PKG-INFO +70 -0
- portlearn-0.0.1.dev0/README.md +59 -0
- portlearn-0.0.1.dev0/examples/foundation_contract_wiring.py +136 -0
- portlearn-0.0.1.dev0/pyproject.toml +39 -0
- portlearn-0.0.1.dev0/scripts/verify_built_wheel.py +284 -0
- portlearn-0.0.1.dev0/src/portlearn/__init__.py +18 -0
- portlearn-0.0.1.dev0/src/portlearn/interfaces.py +539 -0
- portlearn-0.0.1.dev0/src/portlearn/leakage.py +290 -0
- portlearn-0.0.1.dev0/src/portlearn/manifest.py +298 -0
- portlearn-0.0.1.dev0/src/portlearn/observations.py +288 -0
- portlearn-0.0.1.dev0/src/portlearn/py.typed +0 -0
- portlearn-0.0.1.dev0/src/portlearn/timing.py +328 -0
- portlearn-0.0.1.dev0/tests/test_aware_validator_dedup.py +277 -0
- portlearn-0.0.1.dev0/tests/test_fold_semantics.py +439 -0
- portlearn-0.0.1.dev0/tests/test_information_contracts.py +588 -0
- portlearn-0.0.1.dev0/tests/test_interface_contracts.py +1109 -0
- portlearn-0.0.1.dev0/tests/test_invariant_battery.py +689 -0
- portlearn-0.0.1.dev0/tests/test_package_contract.py +388 -0
- portlearn-0.0.1.dev0/tests/test_public_release_mechanism.py +1218 -0
- portlearn-0.0.1.dev0/tests/test_public_release_surface.py +420 -0
- portlearn-0.0.1.dev0/tests/test_timing_contracts.py +567 -0
- portlearn-0.0.1.dev0/tests/test_wiring_contracts.py +605 -0
- portlearn-0.0.1.dev0/uv.lock +108 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Public CI for PortLearn.
|
|
2
|
+
# Runs on every push and pull request, guarded to the public fmasoudy/
|
|
3
|
+
# PortLearn repository, least-privilege permissions, frozen
|
|
4
|
+
# per-interpreter sequence, single dependent build-and-verify-wheel job,
|
|
5
|
+
# immutable action pins.
|
|
6
|
+
name: ci
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
pull_request:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
checks:
|
|
18
|
+
if: github.repository == 'fmasoudy/PortLearn'
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
24
|
+
steps:
|
|
25
|
+
- name: Check out the repository
|
|
26
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
27
|
+
- name: Set up uv
|
|
28
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
29
|
+
with:
|
|
30
|
+
version: "0.12.10"
|
|
31
|
+
- name: Run the frozen per-interpreter sequence
|
|
32
|
+
run: |
|
|
33
|
+
uv sync --locked --python ${{ matrix.python-version }}
|
|
34
|
+
uv run --python ${{ matrix.python-version }} ruff check .
|
|
35
|
+
uv run --python ${{ matrix.python-version }} pytest
|
|
36
|
+
build-and-verify-wheel:
|
|
37
|
+
if: github.repository == 'fmasoudy/PortLearn'
|
|
38
|
+
needs: checks
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- name: Check out the repository
|
|
42
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
43
|
+
- name: Set up uv
|
|
44
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
45
|
+
with:
|
|
46
|
+
version: "0.12.10"
|
|
47
|
+
- name: Build the distribution
|
|
48
|
+
run: |
|
|
49
|
+
uv build
|
|
50
|
+
uv run python scripts/verify_built_wheel.py
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Controlled public release mechanism.
|
|
2
|
+
#
|
|
3
|
+
# This workflow is the intended path for creating a public version tag
|
|
4
|
+
# and its GitHub Release. It never runs automatically: workflow_dispatch
|
|
5
|
+
# is the sole trigger, so every run is a deliberate, manually dispatched
|
|
6
|
+
# release act.
|
|
7
|
+
#
|
|
8
|
+
# It validates every consistency rule FIRST and fails closed on any
|
|
9
|
+
# mismatch, leaving no partial state beyond the failed run log. It never
|
|
10
|
+
# deletes, moves, or overwrites an existing tag or release. Existence
|
|
11
|
+
# queries fail closed the same way: a query whose answer cannot be
|
|
12
|
+
# confirmed (transport error, auth error, unexpected HTTP status)
|
|
13
|
+
# aborts the run rather than being read as "absent". Runs are also
|
|
14
|
+
# serialized in a 'public-release' concurrency group (no cancel), so
|
|
15
|
+
# two dispatches cannot race between validation and creation.
|
|
16
|
+
#
|
|
17
|
+
# Development releases are rejected: a version carrying a PEP 440
|
|
18
|
+
# development segment (for example 0.0.1.dev0) describes an
|
|
19
|
+
# unreleased development state and must never receive a public release
|
|
20
|
+
# tag or GitHub Release. The validation module fails closed on it.
|
|
21
|
+
# Pre-release versions (a/b/rc) are likewise rejected: this mechanism
|
|
22
|
+
# does not mark the GitHub Release as a prerelease, so RC support is
|
|
23
|
+
# deferred until the creation step can label them correctly.
|
|
24
|
+
#
|
|
25
|
+
# Tag-object design decision: creation is a single release-creation
|
|
26
|
+
# command with --target, which creates the release's own lightweight tag
|
|
27
|
+
# at the exact target commit together with the Release. A separate
|
|
28
|
+
# annotated tag created and pushed on its own would split the operation
|
|
29
|
+
# in two and risk partial state (an annotated tag with no Release).
|
|
30
|
+
# Keeping creation to one command reduces partial-state risk; it is not
|
|
31
|
+
# a transaction spanning the underlying API calls. The release-created
|
|
32
|
+
# lightweight tag is therefore selected deliberately to minimize
|
|
33
|
+
# partial-state risk; release provenance is carried by the Release
|
|
34
|
+
# record, the pinned target commit, and the manually dispatched run,
|
|
35
|
+
# not by tag-object metadata.
|
|
36
|
+
name: public-release
|
|
37
|
+
|
|
38
|
+
on:
|
|
39
|
+
workflow_dispatch:
|
|
40
|
+
inputs:
|
|
41
|
+
version:
|
|
42
|
+
description: "Requested release version; must exactly equal [project].version in pyproject.toml at the target commit (the single version authority) and must be a final release: development (dev) and pre-release (a/b/rc) versions are rejected."
|
|
43
|
+
required: true
|
|
44
|
+
type: string
|
|
45
|
+
tag_name:
|
|
46
|
+
description: "Release tag name; must equal v plus the pyproject.toml version (for example v0.0.1)."
|
|
47
|
+
required: true
|
|
48
|
+
type: string
|
|
49
|
+
target_sha:
|
|
50
|
+
description: "Full 40-hex commit SHA of public main to tag; it must exist and be the current origin/main head."
|
|
51
|
+
required: true
|
|
52
|
+
type: string
|
|
53
|
+
|
|
54
|
+
permissions:
|
|
55
|
+
contents: write
|
|
56
|
+
|
|
57
|
+
concurrency:
|
|
58
|
+
group: public-release
|
|
59
|
+
cancel-in-progress: false
|
|
60
|
+
|
|
61
|
+
jobs:
|
|
62
|
+
release:
|
|
63
|
+
if: github.repository == 'fmasoudy/PortLearn'
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
env:
|
|
66
|
+
PL_VERSION: ${{ inputs.version }}
|
|
67
|
+
PL_TAG_NAME: ${{ inputs.tag_name }}
|
|
68
|
+
PL_TARGET_SHA: ${{ inputs.target_sha }}
|
|
69
|
+
steps:
|
|
70
|
+
- name: Check out the exact target commit
|
|
71
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
72
|
+
with:
|
|
73
|
+
ref: ${{ inputs.target_sha }}
|
|
74
|
+
fetch-depth: 0
|
|
75
|
+
# Least authority: no later step may use a persisted token;
|
|
76
|
+
# every repo operation after this is read-only except the
|
|
77
|
+
# final release creation, which names its token and
|
|
78
|
+
# repository explicitly.
|
|
79
|
+
persist-credentials: false
|
|
80
|
+
- name: Fail closed unless the target commit is the current origin main head
|
|
81
|
+
run: |
|
|
82
|
+
set -euo pipefail
|
|
83
|
+
git fetch --no-tags origin "+refs/heads/main:refs/remotes/origin/main"
|
|
84
|
+
git fetch --tags origin
|
|
85
|
+
git cat-file -e "${PL_TARGET_SHA}^{commit}"
|
|
86
|
+
main_head="$(git rev-parse refs/remotes/origin/main)"
|
|
87
|
+
if [ "$main_head" != "$PL_TARGET_SHA" ]; then
|
|
88
|
+
echo "::error::target ${PL_TARGET_SHA} is not the current origin/main head (${main_head}); a stale or off-main commit is never tagged"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
- name: Fail closed if the tag already exists locally or on the remote
|
|
92
|
+
run: |
|
|
93
|
+
set -euo pipefail
|
|
94
|
+
if [ -n "$(git tag --list "$PL_TAG_NAME")" ]; then
|
|
95
|
+
echo "::error::local tag ${PL_TAG_NAME} already exists; existing tags are never overwritten"
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
# A failed remote query must never be read as tag-absence:
|
|
99
|
+
# capture the query status separately and abort on any nonzero
|
|
100
|
+
# (set -e does not fire inside if-conditions, so only the
|
|
101
|
+
# explicit status check below makes failure observable).
|
|
102
|
+
remote_tag_query="$(git ls-remote --tags origin "refs/tags/$PL_TAG_NAME")"
|
|
103
|
+
remote_tag_query_status="$?"
|
|
104
|
+
if [ "$remote_tag_query_status" -ne 0 ]; then
|
|
105
|
+
echo "::error::remote tag query for ${PL_TAG_NAME} failed (exit ${remote_tag_query_status}); absence cannot be confirmed"
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
if [ -n "$remote_tag_query" ]; then
|
|
109
|
+
echo "::error::remote tag ${PL_TAG_NAME} already exists; existing tags are never overwritten"
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
- name: Fail closed if the GitHub Release already exists
|
|
113
|
+
env:
|
|
114
|
+
GH_TOKEN: ${{ github.token }}
|
|
115
|
+
GH_REPO: ${{ github.repository }}
|
|
116
|
+
run: |
|
|
117
|
+
set -euo pipefail
|
|
118
|
+
# Only an explicit answer permits the run to proceed: 200 means
|
|
119
|
+
# the release exists (abort), 404 means absent (proceed), and
|
|
120
|
+
# every other status — auth failure, rate limit, server or
|
|
121
|
+
# transport error — aborts. The CLI release-view helper cannot
|
|
122
|
+
# express this: its exit 1 conflates 404 with every failure
|
|
123
|
+
# mode, so the guard probes the REST endpoint directly.
|
|
124
|
+
release_status="$(curl --silent --show-error \
|
|
125
|
+
--output /dev/null \
|
|
126
|
+
--write-out "%{http_code}" \
|
|
127
|
+
-H "Authorization: Bearer ${GH_TOKEN}" \
|
|
128
|
+
-H "Accept: application/vnd.github+json" \
|
|
129
|
+
"$GITHUB_API_URL/repos/$GH_REPO/releases/tags/$PL_TAG_NAME")"
|
|
130
|
+
if [ "$release_status" = "200" ]; then
|
|
131
|
+
echo "::error::a GitHub Release for ${PL_TAG_NAME} already exists; an existing release is never overwritten"
|
|
132
|
+
exit 1
|
|
133
|
+
elif [ "$release_status" = "404" ]; then
|
|
134
|
+
echo "no existing GitHub Release for ${PL_TAG_NAME} (HTTP 404)"
|
|
135
|
+
else
|
|
136
|
+
echo "::error::release-existence query for ${PL_TAG_NAME} returned HTTP ${release_status}; absence cannot be confirmed"
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
- name: Set up uv
|
|
140
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
141
|
+
with:
|
|
142
|
+
version: "0.12.10"
|
|
143
|
+
- name: Write the release validation module
|
|
144
|
+
run: |
|
|
145
|
+
cat > "${RUNNER_TEMP}/release_validation.py" <<'PY'
|
|
146
|
+
# --- portlearn-release-validation: begin (pure, offline) ---
|
|
147
|
+
"""Pure, offline release-consistency validation.
|
|
148
|
+
|
|
149
|
+
This module is deliberately standalone: it reads only the
|
|
150
|
+
pyproject.toml and CHANGELOG.md of the checked-out target commit
|
|
151
|
+
plus a caller-supplied list of existing version tags. It performs
|
|
152
|
+
no network or repository access and it creates nothing. Every rule
|
|
153
|
+
fails closed by raising ReleaseValidationError with the reason;
|
|
154
|
+
the caller reports the reason and stops before anything is created.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
from __future__ import annotations
|
|
158
|
+
|
|
159
|
+
import argparse
|
|
160
|
+
import re
|
|
161
|
+
import sys
|
|
162
|
+
import tomllib
|
|
163
|
+
from pathlib import Path
|
|
164
|
+
|
|
165
|
+
from packaging.version import InvalidVersion, Version
|
|
166
|
+
|
|
167
|
+
FULL_SHA_PATTERN = re.compile(r"[0-9a-f]{40}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ReleaseValidationError(Exception):
|
|
171
|
+
"""A release-consistency rule failed; nothing may be created."""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def heading_pattern(version: str) -> re.Pattern[str]:
|
|
175
|
+
"""The Keep-a-Changelog release heading pattern for one version."""
|
|
176
|
+
return re.compile(
|
|
177
|
+
rf"^## \[{re.escape(version)}\](?:\s.*)?$", re.MULTILINE
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def changelog_section(changelog_text: str, version: str) -> str | None:
|
|
182
|
+
"""The body of one versioned release heading, or None if absent."""
|
|
183
|
+
match = heading_pattern(version).search(changelog_text)
|
|
184
|
+
if match is None:
|
|
185
|
+
return None
|
|
186
|
+
rest = changelog_text[match.end() :]
|
|
187
|
+
following = re.search(r"^## ", rest, re.MULTILINE)
|
|
188
|
+
return rest[: following.start()] if following else rest
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def meaningful_content(section_text: str) -> list[str]:
|
|
192
|
+
"""The content lines of one section body.
|
|
193
|
+
|
|
194
|
+
Headings (the Keep-a-Changelog ``###`` subsections and the
|
|
195
|
+
``##`` version/Unreleased headings) are structure, not
|
|
196
|
+
content: an otherwise-empty scaffold of headings is an
|
|
197
|
+
empty section. Everything else — bullets, prose, any
|
|
198
|
+
non-empty non-heading line — is content that must be
|
|
199
|
+
released or moved before a release is cut.
|
|
200
|
+
"""
|
|
201
|
+
return [
|
|
202
|
+
line.strip()
|
|
203
|
+
for line in section_text.splitlines()
|
|
204
|
+
if line.strip() and not line.lstrip().startswith("#")
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def declared_version(pyproject_path: Path | str) -> str:
|
|
209
|
+
"""The version declared by pyproject.toml at the target commit."""
|
|
210
|
+
data = tomllib.loads(Path(pyproject_path).read_text(encoding="utf-8"))
|
|
211
|
+
try:
|
|
212
|
+
return str(data["project"]["version"])
|
|
213
|
+
except KeyError as error:
|
|
214
|
+
message = (
|
|
215
|
+
"pyproject.toml at the target commit declares no "
|
|
216
|
+
"[project] version"
|
|
217
|
+
)
|
|
218
|
+
raise ReleaseValidationError(message) from error
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def check_monotone_forward(
|
|
222
|
+
version: str, existing_version_tags: list[str]
|
|
223
|
+
) -> None:
|
|
224
|
+
"""The candidate version must be strictly after every existing tag."""
|
|
225
|
+
candidate = Version(version)
|
|
226
|
+
for tag in existing_version_tags:
|
|
227
|
+
name = tag[1:] if tag.startswith("v") else tag
|
|
228
|
+
try:
|
|
229
|
+
existing = Version(name)
|
|
230
|
+
except InvalidVersion:
|
|
231
|
+
continue # not a version tag; it does not constrain ordering
|
|
232
|
+
if candidate <= existing:
|
|
233
|
+
message = (
|
|
234
|
+
f"version {version} is not monotone-forward: the "
|
|
235
|
+
f"existing public version tag {tag} is at or after it"
|
|
236
|
+
)
|
|
237
|
+
raise ReleaseValidationError(message)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def reject_development_release(candidate: Version) -> None:
|
|
241
|
+
"""A PEP 440 development release must never be released publicly.
|
|
242
|
+
|
|
243
|
+
A version carrying a development segment (``.devN``, as in
|
|
244
|
+
``0.0.1.dev0``) describes an unreleased development state,
|
|
245
|
+
not a releasable version: tagging it would publish a moving
|
|
246
|
+
target. Pre-release segments (``a``, ``b``, ``rc``) are
|
|
247
|
+
rejected separately by ``reject_prerelease_version``.
|
|
248
|
+
"""
|
|
249
|
+
if candidate.dev is not None:
|
|
250
|
+
message = (
|
|
251
|
+
f"version {str(candidate)!r} is a PEP 440 development "
|
|
252
|
+
"release (dev segment present): development releases "
|
|
253
|
+
"are never tagged or released publicly"
|
|
254
|
+
)
|
|
255
|
+
raise ReleaseValidationError(message)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def reject_prerelease_version(candidate: Version) -> None:
|
|
259
|
+
"""Pre-release versions are deferred, not released.
|
|
260
|
+
|
|
261
|
+
This mechanism creates GitHub Releases without the
|
|
262
|
+
prerelease flag, so publishing ``a``/``b``/``rc`` versions
|
|
263
|
+
would mislabel them as full releases. Pre-release support
|
|
264
|
+
must be added to the creation step before any pre-release
|
|
265
|
+
may validate.
|
|
266
|
+
"""
|
|
267
|
+
if candidate.pre is not None:
|
|
268
|
+
message = (
|
|
269
|
+
f"version {str(candidate)!r} is a PEP 440 pre-release "
|
|
270
|
+
f"({candidate.pre[0]}{candidate.pre[1]} segment "
|
|
271
|
+
"present): prerelease (RC) support is deferred, so "
|
|
272
|
+
"pre-release versions are never tagged or released "
|
|
273
|
+
"through this mechanism"
|
|
274
|
+
)
|
|
275
|
+
raise ReleaseValidationError(message)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def validate_release(
|
|
279
|
+
version: str,
|
|
280
|
+
tag_name: str,
|
|
281
|
+
target_sha: str,
|
|
282
|
+
pyproject_path: Path | str,
|
|
283
|
+
changelog_path: Path | str,
|
|
284
|
+
existing_version_tags: list[str] | None = None,
|
|
285
|
+
) -> str:
|
|
286
|
+
"""Validate every release-consistency rule; return the release notes.
|
|
287
|
+
|
|
288
|
+
Any violation raises ReleaseValidationError describing the broken
|
|
289
|
+
rule; the caller must abort before creating anything.
|
|
290
|
+
"""
|
|
291
|
+
if not FULL_SHA_PATTERN.fullmatch(target_sha):
|
|
292
|
+
message = (
|
|
293
|
+
f"target {target_sha!r} is not a 40-character full "
|
|
294
|
+
"commit SHA; a shortened or malformed target is never "
|
|
295
|
+
"tagged"
|
|
296
|
+
)
|
|
297
|
+
raise ReleaseValidationError(message)
|
|
298
|
+
|
|
299
|
+
declared = declared_version(pyproject_path)
|
|
300
|
+
if version != declared:
|
|
301
|
+
message = (
|
|
302
|
+
f"requested version {version!r} does not equal the "
|
|
303
|
+
"version declared by pyproject.toml at the target "
|
|
304
|
+
f"commit ({declared!r})"
|
|
305
|
+
)
|
|
306
|
+
raise ReleaseValidationError(message)
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
candidate = Version(version)
|
|
310
|
+
except InvalidVersion as error:
|
|
311
|
+
message = (
|
|
312
|
+
f"requested version {version!r} is not a valid "
|
|
313
|
+
"PEP 440 version"
|
|
314
|
+
)
|
|
315
|
+
raise ReleaseValidationError(message) from error
|
|
316
|
+
|
|
317
|
+
reject_development_release(candidate)
|
|
318
|
+
reject_prerelease_version(candidate)
|
|
319
|
+
|
|
320
|
+
expected_tag = f"v{version}"
|
|
321
|
+
if tag_name != expected_tag:
|
|
322
|
+
message = (
|
|
323
|
+
f"tag name {tag_name!r} must equal 'v' plus the "
|
|
324
|
+
f"declared version ({expected_tag!r})"
|
|
325
|
+
)
|
|
326
|
+
raise ReleaseValidationError(message)
|
|
327
|
+
|
|
328
|
+
changelog_text = Path(changelog_path).read_text(encoding="utf-8")
|
|
329
|
+
section = changelog_section(changelog_text, version)
|
|
330
|
+
if section is None:
|
|
331
|
+
message = (
|
|
332
|
+
f"CHANGELOG.md has no release section for {version}: "
|
|
333
|
+
f"the '## [{version}]' heading is required before a "
|
|
334
|
+
"release"
|
|
335
|
+
)
|
|
336
|
+
raise ReleaseValidationError(message)
|
|
337
|
+
if not meaningful_content(section):
|
|
338
|
+
message = (
|
|
339
|
+
f"the CHANGELOG.md [{version}] release section must "
|
|
340
|
+
"not be empty: the release notes are drawn from it"
|
|
341
|
+
)
|
|
342
|
+
raise ReleaseValidationError(message)
|
|
343
|
+
|
|
344
|
+
unreleased = changelog_section(changelog_text, "Unreleased")
|
|
345
|
+
pending = meaningful_content(unreleased) if unreleased is not None else []
|
|
346
|
+
if pending:
|
|
347
|
+
message = (
|
|
348
|
+
"the CHANGELOG.md [Unreleased] section is not empty "
|
|
349
|
+
f"or absent: {len(pending)} pending entries must move "
|
|
350
|
+
"into release sections first"
|
|
351
|
+
)
|
|
352
|
+
raise ReleaseValidationError(message)
|
|
353
|
+
|
|
354
|
+
check_monotone_forward(version, existing_version_tags or [])
|
|
355
|
+
return section.strip()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def main(argv: list[str] | None = None) -> int:
|
|
359
|
+
"""Run validation from command-line arguments; 0 on success."""
|
|
360
|
+
parser = argparse.ArgumentParser(
|
|
361
|
+
description="Validate a public release request (fail-closed)."
|
|
362
|
+
)
|
|
363
|
+
parser.add_argument("--version", required=True)
|
|
364
|
+
parser.add_argument("--tag-name", required=True)
|
|
365
|
+
parser.add_argument("--target-sha", required=True)
|
|
366
|
+
parser.add_argument("--pyproject", required=True)
|
|
367
|
+
parser.add_argument("--changelog", required=True)
|
|
368
|
+
parser.add_argument("--existing-tags-file", required=True)
|
|
369
|
+
parser.add_argument("--notes-out", required=True)
|
|
370
|
+
args = parser.parse_args(argv)
|
|
371
|
+
|
|
372
|
+
tags_path = Path(args.existing_tags_file)
|
|
373
|
+
existing: list[str] = []
|
|
374
|
+
if tags_path.is_file():
|
|
375
|
+
existing = [
|
|
376
|
+
line.strip()
|
|
377
|
+
for line in tags_path.read_text(encoding="utf-8").splitlines()
|
|
378
|
+
if line.strip()
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
notes = validate_release(
|
|
383
|
+
version=args.version,
|
|
384
|
+
tag_name=args.tag_name,
|
|
385
|
+
target_sha=args.target_sha,
|
|
386
|
+
pyproject_path=args.pyproject,
|
|
387
|
+
changelog_path=args.changelog,
|
|
388
|
+
existing_version_tags=existing,
|
|
389
|
+
)
|
|
390
|
+
except ReleaseValidationError as error:
|
|
391
|
+
print(f"release validation failed: {error}", file=sys.stderr)
|
|
392
|
+
return 1
|
|
393
|
+
|
|
394
|
+
Path(args.notes_out).write_text(notes + "\n", encoding="utf-8")
|
|
395
|
+
print(f"release validation passed; notes written to {args.notes_out}")
|
|
396
|
+
return 0
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == "__main__":
|
|
400
|
+
raise SystemExit(main())
|
|
401
|
+
# --- portlearn-release-validation: end ---
|
|
402
|
+
PY
|
|
403
|
+
- name: Collect existing public version tags
|
|
404
|
+
run: |
|
|
405
|
+
set -euo pipefail
|
|
406
|
+
git tag --list "v*" | sort > "${RUNNER_TEMP}/existing_tags.txt"
|
|
407
|
+
- name: Fail closed on any release-consistency mismatch
|
|
408
|
+
run: |
|
|
409
|
+
set -euo pipefail
|
|
410
|
+
uv sync --locked
|
|
411
|
+
uv run python "${RUNNER_TEMP}/release_validation.py" \
|
|
412
|
+
--version "$PL_VERSION" \
|
|
413
|
+
--tag-name "$PL_TAG_NAME" \
|
|
414
|
+
--target-sha "$PL_TARGET_SHA" \
|
|
415
|
+
--pyproject pyproject.toml \
|
|
416
|
+
--changelog CHANGELOG.md \
|
|
417
|
+
--existing-tags-file "${RUNNER_TEMP}/existing_tags.txt" \
|
|
418
|
+
--notes-out "${RUNNER_TEMP}/release-notes.md"
|
|
419
|
+
- name: Create the GitHub Release and its tag in one action
|
|
420
|
+
env:
|
|
421
|
+
GH_TOKEN: ${{ github.token }}
|
|
422
|
+
GH_REPO: ${{ github.repository }}
|
|
423
|
+
run: |
|
|
424
|
+
set -euo pipefail
|
|
425
|
+
gh release create "$PL_TAG_NAME" --target "$PL_TARGET_SHA" --title "$PL_TAG_NAME" --notes-file "${RUNNER_TEMP}/release-notes.md"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
|
|
11
|
+
# Environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
.env
|
|
15
|
+
|
|
16
|
+
# OS
|
|
17
|
+
.DS_Store
|
|
18
|
+
|
|
19
|
+
# Local scratch (never commit run artifacts without an explicit milestone decision)
|
|
20
|
+
scratch/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable public changes to PortLearn are documented in this file.
|
|
4
|
+
|
|
5
|
+
Git history remains the detailed development record. This changelog is the curated user/researcher-facing history: it tracks meaningful public changes and public software releases, with release sections recording releases and the entries within them recording user-visible changes.
|
|
6
|
+
|
|
7
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows the project's PEP 440 policy: early-development `0.0.x` public releases.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.0.1.dev0]
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Foundation research contracts: time/chronology contracts, observation handling, information sets, core research interfaces, information-leakage validation, and run manifests, with a synthetic contract-wiring example, public test suite, and public CI.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
### Deprecated
|
|
22
|
+
|
|
23
|
+
### Removed
|