-
Notifications
You must be signed in to change notification settings - Fork 0
127 lines (112 loc) · 4.8 KB
/
Copy pathpublish.yml
File metadata and controls
127 lines (112 loc) · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# Publish mnemosyne-engine to PyPI via OIDC Trusted Publisher
#
# Security model:
# - ONLY fires on GitHub Releases published against the main branch
# - Build and publish are SEPARATE jobs (build artifacts are never modified after build)
# - All actions pinned to full SHA (immune to tag-hijack attacks)
# - Publish job requires "release" environment (configure approval rules in GitHub Settings)
# - Minimal permissions per job (least-privilege)
# - Artifact attestation creates a signed provenance record (SLSA Level 2)
# - No secrets, tokens, or passwords anywhere — pure OIDC
#
# Required setup:
# 1. PyPI: Add trusted publisher (owner=castnettech, repo=mnemosyne, workflow=publish.yml, environment=release)
# 2. GitHub: Create "release" environment in repo Settings > Environments
# - Add deployment protection rule: "Required reviewers" = your account
# - Restrict to "main" branch only
name: Publish to PyPI
on:
release:
types: [published]
# Top-level: deny all permissions by default
permissions: {}
jobs:
# ── Gate: verify release targets main ──────────────────────────────
verify:
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Reject releases not targeting main
if: github.event.release.target_commitish != 'main'
run: |
echo "::error::Release must target 'main' branch. Got '${{ github.event.release.target_commitish }}'."
exit 1
- name: Reject pre-releases
if: github.event.release.prerelease == true
run: |
echo "::error::Pre-releases are not published to PyPI."
exit 1
# ── Build: produce distribution artifacts ──────────────────────────
build:
needs: [verify]
runs-on: ubuntu-latest
permissions:
contents: read # checkout only
steps:
- name: Checkout pinned to release tag
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.release.tag_name }}
persist-credentials: false
- name: Verify tag matches pyproject.toml version
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG_VERSION=$(python3 -c "
import re, pathlib
text = pathlib.Path('pyproject.toml').read_text()
m = re.search(r'^version\s*=\s*\"([^\"]+)\"', text, re.M)
print(m.group(1) if m else 'UNKNOWN')
")
echo "Tag version: $TAG"
echo "Package version: $PKG_VERSION"
if [ "$TAG" != "$PKG_VERSION" ]; then
echo "::error::Tag '$TAG' does not match pyproject.toml version '$PKG_VERSION'"
exit 1
fi
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install build frontend
run: pip install --upgrade build
- name: Build sdist and wheel
run: python -m build
- name: Verify dist contents
run: |
ls -la dist/
# Ensure exactly 1 sdist + 1 wheel, nothing unexpected
SDIST_COUNT=$(ls dist/*.tar.gz 2>/dev/null | wc -l)
WHEEL_COUNT=$(ls dist/*.whl 2>/dev/null | wc -l)
if [ "$SDIST_COUNT" -ne 1 ] || [ "$WHEEL_COUNT" -ne 1 ]; then
echo "::error::Expected 1 sdist + 1 wheel, got $SDIST_COUNT sdist + $WHEEL_COUNT wheel"
exit 1
fi
- name: Upload dist artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/
if-no-files-found: error
retention-days: 5
# ── Publish: push to PyPI via OIDC ─────────────────────────────────
publish:
needs: [build]
runs-on: ubuntu-latest
environment: release # requires environment approval + branch restriction
permissions:
id-token: write # OIDC token for PyPI trusted publisher
attestations: write # artifact attestation (provenance signing)
contents: read # needed for attestation to read repo
steps:
- name: Download dist artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/
- name: Generate artifact attestation
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
with:
subject-path: "dist/*"
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
# No token needed — OIDC trusted publisher handles authentication