openvisionkit 0.4.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.
Files changed (61) hide show
  1. openvisionkit-0.4.0/.flake8 +5 -0
  2. openvisionkit-0.4.0/.gitattributes +3 -0
  3. openvisionkit-0.4.0/.github/workflows/ci-integration.yml +86 -0
  4. openvisionkit-0.4.0/.github/workflows/ci-security.yml +125 -0
  5. openvisionkit-0.4.0/.github/workflows/ci-unit.yml +57 -0
  6. openvisionkit-0.4.0/.github/workflows/publish.yml +135 -0
  7. openvisionkit-0.4.0/.github/workflows/renovate.yml +37 -0
  8. openvisionkit-0.4.0/.github/workflows/reusable-github-release.yml +193 -0
  9. openvisionkit-0.4.0/.github/workflows/semantic-versioning.yml +218 -0
  10. openvisionkit-0.4.0/.gitignore +13 -0
  11. openvisionkit-0.4.0/.pre-commit-config.yaml +101 -0
  12. openvisionkit-0.4.0/.python-version +1 -0
  13. openvisionkit-0.4.0/CLAUDE.md +528 -0
  14. openvisionkit-0.4.0/Makefile +109 -0
  15. openvisionkit-0.4.0/PKG-INFO +1018 -0
  16. openvisionkit-0.4.0/README.md +993 -0
  17. openvisionkit-0.4.0/docs/superpowers/plans/2026-06-09-visionkit-utility-methods.md +2633 -0
  18. openvisionkit-0.4.0/docs/superpowers/specs/2026-06-09-visionkit-utility-methods-design.md +229 -0
  19. openvisionkit-0.4.0/main.py +6 -0
  20. openvisionkit-0.4.0/openvisionkit/__init__.py +1 -0
  21. openvisionkit-0.4.0/openvisionkit/_version.py +24 -0
  22. openvisionkit-0.4.0/openvisionkit/capture/draw_object.py +296 -0
  23. openvisionkit-0.4.0/openvisionkit/capture/image_template.py +61 -0
  24. openvisionkit-0.4.0/openvisionkit/capture/screen_capture.py +13 -0
  25. openvisionkit-0.4.0/openvisionkit/capture/video_recorder.py +128 -0
  26. openvisionkit-0.4.0/openvisionkit/capture/video_template.py +336 -0
  27. openvisionkit-0.4.0/openvisionkit/lib/classifier.py +186 -0
  28. openvisionkit-0.4.0/openvisionkit/lib/face_detector.py +587 -0
  29. openvisionkit-0.4.0/openvisionkit/lib/face_mesh_detector.py +913 -0
  30. openvisionkit-0.4.0/openvisionkit/lib/form_detector.py +465 -0
  31. openvisionkit-0.4.0/openvisionkit/lib/form_roi_annotator.py +679 -0
  32. openvisionkit-0.4.0/openvisionkit/lib/form_roi_detector.py +1078 -0
  33. openvisionkit-0.4.0/openvisionkit/lib/fps_counter.py +38 -0
  34. openvisionkit-0.4.0/openvisionkit/lib/hair_segmentation.py +298 -0
  35. openvisionkit-0.4.0/openvisionkit/lib/hand_detector.py +1230 -0
  36. openvisionkit-0.4.0/openvisionkit/lib/image_detector.py +1095 -0
  37. openvisionkit-0.4.0/openvisionkit/lib/object_detector.py +401 -0
  38. openvisionkit-0.4.0/openvisionkit/lib/pose_detector.py +919 -0
  39. openvisionkit-0.4.0/openvisionkit/lib/selfie_segmentation.py +528 -0
  40. openvisionkit-0.4.0/openvisionkit/lib/text_detector.py +1229 -0
  41. openvisionkit-0.4.0/openvisionkit/utility/live_plot.py +141 -0
  42. openvisionkit-0.4.0/openvisionkit/utility/vision_utilis.py +871 -0
  43. openvisionkit-0.4.0/pyproject.toml +103 -0
  44. openvisionkit-0.4.0/renovate.json +79 -0
  45. openvisionkit-0.4.0/tests/__init__.py +0 -0
  46. openvisionkit-0.4.0/tests/capture/__init__.py +0 -0
  47. openvisionkit-0.4.0/tests/capture/test_video_capture_template.py +560 -0
  48. openvisionkit-0.4.0/tests/conftest.py +96 -0
  49. openvisionkit-0.4.0/tests/lib/__init__.py +0 -0
  50. openvisionkit-0.4.0/tests/lib/test_face_detector.py +116 -0
  51. openvisionkit-0.4.0/tests/lib/test_face_mesh_detector.py +120 -0
  52. openvisionkit-0.4.0/tests/lib/test_form_roi_annotator.py +70 -0
  53. openvisionkit-0.4.0/tests/lib/test_form_roi_detector.py +98 -0
  54. openvisionkit-0.4.0/tests/lib/test_hair_segmentation.py +77 -0
  55. openvisionkit-0.4.0/tests/lib/test_hand_detector.py +138 -0
  56. openvisionkit-0.4.0/tests/lib/test_image_detector.py +82 -0
  57. openvisionkit-0.4.0/tests/lib/test_object_detector.py +85 -0
  58. openvisionkit-0.4.0/tests/lib/test_pose_detector.py +134 -0
  59. openvisionkit-0.4.0/tests/lib/test_selfie_segmentation.py +91 -0
  60. openvisionkit-0.4.0/tests/lib/test_text_detector.py +94 -0
  61. openvisionkit-0.4.0/uv.lock +2790 -0
@@ -0,0 +1,5 @@
1
+ [flake8]
2
+ max-line-length = 88
3
+ extend-ignore = E203
4
+ per-file-ignores =
5
+ tests/*:E402
@@ -0,0 +1,3 @@
1
+ *.tflite filter=lfs diff=lfs merge=lfs -text
2
+ *.task filter=lfs diff=lfs merge=lfs -text
3
+ *.pb filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1,86 @@
1
+ name: Integration Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+ workflow_dispatch:
9
+ inputs:
10
+ skip_model_download:
11
+ description: "Skip model download (run inference-free integration tests only)"
12
+ required: false
13
+ default: "false"
14
+ type: boolean
15
+
16
+ concurrency:
17
+ group: integration-${{ github.ref }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ integration:
22
+ name: pytest integration
23
+ runs-on: ubuntu-latest
24
+
25
+ steps:
26
+ - name: Checkout
27
+ uses: actions/checkout@v4
28
+
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v5
31
+ with:
32
+ enable-cache: true
33
+ cache-dependency-glob: "uv.lock"
34
+
35
+ - name: Set up Python 3.11
36
+ uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.11"
39
+
40
+ - name: Install system dependencies (Tesseract)
41
+ run: |
42
+ sudo apt-get update -qq
43
+ sudo apt-get install -y tesseract-ocr tesseract-ocr-eng
44
+
45
+ - name: Install project dependencies
46
+ run: uv sync --all-groups
47
+
48
+ # ── Model files ──────────────────────────────────────────────────────
49
+ # Models are NOT committed to git (binary, large, user-provided).
50
+ # Store each .tflite/.task file as a GitHub Actions secret (base64)
51
+ # or upload them to a private artifact store and cache them here.
52
+ #
53
+ # Example: add secret FACE_DETECTION_MODEL_B64 = base64-encoded .tflite
54
+ # and uncomment the block below.
55
+ #
56
+ # - name: Restore model files
57
+ # env:
58
+ # FACE_DETECTION_MODEL_B64: ${{ secrets.FACE_DETECTION_MODEL_B64 }}
59
+ # run: |
60
+ # mkdir -p models
61
+ # echo "$FACE_DETECTION_MODEL_B64" | base64 -d > models/face_detection.tflite
62
+
63
+ - name: Cache model files
64
+ id: cache-models
65
+ uses: actions/cache@v4
66
+ with:
67
+ path: models/
68
+ key: openvisionkit-models-${{ hashFiles('pyproject.toml') }}
69
+
70
+ - name: Run integration tests
71
+ env:
72
+ VISIONKIT_MODELS_DIR: ${{ github.workspace }}/models
73
+ run: |
74
+ uv run pytest tests/ \
75
+ -m "integration" \
76
+ --tb=short \
77
+ -v
78
+
79
+ - name: Upload test results
80
+ if: always()
81
+ uses: actions/upload-artifact@v4
82
+ with:
83
+ name: integration-test-results
84
+ path: |
85
+ .pytest_cache/
86
+ retention-days: 7
@@ -0,0 +1,125 @@
1
+ name: Security Scan
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+ schedule:
9
+ # Run daily at 02:00 UTC to catch newly published CVEs
10
+ - cron: "0 2 * * *"
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+ security-events: write # required to upload SARIF to GitHub Security tab
16
+
17
+ jobs:
18
+ # ── 1. Python dependency audit (pip-audit + OSV database) ─────────────────
19
+ pip-audit:
20
+ name: pip-audit (Python deps)
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - name: Checkout
25
+ uses: actions/checkout@v4
26
+
27
+ - name: Install uv
28
+ uses: astral-sh/setup-uv@v5
29
+ with:
30
+ enable-cache: true
31
+ cache-dependency-glob: "uv.lock"
32
+
33
+ - name: Set up Python 3.11
34
+ uses: actions/setup-python@v5
35
+ with:
36
+ python-version: "3.11"
37
+
38
+ - name: Install project dependencies
39
+ run: uv sync --all-groups
40
+
41
+ - name: Run pip-audit (SBOM)
42
+ run: |
43
+ uv run pip-audit \
44
+ --local \
45
+ --format cyclonedx-json \
46
+ --output pip-audit-sbom.json \
47
+ --progress-spinner off || true
48
+
49
+ - name: Run pip-audit (fail on critical)
50
+ run: |
51
+ uv run pip-audit \
52
+ --local \
53
+ --progress-spinner off
54
+
55
+ - name: Upload SBOM artifact
56
+ if: always()
57
+ uses: actions/upload-artifact@v4
58
+ with:
59
+ name: pip-audit-sbom
60
+ path: pip-audit-sbom.json
61
+ retention-days: 30
62
+
63
+ # ── 2. Trivy filesystem + dependency scan with SARIF upload ───────────────
64
+ trivy:
65
+ name: Trivy (filesystem + SCA)
66
+ runs-on: ubuntu-latest
67
+
68
+ steps:
69
+ - name: Checkout
70
+ uses: actions/checkout@v4
71
+
72
+ - name: Run Trivy vulnerability scan
73
+ uses: aquasecurity/trivy-action@v0.36.0
74
+ with:
75
+ scan-type: fs
76
+ scan-ref: .
77
+ format: sarif
78
+ output: trivy-results.sarif
79
+ severity: CRITICAL,HIGH,MEDIUM
80
+ exit-code: "0" # don't block CI — upload findings first
81
+ ignore-unfixed: true
82
+
83
+ - name: Upload Trivy SARIF to GitHub Security tab
84
+ uses: github/codeql-action/upload-sarif@v3
85
+ if: always()
86
+ with:
87
+ sarif_file: trivy-results.sarif
88
+ category: trivy
89
+
90
+ - name: Fail on CRITICAL vulnerabilities
91
+ uses: aquasecurity/trivy-action@v0.36.0
92
+ with:
93
+ scan-type: fs
94
+ scan-ref: .
95
+ format: table
96
+ severity: CRITICAL
97
+ exit-code: "1"
98
+ ignore-unfixed: true
99
+
100
+ # ── 3. CodeQL static analysis ─────────────────────────────────────────────
101
+ codeql:
102
+ name: CodeQL (static analysis)
103
+ runs-on: ubuntu-latest
104
+ permissions:
105
+ actions: read
106
+ contents: read
107
+ security-events: write
108
+
109
+ steps:
110
+ - name: Checkout
111
+ uses: actions/checkout@v4
112
+
113
+ - name: Initialize CodeQL
114
+ uses: github/codeql-action/init@v3
115
+ with:
116
+ languages: python
117
+ queries: security-and-quality
118
+
119
+ - name: Autobuild
120
+ uses: github/codeql-action/autobuild@v3
121
+
122
+ - name: Perform CodeQL Analysis
123
+ uses: github/codeql-action/analyze@v3
124
+ with:
125
+ category: codeql-python
@@ -0,0 +1,57 @@
1
+ name: Unit Tests
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ concurrency:
10
+ group: unit-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ unit:
15
+ name: pytest unit — Python ${{ matrix.python-version }}
16
+ runs-on: ubuntu-latest
17
+
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ python-version: ["3.11", "3.12"]
22
+
23
+ steps:
24
+ - name: Checkout
25
+ uses: actions/checkout@v4
26
+
27
+ - name: Install uv
28
+ uses: astral-sh/setup-uv@v5
29
+ with:
30
+ enable-cache: true
31
+ cache-dependency-glob: "uv.lock"
32
+
33
+ - name: Set up Python ${{ matrix.python-version }}
34
+ uses: actions/setup-python@v5
35
+ with:
36
+ python-version: ${{ matrix.python-version }}
37
+
38
+ - name: Install dependencies
39
+ run: uv sync --all-groups
40
+
41
+ - name: Run unit tests
42
+ run: |
43
+ uv run pytest tests/ \
44
+ -m "not integration" \
45
+ --cov=openvisionkit \
46
+ --cov-report=xml \
47
+ --cov-report=term-missing \
48
+ -v
49
+
50
+ - name: Upload coverage to Codecov
51
+ if: matrix.python-version == '3.11'
52
+ uses: codecov/codecov-action@v4
53
+ with:
54
+ files: coverage.xml
55
+ fail_ci_if_error: false
56
+ env:
57
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,135 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ inputs:
8
+ target:
9
+ description: "Publish target"
10
+ required: true
11
+ default: testpypi
12
+ type: choice
13
+ options:
14
+ - testpypi
15
+ - pypi
16
+
17
+ permissions:
18
+ contents: read
19
+ id-token: write # required for PyPI Trusted Publishing (OIDC)
20
+
21
+ jobs:
22
+ # ── 1. Unit test gate ─────────────────────────────────────────────────────
23
+ test:
24
+ name: Unit tests (pre-publish gate)
25
+ runs-on: ubuntu-latest
26
+
27
+ steps:
28
+ - name: Checkout
29
+ uses: actions/checkout@v4
30
+
31
+ - name: Install uv
32
+ uses: astral-sh/setup-uv@v5
33
+ with:
34
+ enable-cache: true
35
+ cache-dependency-glob: "uv.lock"
36
+
37
+ - name: Set up Python 3.11
38
+ uses: actions/setup-python@v5
39
+ with:
40
+ python-version: "3.11"
41
+
42
+ - name: Install dependencies
43
+ run: uv sync --all-groups
44
+
45
+ - name: Run unit tests
46
+ run: uv run pytest tests/ -m "not integration" -q
47
+
48
+ # ── 2. Build sdist + wheel ────────────────────────────────────────────────
49
+ build:
50
+ name: Build distribution
51
+ runs-on: ubuntu-latest
52
+ needs: test
53
+
54
+ steps:
55
+ - name: Checkout
56
+ uses: actions/checkout@v4
57
+
58
+ - name: Install uv
59
+ uses: astral-sh/setup-uv@v5
60
+ with:
61
+ enable-cache: true
62
+
63
+ - name: Set up Python 3.11
64
+ uses: actions/setup-python@v5
65
+ with:
66
+ python-version: "3.11"
67
+
68
+ - name: Install dependencies
69
+ run: uv sync --all-groups
70
+
71
+ - name: Build sdist and wheel
72
+ run: uv build
73
+
74
+ - name: Verify build artifacts
75
+ run: |
76
+ ls -lh dist/
77
+ uv run python -m twine check dist/*
78
+
79
+ - name: Upload dist artifacts
80
+ uses: actions/upload-artifact@v4
81
+ with:
82
+ name: dist
83
+ path: dist/
84
+ retention-days: 7
85
+
86
+ # ── 3. Publish to TestPyPI (pre-releases + manual) ────────────────────────
87
+ publish-testpypi:
88
+ name: Publish → TestPyPI
89
+ runs-on: ubuntu-latest
90
+ needs: build
91
+ environment:
92
+ name: testpypi
93
+ url: https://test.pypi.org/p/openvisionkit
94
+
95
+ if: |
96
+ (github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi') ||
97
+ (github.event_name == 'release' && github.event.release.prerelease == true)
98
+
99
+ steps:
100
+ - name: Download dist artifacts
101
+ uses: actions/download-artifact@v4
102
+ with:
103
+ name: dist
104
+ path: dist/
105
+
106
+ - name: Publish to TestPyPI
107
+ uses: pypa/gh-action-pypi-publish@release/v1
108
+ with:
109
+ repository-url: https://test.pypi.org/legacy/
110
+ print-hash: true
111
+
112
+ # ── 4. Publish to PyPI (stable releases + manual) ────────────────────────
113
+ publish-pypi:
114
+ name: Publish → PyPI
115
+ runs-on: ubuntu-latest
116
+ needs: build
117
+ environment:
118
+ name: pypi
119
+ url: https://pypi.org/p/openvisionkit
120
+
121
+ if: |
122
+ (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi') ||
123
+ (github.event_name == 'release' && github.event.release.prerelease == false)
124
+
125
+ steps:
126
+ - name: Download dist artifacts
127
+ uses: actions/download-artifact@v4
128
+ with:
129
+ name: dist
130
+ path: dist/
131
+
132
+ - name: Publish to PyPI
133
+ uses: pypa/gh-action-pypi-publish@release/v1
134
+ with:
135
+ print-hash: true
@@ -0,0 +1,37 @@
1
+ name: Renovate
2
+
3
+ on:
4
+ schedule:
5
+ # Run every Monday at 01:00 UTC (before 6am SGT per renovate.json schedule)
6
+ - cron: "0 1 * * 1"
7
+ workflow_dispatch:
8
+ inputs:
9
+ dry_run:
10
+ description: "Dry-run (log only, no PRs created)"
11
+ required: false
12
+ default: "false"
13
+ type: boolean
14
+
15
+ permissions:
16
+ contents: write
17
+ pull-requests: write
18
+ issues: write
19
+
20
+ jobs:
21
+ renovate:
22
+ name: Renovate dependency update bot
23
+ runs-on: ubuntu-latest
24
+
25
+ steps:
26
+ - name: Checkout
27
+ uses: actions/checkout@v4
28
+
29
+ - name: Run Renovate
30
+ uses: renovatebot/github-action@v40
31
+ env:
32
+ # Set LOG_LEVEL=debug for verbose output
33
+ LOG_LEVEL: info
34
+ RENOVATE_DRY_RUN: ${{ inputs.dry_run == 'true' && 'full' || 'null' }}
35
+ with:
36
+ token: ${{ secrets.RENOVATE_TOKEN }}
37
+ # renovate.json at repo root is picked up automatically
@@ -0,0 +1,193 @@
1
+ name: Reusable GitHub Release
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ tag:
7
+ description: Git tag to release (e.g. v0.2.1). Must already exist in the repo.
8
+ required: true
9
+ type: string
10
+ python-version:
11
+ description: Python version used to build distribution artifacts
12
+ required: false
13
+ default: "3.11"
14
+ type: string
15
+ prerelease:
16
+ description: Mark this release as a pre-release
17
+ required: false
18
+ default: false
19
+ type: boolean
20
+
21
+ jobs:
22
+ github-release:
23
+ name: Create GitHub Release — ${{ inputs.tag }}
24
+ runs-on: ubuntu-latest
25
+ permissions:
26
+ contents: write # create release, upload assets
27
+
28
+ steps:
29
+ # ── Checkout at the exact tag ─────────────────────────────────────────
30
+ - name: Check out repository at tag
31
+ uses: actions/checkout@v4
32
+ with:
33
+ ref: ${{ inputs.tag }}
34
+ fetch-depth: 0
35
+ fetch-tags: true
36
+
37
+ # ── Toolchain ─────────────────────────────────────────────────────────
38
+ - name: Set up uv
39
+ uses: astral-sh/setup-uv@v5
40
+ with:
41
+ python-version: ${{ inputs.python-version }}
42
+ enable-cache: true
43
+
44
+ # ── Derive version string from tag ────────────────────────────────────
45
+ - name: Derive version from tag
46
+ id: version
47
+ run: |
48
+ TAG="${{ inputs.tag }}"
49
+ VERSION="${TAG#v}" # strip leading 'v'
50
+ echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
51
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
52
+
53
+ PRERELEASE="${{ inputs.prerelease }}"
54
+ if [ "$PRERELEASE" = "true" ] || echo "$TAG" | grep -qE '\-(alpha|beta|rc|dev)'; then
55
+ echo "prerelease=true" >> "$GITHUB_OUTPUT"
56
+ else
57
+ echo "prerelease=false" >> "$GITHUB_OUTPUT"
58
+ fi
59
+
60
+ # ── Build distribution artifacts ──────────────────────────────────────
61
+ - name: Build sdist and wheel
62
+ run: uv build
63
+
64
+ - name: List built artifacts
65
+ run: ls -lh dist/
66
+
67
+ # ── Validate distribution with twine ──────────────────────────────────
68
+ - name: Validate distributions
69
+ run: |
70
+ uv tool install twine
71
+ uv tool run twine check dist/*
72
+
73
+ # ── Generate SHA-256 checksums ─────────────────────────────────────────
74
+ - name: Generate SHA-256 checksums
75
+ run: |
76
+ VERSION="${{ steps.version.outputs.version }}"
77
+ cd dist
78
+ sha256sum * | tee "../checksums-${VERSION}.sha256"
79
+ cd ..
80
+ echo "Checksums:"
81
+ cat "checksums-${VERSION}.sha256"
82
+
83
+ # ── Extract release notes from CHANGELOG.md ───────────────────────────
84
+ - name: Extract release notes from CHANGELOG.md
85
+ id: notes
86
+ env:
87
+ RELEASE_VERSION: ${{ steps.version.outputs.version }}
88
+ RELEASE_TAG: ${{ steps.version.outputs.tag }}
89
+ run: |
90
+ NOTES=$(python3 - <<'PYEOF'
91
+ import re, os, sys
92
+
93
+ version = os.environ.get("RELEASE_VERSION", "")
94
+ tag = os.environ.get("RELEASE_TAG", "")
95
+
96
+ try:
97
+ content = open("CHANGELOG.md").read()
98
+ except FileNotFoundError:
99
+ print("No CHANGELOG.md found.")
100
+ sys.exit(0)
101
+
102
+ sections = re.split(r"\n(?=## )", content)
103
+ target = next(
104
+ (s for s in sections
105
+ if version in s.split("\n")[0] or tag in s.split("\n")[0]),
106
+ None,
107
+ )
108
+ if target:
109
+ body = "\n".join(target.split("\n")[1:]).strip()
110
+ print(body if body else "No details in CHANGELOG for this version.")
111
+ else:
112
+ print(f"See CHANGELOG.md for details on {tag}.")
113
+ PYEOF
114
+ )
115
+
116
+ {
117
+ echo "notes<<RELEASE_NOTES_DELIMITER"
118
+ echo "$NOTES"
119
+ echo "RELEASE_NOTES_DELIMITER"
120
+ } >> "$GITHUB_OUTPUT"
121
+
122
+ # ── Create or update the GitHub release ───────────────────────────────
123
+ - name: Create GitHub release
124
+ id: create-release
125
+ env:
126
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
127
+ run: |
128
+ TAG="${{ steps.version.outputs.tag }}"
129
+ PRERELEASE="${{ steps.version.outputs.prerelease }}"
130
+ TITLE="Release ${TAG}"
131
+ NOTES="${{ steps.notes.outputs.notes }}"
132
+
133
+ PR_FLAG=""
134
+ [ "$PRERELEASE" = "true" ] && PR_FLAG="--prerelease"
135
+
136
+ if gh release view "$TAG" &>/dev/null; then
137
+ echo "Release $TAG already exists — updating notes."
138
+ gh release edit "$TAG" \
139
+ --title "$TITLE" \
140
+ --notes "$NOTES" \
141
+ $PR_FLAG \
142
+ --latest
143
+ else
144
+ gh release create "$TAG" \
145
+ --title "$TITLE" \
146
+ --notes "$NOTES" \
147
+ --verify-tag \
148
+ $PR_FLAG \
149
+ --latest
150
+ fi
151
+
152
+ # ── Upload distribution artifacts and checksums as release assets ─────
153
+ - name: Upload release assets
154
+ env:
155
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
156
+ run: |
157
+ TAG="${{ steps.version.outputs.tag }}"
158
+ VERSION="${{ steps.version.outputs.version }}"
159
+
160
+ gh release upload "$TAG" \
161
+ dist/* \
162
+ "checksums-${VERSION}.sha256" \
163
+ --clobber
164
+
165
+ # ── Job summary ───────────────────────────────────────────────────────
166
+ - name: Write job summary
167
+ if: always()
168
+ env:
169
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
170
+ run: |
171
+ TAG="${{ steps.version.outputs.tag }}"
172
+ VERSION="${{ steps.version.outputs.version }}"
173
+ RELEASE_URL=$(gh release view "$TAG" --json url -q .url 2>/dev/null || echo "N/A")
174
+
175
+ {
176
+ echo "## GitHub Release: ${TAG:-unknown}"
177
+ echo ""
178
+ echo "| Field | Value |"
179
+ echo "|-------|-------|"
180
+ echo "| Tag | \`${TAG}\` |"
181
+ echo "| Version | \`${VERSION}\` |"
182
+ echo "| Pre-release | \`${{ steps.version.outputs.prerelease }}\` |"
183
+ echo "| Release URL | ${RELEASE_URL} |"
184
+ echo ""
185
+ echo "### Artifacts"
186
+ echo "\`\`\`"
187
+ [ -d dist ] && ls -lh dist/ || echo "dist/ not found — build may have failed"
188
+ [ -f "checksums-${VERSION}.sha256" ] && cat "checksums-${VERSION}.sha256" || echo "checksums file not found"
189
+ echo "\`\`\`"
190
+ echo ""
191
+ echo "### Release Notes"
192
+ echo "${{ steps.notes.outputs.notes }}"
193
+ } >> "$GITHUB_STEP_SUMMARY"