Skip to content

Commit 4960f4e

Browse files
committed
ci: harden workflow gates release checks and version consistency
1 parent 27acef5 commit 4960f4e

15 files changed

Lines changed: 533 additions & 34 deletions

.github/workflows/build-wheels.yml

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ jobs:
6262
/tmp/test_venv/bin/python -c "import dmPython; print('dmPython version:', dmPython.version)"
6363
6464
- name: Build extension for optional integration tests
65-
if: ${{ secrets.DM_TEST_HOST != '' && secrets.DM_TEST_PORT != '' && secrets.DM_TEST_USER != '' && secrets.DM_TEST_PASSWORD != '' }}
65+
if: ${{ matrix.python-version == '3.10' && secrets.DM_TEST_HOST != '' && secrets.DM_TEST_PORT != '' && secrets.DM_TEST_USER != '' && secrets.DM_TEST_PASSWORD != '' }}
6666
run: DMPYTHON_SKIP_GO_BUILD=1 python setup.py build_ext --inplace
6767

6868
- name: Run optional DM integration tests
69-
if: ${{ secrets.DM_TEST_HOST != '' && secrets.DM_TEST_PORT != '' && secrets.DM_TEST_USER != '' && secrets.DM_TEST_PASSWORD != '' }}
69+
if: ${{ matrix.python-version == '3.10' && secrets.DM_TEST_HOST != '' && secrets.DM_TEST_PORT != '' && secrets.DM_TEST_USER != '' && secrets.DM_TEST_PASSWORD != '' }}
7070
env:
7171
DYLD_LIBRARY_PATH: ${{ github.workspace }}/dpi_bridge
7272
DM_TEST_HOST: ${{ secrets.DM_TEST_HOST }}
@@ -83,15 +83,16 @@ jobs:
8383
python -m pytest -q -m requires_dm tests --cov=. --cov-report=term-missing --cov-report=xml:coverage.xml
8484
8585
- name: Upload optional integration coverage
86-
if: ${{ secrets.DM_TEST_HOST != '' && secrets.DM_TEST_PORT != '' && secrets.DM_TEST_USER != '' && secrets.DM_TEST_PASSWORD != '' }}
86+
if: ${{ matrix.python-version == '3.10' && secrets.DM_TEST_HOST != '' && secrets.DM_TEST_PORT != '' && secrets.DM_TEST_USER != '' && secrets.DM_TEST_PASSWORD != '' }}
8787
uses: actions/upload-artifact@v4
8888
with:
89-
name: coverage-wheel-py${{ matrix.python-version }}
89+
name: coverage-wheel-baseline-py${{ matrix.python-version }}
9090
path: coverage.xml
9191

9292
- name: Skip optional integration tests (missing DM secrets)
93-
if: ${{ secrets.DM_TEST_HOST == '' || secrets.DM_TEST_PORT == '' || secrets.DM_TEST_USER == '' || secrets.DM_TEST_PASSWORD == '' }}
94-
run: echo "Skipping requires_dm tests: DM_TEST_HOST/PORT/USER/PASSWORD secrets not fully configured."
93+
if: ${{ matrix.python-version == '3.10' && (secrets.DM_TEST_HOST == '' || secrets.DM_TEST_PORT == '' || secrets.DM_TEST_USER == '' || secrets.DM_TEST_PASSWORD == '') }}
94+
run: |
95+
echo "::notice::SKIP_DM_INTEGRATION: DM_TEST_HOST/PORT/USER/PASSWORD secrets are not fully configured."
9596
9697
- uses: actions/upload-artifact@v4
9798
with:
@@ -114,15 +115,103 @@ jobs:
114115
pattern: wheel-*
115116
merge-multiple: true
116117

118+
- name: Validate wheel set and generate release metadata
119+
env:
120+
TAG_NAME: ${{ github.ref_name }}
121+
run: |
122+
python - <<'PY'
123+
import glob
124+
import json
125+
import os
126+
import re
127+
import subprocess
128+
import sys
129+
130+
wheels = sorted(glob.glob("dist_fixed/*.whl"))
131+
if len(wheels) != 5:
132+
print(f"Expected 5 wheels, got {len(wheels)}")
133+
print("\n".join(wheels))
134+
sys.exit(1)
135+
136+
expected_tags = {"cp39-cp39", "cp310-cp310", "cp311-cp311", "cp312-cp312", "cp313-cp313"}
137+
found_tags = set()
138+
for wheel in wheels:
139+
name = os.path.basename(wheel)
140+
if "arm64.whl" not in name:
141+
print(f"Non-arm64 wheel found: {name}")
142+
sys.exit(1)
143+
match = re.search(r"-(cp\d{2,3}-cp\d{2,3})-macosx_.*_arm64\.whl$", name)
144+
if not match:
145+
print(f"Unrecognized wheel naming pattern: {name}")
146+
sys.exit(1)
147+
found_tags.add(match.group(1))
148+
if found_tags != expected_tags:
149+
print(f"Wheel tag mismatch. expected={sorted(expected_tags)} found={sorted(found_tags)}")
150+
sys.exit(1)
151+
152+
with open("dist_fixed/build-metadata.json", "w", encoding="utf-8") as f:
153+
json.dump(
154+
{
155+
"tag": os.environ["TAG_NAME"],
156+
"commit": os.environ.get("GITHUB_SHA", ""),
157+
"workflow": os.environ.get("GITHUB_WORKFLOW", ""),
158+
"run_id": os.environ.get("GITHUB_RUN_ID", ""),
159+
"run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT", ""),
160+
"wheels": [os.path.basename(w) for w in wheels],
161+
},
162+
f,
163+
ensure_ascii=False,
164+
indent=2,
165+
)
166+
167+
subprocess.check_call("shasum -a 256 dist_fixed/*.whl > dist_fixed/checksums.txt", shell=True)
168+
PY
169+
117170
- name: Upsert GitHub Release
118171
env:
119172
GH_TOKEN: ${{ github.token }}
120173
TAG_NAME: ${{ github.ref_name }}
121174
run: |
122175
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
123176
echo "Release $TAG_NAME already exists, uploading artifacts with --clobber"
124-
gh release upload "$TAG_NAME" dist_fixed/*.whl --clobber
177+
gh release upload "$TAG_NAME" dist_fixed/*.whl dist_fixed/checksums.txt dist_fixed/build-metadata.json --clobber
125178
else
126179
echo "Creating release $TAG_NAME"
127-
gh release create "$TAG_NAME" --generate-notes dist_fixed/*.whl
180+
gh release create "$TAG_NAME" --generate-notes dist_fixed/*.whl dist_fixed/checksums.txt dist_fixed/build-metadata.json
128181
fi
182+
183+
- name: Verify release assets completeness
184+
env:
185+
GH_TOKEN: ${{ github.token }}
186+
TAG_NAME: ${{ github.ref_name }}
187+
run: |
188+
python - <<'PY'
189+
import json
190+
import os
191+
import re
192+
import subprocess
193+
import sys
194+
195+
tag = os.environ["TAG_NAME"]
196+
raw = subprocess.check_output(
197+
["gh", "release", "view", tag, "--json", "assets"],
198+
text=True,
199+
)
200+
assets = [a["name"] for a in json.loads(raw)["assets"]]
201+
expected_tags = {"cp39-cp39", "cp310-cp310", "cp311-cp311", "cp312-cp312", "cp313-cp313"}
202+
found_tags = set()
203+
for name in assets:
204+
m = re.search(r"-(cp\d{2,3}-cp\d{2,3})-macosx_.*_arm64\.whl$", name)
205+
if m:
206+
found_tags.add(m.group(1))
207+
missing_tags = sorted(expected_tags - found_tags)
208+
required_files = {"checksums.txt", "build-metadata.json"}
209+
missing_files = sorted(f for f in required_files if f not in assets)
210+
if missing_tags or missing_files:
211+
print("Release assets are incomplete.")
212+
print(f"missing wheel tags: {missing_tags}")
213+
print(f"missing files: {missing_files}")
214+
print(f"assets: {assets}")
215+
sys.exit(1)
216+
print("Release assets verified:", assets)
217+
PY

.github/workflows/integration-tests.yml

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,21 @@ jobs:
5858
run: |
5959
python -m pip install pytest pytest-timeout pytest-cov
6060
python setup.py build_ext --inplace
61-
python -m pytest -q tests/integration -m "p0_stability or p1_contract" --cov=. --cov-report=term-missing --cov-report=xml:coverage.xml
61+
python -m pytest -q tests/integration -m "p0_stability or p1_contract" --junitxml=pytest-pr.xml --cov=. --cov-report=term-missing --cov-report=xml:coverage.xml
6262
6363
- name: Upload coverage report (PR)
6464
if: steps.dm_ready.outputs.available == 'true'
6565
uses: actions/upload-artifact@v4
6666
with:
6767
name: coverage-pr
68-
path: coverage.xml
68+
path: |
69+
coverage.xml
70+
pytest-pr.xml
6971
7072
- name: Skip lightweight integration tests
7173
if: steps.dm_ready.outputs.available != 'true'
72-
run: echo "Skipping PR integration tests: DM secrets are not configured."
74+
run: |
75+
echo "::notice::SKIP_DM_INTEGRATION: PR integration tests are skipped because DM secrets are not configured."
7376
7477
full-regression:
7578
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
@@ -125,15 +128,40 @@ jobs:
125128
run: |
126129
python -m pip install pytest pytest-timeout pytest-cov
127130
python setup.py build_ext --inplace
128-
python -m pytest -q tests/integration -m "p0_stability or p1_contract or p2_scale" --cov=. --cov-report=term-missing --cov-report=xml:coverage.xml
131+
start_ts=$(date +%s)
132+
python -m pytest -q tests/integration -m "p0_stability or p1_contract or p2_scale" --junitxml=pytest-full.xml --cov=. --cov-report=term-missing --cov-report=xml:coverage.xml
133+
end_ts=$(date +%s)
134+
duration=$((end_ts - start_ts))
135+
python - <<PY
136+
import json
137+
import xml.etree.ElementTree as ET
138+
139+
root = ET.parse("pytest-full.xml").getroot()
140+
attrs = root.attrib
141+
trend = {
142+
"tests": int(float(attrs.get("tests", "0"))),
143+
"failures": int(float(attrs.get("failures", "0"))),
144+
"errors": int(float(attrs.get("errors", "0"))),
145+
"skipped": int(float(attrs.get("skipped", "0"))),
146+
"pytest_reported_seconds": float(attrs.get("time", "0")),
147+
"wall_clock_seconds": int(${duration}),
148+
}
149+
with open("trend-full-regression.json", "w", encoding="utf-8") as f:
150+
json.dump(trend, f, indent=2, ensure_ascii=False)
151+
print(trend)
152+
PY
129153
130154
- name: Upload coverage report (full)
131155
if: steps.dm_ready.outputs.available == 'true'
132156
uses: actions/upload-artifact@v4
133157
with:
134158
name: coverage-full
135-
path: coverage.xml
159+
path: |
160+
coverage.xml
161+
pytest-full.xml
162+
trend-full-regression.json
136163
137164
- name: Skip full integration tests
138165
if: steps.dm_ready.outputs.available != 'true'
139-
run: echo "Skipping full integration tests: DM secrets are not configured."
166+
run: |
167+
echo "::notice::SKIP_DM_INTEGRATION: Full regression is skipped because DM secrets are not configured."
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Workflow Lint
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.11"
18+
19+
- name: Install lint dependencies
20+
run: |
21+
python -m pip install --upgrade pip
22+
python -m pip install pyyaml build twine
23+
24+
- name: Parse workflow YAML files
25+
run: |
26+
python scripts/check_workflow_yaml.py
27+
28+
- name: Run actionlint
29+
uses: rhysd/actionlint@v1
30+
31+
- name: Check version consistency
32+
run: |
33+
python scripts/check_version_consistency.py
34+
35+
- name: Check third-party patch consistency
36+
run: |
37+
python scripts/check_third_party_patch.py
38+
39+
- name: Packaging metadata check (sdist)
40+
run: |
41+
python -m build --sdist --outdir /tmp/dist_meta
42+
python -m twine check /tmp/dist_meta/*

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: go-bridge wheel wheel-delocated universal2 clean test-install dpi-headers-secret
1+
.PHONY: go-bridge wheel wheel-delocated universal2 clean test-install dpi-headers-secret release-preflight
22

33
# Go bridge library
44
GO_BRIDGE_DIR = dpi_bridge
@@ -47,6 +47,10 @@ test-install:
4747
@echo "Test passed! Cleaning up..."
4848
rm -rf /tmp/dmpython_test_venv
4949

50+
# Release preflight checks (optional: make release-preflight TAG=v2.5.31)
51+
release-preflight:
52+
./scripts/release_preflight.sh $(TAG)
53+
5054
# Package DPI headers as base64 for GitHub Secret
5155
dpi-headers-secret:
5256
@tar czf - -C dpi_include . | base64 | pbcopy

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This is a community fork of the [official dmPython](https://github.com/DamengDB/
1414
Download a pre-built wheel from [GitHub Releases](https://github.com/skhe/dmPython/releases):
1515

1616
```bash
17-
pip install dmPython_macOS-2.5.30-cp312-cp312-macosx_14_0_arm64.whl
17+
pip install dmPython_macOS-2.5.31-cp312-cp312-macosx_14_0_arm64.whl
1818
```
1919

2020
## Quick Start

docs/README_zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ dmPython 是达梦数据库(DM8)的原生 Python 驱动程序,遵循 [Pyth
2525
[GitHub Releases](https://github.com/skhe/dmPython/releases) 下载预编译 wheel:
2626

2727
```bash
28-
pip install dmPython_macOS-2.5.30-cp312-cp312-macosx_14_0_arm64.whl
28+
pip install dmPython_macOS-2.5.31-cp312-cp312-macosx_14_0_arm64.whl
2929
```
3030

3131
## 快速开始

docs/release-checklist.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Release Checklist
2+
3+
## 1. Preflight
4+
- [ ] Run `./scripts/release_preflight.sh vX.Y.Z`
5+
- [ ] Confirm workflow lint and actionlint checks are green
6+
- [ ] Confirm version consistency (`pyproject.toml`, `setup.py`, `src/native/py_Dameng.h`, `dmPython.version`)
7+
- [ ] Confirm third-party patch checks pass (`scripts/check_third_party_patch.py`)
8+
9+
## 2. Regression
10+
- [ ] Run `DYLD_LIBRARY_PATH=/Users/skhe/projects/dmPython/dpi_bridge python3 -m pytest -q -m requires_dm tests`
11+
- [ ] Confirm P0/P1/P2 integration markers are green
12+
- [ ] Confirm no `Segmentation fault` / no `139/-11` exits
13+
14+
## 3. Tag & CI
15+
- [ ] Push release tag `vX.Y.Z`
16+
- [ ] Confirm `Build macOS wheels` workflow succeeds for all Python targets (`cp39/cp310/cp311/cp312/cp313`)
17+
- [ ] Confirm release step is idempotent (re-run does not fail)
18+
19+
## 4. Release Assets
20+
- [ ] Confirm 5 arm64 wheel assets exist on release page
21+
- [ ] Confirm `checksums.txt` is attached
22+
- [ ] Confirm `build-metadata.json` is attached
23+
- [ ] Spot-check one wheel install and `import dmPython`
24+
25+
## 5. Post-release
26+
- [ ] Update `CHANGELOG.md` if needed
27+
- [ ] Verify release notes and links are correct
28+
- [ ] Record any incidents/fixes back into `PATCHES.md` or docs

scripts/check_third_party_patch.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env python3
2+
"""Validate local patched DM driver contract and patch docs."""
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
9+
ROOT = Path(__file__).resolve().parents[1]
10+
GO_MOD = ROOT / "dpi_bridge/go.mod"
11+
PATCH_DOC = ROOT / "dpi_bridge/third_party/chunanyong_dm/PATCHES.md"
12+
PATCH_FILE = ROOT / "dpi_bridge/third_party/chunanyong_dm/a.go"
13+
14+
15+
def fail(msg: str) -> None:
16+
raise RuntimeError(msg)
17+
18+
19+
def require_contains(path: Path, needle: str) -> None:
20+
text = path.read_text(encoding="utf-8")
21+
if needle not in text:
22+
fail(f"{path} missing expected content: {needle}")
23+
24+
25+
def main() -> int:
26+
if not GO_MOD.exists():
27+
fail(f"missing {GO_MOD}")
28+
if not PATCH_DOC.exists():
29+
fail(f"missing {PATCH_DOC}")
30+
if not PATCH_FILE.exists():
31+
fail(f"missing {PATCH_FILE}")
32+
33+
require_contains(GO_MOD, "replace gitee.com/chunanyong/dm => ./third_party/chunanyong_dm")
34+
35+
# Patch documentation should describe the actual patch point and its tests.
36+
require_contains(PATCH_DOC, "File: `a.go`")
37+
require_contains(PATCH_DOC, "Function: `dm_build_610`")
38+
require_contains(PATCH_DOC, "test_clob_unicode_problem_patterns_roundtrip")
39+
require_contains(PATCH_DOC, "test_clob_unicode_problem_patterns_subprocess_no_crash")
40+
41+
# Patch implementation invariants for unicode chunk-boundary fix.
42+
require_contains(PATCH_FILE, "dm_build_610(")
43+
require_contains(PATCH_FILE, "var dm_build_615 bytes.Buffer")
44+
require_contains(PATCH_FILE, "Avoid splitting a UTF-8 sequence at chunk boundaries.")
45+
46+
print("[OK] third-party patch consistency checks passed")
47+
return 0
48+
49+
50+
if __name__ == "__main__":
51+
try:
52+
raise SystemExit(main())
53+
except RuntimeError as exc:
54+
print(f"[FAIL] {exc}")
55+
raise SystemExit(2)

0 commit comments

Comments
 (0)