-
Notifications
You must be signed in to change notification settings - Fork 0
150 lines (138 loc) · 6.3 KB
/
Copy pathpublish.yml
File metadata and controls
150 lines (138 loc) · 6.3 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
name: Publish
run-name: "Publish opik-hermes (${{ github.event_name }}) by @${{ github.actor }}"
# Two targets — deliberate, like the other Opik packages (no publish on every
# push):
# - TestPyPI: via manual `workflow_dispatch` with target=testpypi. Run it
# from any branch to exercise the full build→upload→install flow before a
# real release. Bump the version in pyproject.toml first so the upload
# doesn't collide (TestPyPI/PyPI reject re-uploading a version —
# skip-existing turns that into a no-op rather than a failure).
# - PyPI: on a published GitHub Release (the real release), or dispatch with
# target=pypi. Uses the org PYPI_API_TOKEN, same as the other Opik packages.
on:
release:
types: [published]
workflow_dispatch:
inputs:
target:
description: "Where to publish"
type: choice
options: [testpypi, pypi]
default: testpypi
permissions:
contents: read
id-token: write # required for PyPI Trusted Publishing (OIDC)
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# On a real release the git tag (vX.Y.Z) and pyproject.toml's version
# must agree — the wheel is built from pyproject, so a mismatch would
# tag one version while `skip-existing` silently no-ops the upload of the
# already-published one. Fail loudly instead.
- name: Tag matches pyproject version (release only)
if: github.event_name == 'release'
run: |
FILE_VER=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
TAG_VER="${GITHUB_REF_NAME#v}"
if [ "$FILE_VER" != "$TAG_VER" ]; then
echo "::error title=Version mismatch::Release tag v$TAG_VER != pyproject version $FILE_VER. Bump pyproject.toml to match the tag before publishing."
exit 1
fi
echo "OK: tag v$TAG_VER matches pyproject version $FILE_VER"
- name: Build sdist + wheel
run: |
pip install -U pip build
python -m build --sdist --wheel --outdir dist/ .
- name: Verify wheel exposes the plugin entry point
run: |
python - <<'PY'
import glob, zipfile
wheel = glob.glob("dist/*.whl")[0]
names = zipfile.ZipFile(wheel).namelist()
assert "opik_hermes/__init__.py" in names, "plugin module missing from wheel"
body = zipfile.ZipFile(wheel).read(
next(n for n in names if n.endswith("entry_points.txt"))
).decode()
# The entry point must resolve to the MODULE (opik = opik_hermes), not
# opik_hermes:register — Hermes does ep.load() then getattr(mod,
# "register"), so a ":register" suffix breaks plugin loading.
import configparser, io
cp = configparser.ConfigParser()
cp.read_string(body)
ep = cp["hermes_agent.plugins"]["opik"].strip()
assert ep == "opik_hermes", (
f"opik entry point must be 'opik_hermes' (module), got {ep!r}"
)
print("OK:", wheel, "entry point:", ep)
PY
# --- Decide target -----------------------------------------------------
# release -> pypi
# workflow_dispatch-> the chosen input
# push (branch/PR) -> testpypi
- name: Resolve publish target
id: target
run: |
if [ "${{ github.event_name }}" = "release" ]; then
echo "to=pypi" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "to=${{ inputs.target }}" >> "$GITHUB_OUTPUT"
else
echo "to=testpypi" >> "$GITHUB_OUTPUT"
fi
# --- Token preflight ---------------------------------------------------
# Fail fast with an actionable message rather than reaching the upload
# step with a malformed token. Probes the matching upload endpoint:
# 400/422 = auth OK + empty payload (success for a preflight),
# 401/403 = token rejected.
#
# No token => Trusted Publishing (OIDC): there is nothing to preflight, so
# skip the probe entirely (and DON'T POST to the upload endpoint — that
# request counts against PyPI's upload rate limit even though it can't
# authenticate anything). The action + PyPI handle OIDC auth at upload.
- name: Token preflight
env:
TARGET: ${{ steps.target.outputs.to }}
PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
run: |
set -e
if [ "$TARGET" = "pypi" ]; then
TOKEN="$PYPI_TOKEN"; URL="https://upload.pypi.org/legacy/"; NAME="PYPI_API_TOKEN"
else
TOKEN="$TEST_PYPI_TOKEN"; URL="https://test.pypi.org/legacy/"; NAME="TEST_PYPI_API_TOKEN"
fi
if [ -z "${TOKEN:-}" ]; then
echo "No $NAME set — assuming Trusted Publishing (OIDC) for $TARGET; skipping token preflight."
exit 0
fi
if [[ "$TOKEN" != pypi-* ]]; then
echo "::error title=Publish preflight::$NAME is set but does not start with 'pypi-'."
exit 1
fi
CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-u "__token__:${TOKEN}" -H "User-Agent: opik-hermes-release-preflight" "$URL" || echo "000")
echo "Preflight HTTP $CODE against $URL"
case "$CODE" in
400|422|200) echo "Token accepted by $TARGET." ;;
401|403) echo "::error title=Publish preflight::$NAME rejected by $TARGET (HTTP $CODE)."; exit 1 ;;
*) echo "::warning title=Publish preflight::Unexpected HTTP $CODE; proceeding to upload." ;;
esac
- name: Publish to TestPyPI
if: steps.target.outputs.to == 'testpypi'
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository-url: https://test.pypi.org/legacy/
skip-existing: true
- name: Publish to PyPI
if: steps.target.outputs.to == 'pypi'
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
password: ${{ secrets.PYPI_API_TOKEN }}
skip-existing: true