qso-graph-lib 0.1.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.
- qso_graph_lib-0.1.0/.github/dependabot.yml +37 -0
- qso_graph_lib-0.1.0/.github/workflows/ci.yml +136 -0
- qso_graph_lib-0.1.0/.github/workflows/publish.yml +306 -0
- qso_graph_lib-0.1.0/.gitignore +12 -0
- qso_graph_lib-0.1.0/CHANGELOG.md +16 -0
- qso_graph_lib-0.1.0/LICENSE +674 -0
- qso_graph_lib-0.1.0/PKG-INFO +77 -0
- qso_graph_lib-0.1.0/README.md +53 -0
- qso_graph_lib-0.1.0/pyproject.toml +75 -0
- qso_graph_lib-0.1.0/src/qso_graph_lib/__init__.py +15 -0
- qso_graph_lib-0.1.0/src/qso_graph_lib/jt/__init__.py +11 -0
- qso_graph_lib-0.1.0/src/qso_graph_lib/jt/alltxt.py +103 -0
- qso_graph_lib-0.1.0/src/qso_graph_lib/jt/protocol.py +315 -0
- qso_graph_lib-0.1.0/src/qso_graph_lib/jt/samples/ALL-sample.TXT +120 -0
- qso_graph_lib-0.1.0/tests/test_jt_alltxt.py +68 -0
- qso_graph_lib-0.1.0/tests/test_jt_protocol.py +101 -0
- qso_graph_lib-0.1.0/tests/test_packaging.py +60 -0
- qso_graph_lib-0.1.0/tests/test_release_gate.py +74 -0
- qso_graph_lib-0.1.0/tests/test_security.py +87 -0
- qso_graph_lib-0.1.0/tests/test_version.py +9 -0
- qso_graph_lib-0.1.0/uv.lock +381 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Dependabot: security updates only, grouped into one pull request.
|
|
2
|
+
#
|
|
3
|
+
# WHY THIS FILE EXISTS (2026-10-25). There was no dependabot.yml in any of the 22
|
|
4
|
+
# servers. Without one Dependabot runs *security* updates only -- those need no
|
|
5
|
+
# config -- and they are ungrouped by default: one advisory x one package x one
|
|
6
|
+
# repository = one pull request. Four upgrades produced **57 open pull requests**
|
|
7
|
+
# across the fleet, and KI7MT's objection was the right one: nobody could say
|
|
8
|
+
# what 57 pull requests did. Measured, they were four dependency upgrades closing
|
|
9
|
+
# 130 alerts, one of them CRITICAL. The volume was not noise, it was signal
|
|
10
|
+
# nobody could read.
|
|
11
|
+
#
|
|
12
|
+
# `groups` with `applies-to: security-updates` collapses that to one pull request
|
|
13
|
+
# per repository.
|
|
14
|
+
#
|
|
15
|
+
# open-pull-requests-limit: 0 disables *version* updates, which we do not want --
|
|
16
|
+
# ci.yml audits the locked dependencies on every pull request and weekly, so a
|
|
17
|
+
# stale lock already fails CI and does not need a bot to notice. Security updates
|
|
18
|
+
# are unaffected by this limit: they have their own fixed limit of 10, which
|
|
19
|
+
# cannot be configured. Checked against the Dependabot options reference rather
|
|
20
|
+
# than assumed, because setting this wrong would silently switch off the thing
|
|
21
|
+
# that found those 130 alerts.
|
|
22
|
+
#
|
|
23
|
+
# `uv` is the ecosystem for a uv.lock project, also checked against that
|
|
24
|
+
# reference and not inferred from the file name.
|
|
25
|
+
version: 2
|
|
26
|
+
updates:
|
|
27
|
+
- package-ecosystem: "uv"
|
|
28
|
+
directory: "/"
|
|
29
|
+
schedule:
|
|
30
|
+
interval: "weekly"
|
|
31
|
+
day: "monday"
|
|
32
|
+
open-pull-requests-limit: 0
|
|
33
|
+
groups:
|
|
34
|
+
security:
|
|
35
|
+
applies-to: security-updates
|
|
36
|
+
patterns:
|
|
37
|
+
- "*"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# A pull request is tested whatever it targets, and deliberately has no branch
|
|
4
|
+
# filter. `main` is the one long-lived branch now, so `push` names it; but a filter
|
|
5
|
+
# here would mean a pull request into any other branch ran no ruff, no mypy and no
|
|
6
|
+
# pytest while still reporting green.
|
|
7
|
+
on:
|
|
8
|
+
pull_request:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
# Weekly, because an advisory is published against a lockfile that has not
|
|
12
|
+
# changed, and no pull-request-or-push trigger fires for that.
|
|
13
|
+
schedule:
|
|
14
|
+
- cron: "17 6 * * 1"
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
# Lint and types, once rather than per Python: both read the source, and
|
|
25
|
+
# neither answer changes with the interpreter running them (qso-graph-devel#66).
|
|
26
|
+
lint:
|
|
27
|
+
name: ruff and mypy
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v5
|
|
31
|
+
|
|
32
|
+
- uses: astral-sh/setup-uv@v7
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.11.18"
|
|
35
|
+
enable-cache: true
|
|
36
|
+
|
|
37
|
+
- name: Install (locked)
|
|
38
|
+
run: uv sync --group dev --frozen
|
|
39
|
+
|
|
40
|
+
- name: Lint (ruff)
|
|
41
|
+
run: uv run ruff check .
|
|
42
|
+
|
|
43
|
+
- name: Type-check (mypy)
|
|
44
|
+
run: uv run mypy src
|
|
45
|
+
|
|
46
|
+
test:
|
|
47
|
+
name: pytest (${{ matrix.python-version }})
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
strategy:
|
|
50
|
+
fail-fast: false
|
|
51
|
+
matrix:
|
|
52
|
+
# Patch versions, not minor ones. Asked for "3.12", uv takes whatever
|
|
53
|
+
# copy the runner already has -- Ubuntu 24.04's 3.12.3, twelve patches
|
|
54
|
+
# behind -- and nothing says so. These are the releases that reject the
|
|
55
|
+
# compromised eMudhra intermediate CAs (2026-10).
|
|
56
|
+
python-version: ["3.11.18", "3.12.16", "3.13.18"]
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v5
|
|
59
|
+
|
|
60
|
+
- uses: astral-sh/setup-uv@v7
|
|
61
|
+
with:
|
|
62
|
+
python-version: ${{ matrix.python-version }}
|
|
63
|
+
enable-cache: true
|
|
64
|
+
|
|
65
|
+
- name: Install (locked)
|
|
66
|
+
run: uv sync --group dev --frozen
|
|
67
|
+
|
|
68
|
+
- name: Unit tests
|
|
69
|
+
run: uv run pytest tests --ignore=tests/test_security.py -v
|
|
70
|
+
|
|
71
|
+
- name: Security tests
|
|
72
|
+
run: uv run pytest tests/test_security.py -v
|
|
73
|
+
|
|
74
|
+
# Audits the LOCKED environment, which is a different question from the one the
|
|
75
|
+
# release gate asks. publish.yml runs `pip-audit --strict --desc on .` -- the
|
|
76
|
+
# project's *declared* dependencies, resolved fresh -- which protects the people
|
|
77
|
+
# who `pip install` this package and cannot see uv.lock at all.
|
|
78
|
+
#
|
|
79
|
+
# This job syncs `--frozen` and audits what that produced, so a stale lock cannot
|
|
80
|
+
# sit unnoticed. On 2026-10-25 five MCP servers carried a CRITICAL certifi
|
|
81
|
+
# advisory in their lockfiles that Dependabot never opened a pull request for.
|
|
82
|
+
audit:
|
|
83
|
+
name: audit (locked dependencies)
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v5
|
|
87
|
+
|
|
88
|
+
- uses: astral-sh/setup-uv@v7
|
|
89
|
+
with:
|
|
90
|
+
python-version: "3.12.16"
|
|
91
|
+
enable-cache: true
|
|
92
|
+
|
|
93
|
+
- name: Install (locked)
|
|
94
|
+
run: uv sync --group dev --frozen
|
|
95
|
+
|
|
96
|
+
- name: No known vulnerability in the locked dependencies
|
|
97
|
+
# No --strict, deliberately. --strict fails on any distribution it
|
|
98
|
+
# could not audit, and `uv sync` installs the project itself -- so the
|
|
99
|
+
# moment a release bumps the version to one not yet on PyPI, pip-audit
|
|
100
|
+
# cannot look the project up and --strict turns that into a failure.
|
|
101
|
+
# It blocked every version bump in all 23 repositories, which is how it
|
|
102
|
+
# was found: the first real release, dxe-mcp 0.0.3, went red on it with
|
|
103
|
+
# "dxe-mcp: Dependency not found on PyPI and could not be audited"
|
|
104
|
+
# while the lockfile was perfectly clean.
|
|
105
|
+
#
|
|
106
|
+
# Without --strict, pip-audit still exits 1 when it FINDS a
|
|
107
|
+
# vulnerability, which is the whole job. An unauditable local project is
|
|
108
|
+
# reported as skipped, with its reason, and does not fail the run.
|
|
109
|
+
#
|
|
110
|
+
# Verified in both directions against a lock holding certifi 2026.7.22
|
|
111
|
+
# with the version bumped past PyPI: it reports PYSEC-2026-4320 and
|
|
112
|
+
# exits 1; with the lock current it reports no vulnerabilities, names the
|
|
113
|
+
# project as skipped, and exits 0.
|
|
114
|
+
run: uv run --with pip-audit pip-audit
|
|
115
|
+
|
|
116
|
+
ci-all-green:
|
|
117
|
+
name: ci-all-green
|
|
118
|
+
needs: [lint, test, audit]
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
if: always()
|
|
121
|
+
steps:
|
|
122
|
+
- name: Verify required jobs all succeeded
|
|
123
|
+
run: |
|
|
124
|
+
if [[ "${{ needs.lint.result }}" != "success" ]]; then
|
|
125
|
+
echo "FAIL: ruff or mypy did not pass (result=${{ needs.lint.result }})"
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
if [[ "${{ needs.test.result }}" != "success" ]]; then
|
|
129
|
+
echo "FAIL: test matrix did not all succeed (result=${{ needs.test.result }})"
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
if [[ "${{ needs.audit.result }}" != "success" ]]; then
|
|
133
|
+
echo "FAIL: the locked dependencies have a known vulnerability (result=${{ needs.audit.result }})"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
echo "All required CI jobs passed."
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# A release happens on purpose, not as a side effect of merging (KI7MT, 2026-10-25).
|
|
4
|
+
# qso-graph-lib is a library: PyPI only, no MCP Registry entry, no server.json.
|
|
5
|
+
# This file is qso-graph-auth's, the org's other PyPI-only package, so the two release
|
|
6
|
+
# the same way.
|
|
7
|
+
#
|
|
8
|
+
# `main` is the one working branch. Merging into it builds and tests and publishes
|
|
9
|
+
# nothing; a release is Actions -> Publish to PyPI -> Run workflow, with the version typed
|
|
10
|
+
# in. Typed, because it is a decision: it must match what the tree already says.
|
|
11
|
+
on:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
inputs:
|
|
14
|
+
version:
|
|
15
|
+
description: "The version to publish, e.g. 0.1.7 — must match pyproject.toml"
|
|
16
|
+
required: true
|
|
17
|
+
type: string
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: publish
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
security:
|
|
28
|
+
name: Security gates
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
checks: read
|
|
33
|
+
outputs:
|
|
34
|
+
version: ${{ steps.version.outputs.version }}
|
|
35
|
+
package: ${{ steps.version.outputs.package }}
|
|
36
|
+
# "true" when this version is on PyPI with no tag: an interrupted
|
|
37
|
+
# release being finished, not a new one being made.
|
|
38
|
+
resume: ${{ steps.version.outputs.resume }}
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v5
|
|
41
|
+
|
|
42
|
+
# workflow_dispatch offers a branch picker, so unlike the old push trigger this
|
|
43
|
+
# workflow can be started from anywhere. The lock is the pypi Environment's
|
|
44
|
+
# deployment branch rule (main only); this is the same rule stated where a failed
|
|
45
|
+
# run can show it, because an Environment rejection does not say what was wrong.
|
|
46
|
+
- name: This runs from main
|
|
47
|
+
run: |
|
|
48
|
+
if [ "$GITHUB_REF" != "refs/heads/main" ]; then
|
|
49
|
+
echo "FAIL: a release is published from main only (this is $GITHUB_REF)"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Merging no longer publishes, so nothing else guarantees that what is about to be
|
|
54
|
+
# released is the code CI passed on. Under the old trigger the release PR had to be
|
|
55
|
+
# green before the merge that published it; this restores that property.
|
|
56
|
+
- name: CI passed on the commit being published
|
|
57
|
+
env:
|
|
58
|
+
GH_TOKEN: ${{ github.token }}
|
|
59
|
+
run: |
|
|
60
|
+
PASSED="$(gh api \
|
|
61
|
+
"repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/check-runs?check_name=ci-all-green" \
|
|
62
|
+
--jq '[.check_runs[] | select(.conclusion == "success")] | length')"
|
|
63
|
+
if [ "${PASSED:-0}" -lt 1 ]; then
|
|
64
|
+
echo "FAIL: ci-all-green has not passed on ${GITHUB_SHA}"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
echo "ci-all-green passed on ${GITHUB_SHA}"
|
|
68
|
+
|
|
69
|
+
# A release publishes a version that is not out yet. Computed once; every later job
|
|
70
|
+
# uses it. The gate fails closed: anything it cannot confirm stops the release.
|
|
71
|
+
- name: The version is the one asked for, and new
|
|
72
|
+
id: version
|
|
73
|
+
env:
|
|
74
|
+
# Through the environment rather than interpolated into the script: a ${{ }}
|
|
75
|
+
# expansion inside run: is substituted before the shell sees it, so the input
|
|
76
|
+
# would be script rather than data. Only a writer can dispatch this, and the
|
|
77
|
+
# rule is still input validation on every supplied string.
|
|
78
|
+
ASKED: ${{ inputs.version }}
|
|
79
|
+
run: |
|
|
80
|
+
VERSION="$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)"
|
|
81
|
+
PACKAGE="$(grep -m1 '^name' pyproject.toml | cut -d'"' -f2)"
|
|
82
|
+
if ! printf '%s' "$ASKED" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-z0-9.-]*)$'; then
|
|
83
|
+
echo "FAIL: '$ASKED' is not a version"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
# Typed by the person releasing, so a slip names a version that is not here
|
|
87
|
+
# rather than shipping whatever happens to be on main.
|
|
88
|
+
if [ "$ASKED" != "$VERSION" ]; then
|
|
89
|
+
echo "FAIL: asked to publish ${ASKED}, but this tree is ${VERSION}"
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
echo "${PACKAGE} ${VERSION}"
|
|
93
|
+
# Only a 404 means "not published yet"; unreachable or 5xx is not a yes.
|
|
94
|
+
CODE="$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json")"
|
|
95
|
+
# Read with the tag below, not alone. This refused on PyPI 200 full
|
|
96
|
+
# stop, so a failure in verify or tag -- both of which run after the
|
|
97
|
+
# upload -- left the version on PyPI, immutable and untagged, with no
|
|
98
|
+
# recovery but abandoning it. Three releases went that way across the
|
|
99
|
+
# fleet before 2026-10-27.
|
|
100
|
+
case "$CODE" in
|
|
101
|
+
404|200) : ;;
|
|
102
|
+
*) echo "FAIL: PyPI returned ${CODE}; cannot tell whether ${VERSION} is published"; exit 1 ;;
|
|
103
|
+
esac
|
|
104
|
+
# git ls-remote --exit-code: 2 means no such tag; anything else (0, 128) stops.
|
|
105
|
+
set +e
|
|
106
|
+
git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" > /dev/null
|
|
107
|
+
RC=$?
|
|
108
|
+
set -e
|
|
109
|
+
case "$RC" in
|
|
110
|
+
0) TAGGED=yes ;;
|
|
111
|
+
2) TAGGED=no ;;
|
|
112
|
+
*) echo "FAIL: could not check tags (git ls-remote exited ${RC})"; exit 1 ;;
|
|
113
|
+
esac
|
|
114
|
+
|
|
115
|
+
RESUME=false
|
|
116
|
+
if [ "$CODE" = 404 ] && [ "$TAGGED" = no ]; then
|
|
117
|
+
echo "PyPI: ${PACKAGE} ${VERSION} is new, tag v${VERSION} is free"
|
|
118
|
+
elif [ "$CODE" = 200 ] && [ "$TAGGED" = no ]; then
|
|
119
|
+
RESUME=true
|
|
120
|
+
echo "NOTE: ${PACKAGE} ${VERSION} is already on PyPI with no tag."
|
|
121
|
+
echo " A previous run uploaded it and did not finish. Resuming:"
|
|
122
|
+
echo " the upload is skipped, verify and tag complete the release."
|
|
123
|
+
elif [ "$CODE" = 200 ] && [ "$TAGGED" = yes ]; then
|
|
124
|
+
echo "FAIL: ${PACKAGE} ${VERSION} is on PyPI and tagged — already released."
|
|
125
|
+
exit 1
|
|
126
|
+
else
|
|
127
|
+
echo "FAIL: tag v${VERSION} exists but PyPI has no ${VERSION}."
|
|
128
|
+
echo " Not a state this workflow creates; stopping rather than guessing."
|
|
129
|
+
exit 1
|
|
130
|
+
fi
|
|
131
|
+
echo "resume=${RESUME}" >> "$GITHUB_OUTPUT"
|
|
132
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
133
|
+
echo "package=${PACKAGE}" >> "$GITHUB_OUTPUT"
|
|
134
|
+
|
|
135
|
+
- uses: actions/setup-python@v6
|
|
136
|
+
with:
|
|
137
|
+
python-version: "3.12.16"
|
|
138
|
+
|
|
139
|
+
# pip loads its own vendored certifi as well as the system store, with no
|
|
140
|
+
# way to turn that off, so a current certifi in uv.lock does nothing for
|
|
141
|
+
# pip itself. 26.2.2 is the release past the eMudhra advisory.
|
|
142
|
+
- name: Current pip
|
|
143
|
+
run: |
|
|
144
|
+
python -m pip install --upgrade "pip>=26.2.2"
|
|
145
|
+
python -m pip --version
|
|
146
|
+
|
|
147
|
+
- name: Install test dependencies
|
|
148
|
+
run: pip install pytest pip-audit
|
|
149
|
+
|
|
150
|
+
- name: Run security test suite
|
|
151
|
+
run: python -m pytest tests/test_security.py -v
|
|
152
|
+
|
|
153
|
+
- name: Audit dependencies for vulnerabilities
|
|
154
|
+
# Audits this package's declared dependencies (it has none today), and can fail: a known CVE stops the release.
|
|
155
|
+
# A finding with no fix yet is excepted only explicitly: --ignore-vuln ID, with a reason and date.
|
|
156
|
+
run: pip-audit --strict --desc on .
|
|
157
|
+
|
|
158
|
+
- name: Grep check — no subprocess/shell
|
|
159
|
+
run: |
|
|
160
|
+
if grep -rn 'subprocess\.\|os\.system\|shell=True' src/; then
|
|
161
|
+
echo "FAIL: subprocess/shell found in source"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
- name: Grep check — no plaintext http://
|
|
166
|
+
run: |
|
|
167
|
+
# Tests the output rather than the pipeline's exit status, because the output is
|
|
168
|
+
# what this gate is actually asking about: are there any plaintext URLs. The old
|
|
169
|
+
# form here printed nothing when it failed, and answered through whichever grep
|
|
170
|
+
# was on PATH.
|
|
171
|
+
#
|
|
172
|
+
# This fix landed on `develop` as #16 and never reached `main`, which is where the
|
|
173
|
+
# release gate runs -- so every release since went out through the old check. That
|
|
174
|
+
# is the "fixed on develop is not fixed" case in .claude/rules/security.md, and
|
|
175
|
+
# retiring develop is what finally carries it across.
|
|
176
|
+
hits="$(grep -rn 'http://' src/ | grep -v 'localhost\|127\.0\.0\.1\|::1' || true)"
|
|
177
|
+
if [ -n "$hits" ]; then
|
|
178
|
+
echo "$hits"
|
|
179
|
+
echo "FAIL: non-HTTPS URL found in source"
|
|
180
|
+
exit 1
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
- name: Grep check — no credential logging
|
|
184
|
+
run: |
|
|
185
|
+
if grep -rn -i 'print.*password\|print.*api_key\|logging.*password\|logging.*api_key' src/; then
|
|
186
|
+
echo "FAIL: credential logging found in source"
|
|
187
|
+
exit 1
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
publish:
|
|
191
|
+
name: Build and publish to PyPI
|
|
192
|
+
needs: security
|
|
193
|
+
runs-on: ubuntu-latest
|
|
194
|
+
environment: pypi
|
|
195
|
+
permissions:
|
|
196
|
+
id-token: write # Required for trusted publishing (OIDC)
|
|
197
|
+
steps:
|
|
198
|
+
- uses: actions/checkout@v5
|
|
199
|
+
|
|
200
|
+
- uses: actions/setup-python@v6
|
|
201
|
+
with:
|
|
202
|
+
python-version: "3.12.16"
|
|
203
|
+
|
|
204
|
+
- name: Install build dependencies
|
|
205
|
+
if: needs.security.outputs.resume != 'true'
|
|
206
|
+
run: |
|
|
207
|
+
python -m pip install --upgrade "pip>=26.2.2"
|
|
208
|
+
python -m pip --version
|
|
209
|
+
pip install build
|
|
210
|
+
|
|
211
|
+
- name: Build package
|
|
212
|
+
if: needs.security.outputs.resume != 'true'
|
|
213
|
+
run: python -m build
|
|
214
|
+
|
|
215
|
+
- name: Publish to PyPI
|
|
216
|
+
if: needs.security.outputs.resume != 'true'
|
|
217
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
218
|
+
|
|
219
|
+
- name: Already on PyPI — resuming
|
|
220
|
+
if: needs.security.outputs.resume == 'true'
|
|
221
|
+
run: |
|
|
222
|
+
echo "skipped the upload: this version is already on PyPI"
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Done when PyPI serves the version (verify); then tag the released commit. A workflow's
|
|
226
|
+
# own tag push triggers nothing. If tagging fails after PyPI serves it, the
|
|
227
|
+
# release IS published: re-running the job is safe.
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
verify:
|
|
230
|
+
name: Verify PyPI
|
|
231
|
+
needs: [security, publish]
|
|
232
|
+
runs-on: ubuntu-latest
|
|
233
|
+
steps:
|
|
234
|
+
- name: PyPI serves the released version
|
|
235
|
+
run: |
|
|
236
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
237
|
+
PACKAGE="${{ needs.security.outputs.package }}"
|
|
238
|
+
for i in $(seq 1 40); do
|
|
239
|
+
if [ "$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json")" = "200" ]; then
|
|
240
|
+
echo "PyPI serves ${PACKAGE} ${VERSION}"
|
|
241
|
+
exit 0
|
|
242
|
+
fi
|
|
243
|
+
sleep 15
|
|
244
|
+
done
|
|
245
|
+
echo "FAIL: PyPI never served ${PACKAGE} ${VERSION}"
|
|
246
|
+
exit 1
|
|
247
|
+
|
|
248
|
+
tag:
|
|
249
|
+
name: Tag the release
|
|
250
|
+
needs: [security, verify]
|
|
251
|
+
runs-on: ubuntu-latest
|
|
252
|
+
permissions:
|
|
253
|
+
contents: write
|
|
254
|
+
steps:
|
|
255
|
+
- uses: actions/checkout@v5
|
|
256
|
+
|
|
257
|
+
- name: Create and push vX.Y.Z
|
|
258
|
+
run: |
|
|
259
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
260
|
+
git config user.name "github-actions[bot]"
|
|
261
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
262
|
+
git tag -a "v${VERSION}" -m "${GITHUB_REPOSITORY#*/} ${VERSION}" "$GITHUB_SHA"
|
|
263
|
+
git push origin "v${VERSION}"
|
|
264
|
+
|
|
265
|
+
# The tag alone left every Releases page in this org empty. The body is
|
|
266
|
+
# this version's own CHANGELOG section, extracted by heading rather than
|
|
267
|
+
# by line count, because the fleet has three heading styles:
|
|
268
|
+
# ## [0.4.0] — date ## [2.0.0] - date ## 0.2.0 (date)
|
|
269
|
+
# It stops at the next "## " heading, and returns nothing for a version
|
|
270
|
+
# it cannot find -- so a missing section falls back to GitHub's own
|
|
271
|
+
# generated notes rather than publishing the whole changelog.
|
|
272
|
+
#
|
|
273
|
+
# After `verify` by construction: a Release only ever describes a version
|
|
274
|
+
# already confirmed on PyPI.
|
|
275
|
+
- name: Create the GitHub Release
|
|
276
|
+
env:
|
|
277
|
+
GH_TOKEN: ${{ github.token }}
|
|
278
|
+
run: |
|
|
279
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
280
|
+
python3 - "$VERSION" CHANGELOG.md > /tmp/notes.md <<'EXTRACT'
|
|
281
|
+
import re, sys, pathlib
|
|
282
|
+
ver, path = sys.argv[1], sys.argv[2]
|
|
283
|
+
text = pathlib.Path(path).read_text()
|
|
284
|
+
start = re.search(r'(?m)^##\s+\[?%s\]?\b.*$' % re.escape(ver), text)
|
|
285
|
+
if not start:
|
|
286
|
+
raise SystemExit(0)
|
|
287
|
+
rest = text[start.end():]
|
|
288
|
+
nxt = re.search(r'(?m)^##\s', rest)
|
|
289
|
+
print((rest[:nxt.start()] if nxt else rest).strip())
|
|
290
|
+
EXTRACT
|
|
291
|
+
|
|
292
|
+
# A version with a suffix (0.2.1rc1) is a pre-release, not Latest.
|
|
293
|
+
PRE=""
|
|
294
|
+
case "$VERSION" in
|
|
295
|
+
*[!0-9.]*) PRE="--prerelease" ;;
|
|
296
|
+
esac
|
|
297
|
+
|
|
298
|
+
if [ -s /tmp/notes.md ]; then
|
|
299
|
+
echo "release notes: this version's CHANGELOG section ($(wc -l < /tmp/notes.md) lines)"
|
|
300
|
+
gh release create "v${VERSION}" --verify-tag --title "v${VERSION}" \
|
|
301
|
+
--notes-file /tmp/notes.md $PRE
|
|
302
|
+
else
|
|
303
|
+
echo "release notes: no CHANGELOG section for ${VERSION}; using generated notes"
|
|
304
|
+
gh release create "v${VERSION}" --verify-tag --title "v${VERSION}" \
|
|
305
|
+
--generate-notes $PRE
|
|
306
|
+
fi
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `qso-graph-lib` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`qso_graph_lib.jt`**: WSJT-X's UDP protocol for the JT family (`protocol`), encoded and decoded
|
|
15
|
+
to `NetworkMessage.hpp`, schema 3, and WSJT-X's `ALL.TXT` reader (`alltxt`). Moved from
|
|
16
|
+
qso-graph-sim, so it and jt-mcp share one implementation.
|