aca01aeb63
Closes the Phase-0 follow-up (#5 merged the code but the release never ran). Version-as-declared: the PR bumps pyproject, a green merge cuts the tag (tag-ref write, no CI pushes to main), the tag triggers the publish with Infisical-sourced publisher creds (/ci/gitea-pypi-publisher, org-level CI machine identity) + idempotent upload + install-from-registry smoke. Bump 0.1.0 -> 0.2.0 so the first merge exercises the whole chain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
196 lines
8.7 KiB
YAML
196 lines
8.7 KiB
YAML
name: publish
|
|
|
|
# Tag-triggered publish to the org Gitea PyPI registry (mirrors the runtime's
|
|
# publish-runtime.yml, minus the consumer-propagation job — nothing pins this
|
|
# package yet). The tag is the source of truth: its version is stamped into
|
|
# pyproject.toml at build time (ephemeral, in-CI, never committed), so the
|
|
# wheel carries the tag's version by construction even if main's pyproject
|
|
# were to lag.
|
|
#
|
|
# Publisher credentials come from the Infisical SSOT (/ci/gitea-pypi-publisher,
|
|
# prod) via the org-level CI machine identity — no per-repo credential copies
|
|
# (the unified-credentials rule). A broken/empty fetch hard-fails loudly.
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: publish
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
GITEA_PYPI_SIMPLE_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
GITEA_PYPI_UPLOAD_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/
|
|
|
|
jobs:
|
|
publish:
|
|
runs-on: publish
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
cache: pip
|
|
|
|
- name: Derive version + stamp pyproject (tag is source of truth)
|
|
id: version
|
|
run: |
|
|
set -eu
|
|
STAMP_FROM_TAG=0
|
|
if echo "$GITHUB_REF" | grep -q "^refs/tags/v"; then
|
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
STAMP_FROM_TAG=1
|
|
else
|
|
VERSION=$(python - <<'PY'
|
|
import tomllib
|
|
with open("pyproject.toml", "rb") as f:
|
|
print(tomllib.load(f)["project"]["version"])
|
|
PY
|
|
)
|
|
fi
|
|
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+([a-z0-9.+-]+)?$'; then
|
|
echo "::error::version $VERSION does not look like a PEP 440 release"
|
|
exit 1
|
|
fi
|
|
if [ "$STAMP_FROM_TAG" = "1" ]; then
|
|
VERSION="$VERSION" python - <<'PY'
|
|
import os, re
|
|
version = os.environ["VERSION"]
|
|
with open("pyproject.toml", "r", encoding="utf-8") as f:
|
|
text = f.read()
|
|
new, n = re.subn(
|
|
r'(?m)^(version\s*=\s*)"[^"]+"',
|
|
r'\g<1>"' + version + '"',
|
|
text,
|
|
count=1,
|
|
)
|
|
if n != 1:
|
|
raise SystemExit('could not locate a single version = "..." line in pyproject.toml')
|
|
with open("pyproject.toml", "w", encoding="utf-8") as f:
|
|
f.write(new)
|
|
print(f"stamped pyproject [project].version = {version} (ephemeral build-time)")
|
|
PY
|
|
fi
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "Publishing lark-channel-molecule $VERSION to the Gitea package registry"
|
|
|
|
- name: Build wheel + sdist
|
|
run: |
|
|
pip install build twine
|
|
python -m build
|
|
python -m twine check dist/*
|
|
|
|
- name: Fetch Gitea PyPI publisher creds from Infisical SSOT
|
|
env:
|
|
INFISICAL_CI_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
|
|
INFISICAL_CI_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
|
|
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
|
|
run: |
|
|
set -uo pipefail
|
|
BASE="https://key.moleculesai.app"
|
|
if [ -z "${INFISICAL_CI_CLIENT_ID:-}" ] || [ -z "${INFISICAL_CI_CLIENT_SECRET:-}" ] || [ -z "${INFISICAL_PROJECT_ID:-}" ]; then
|
|
echo "::error::Infisical CI machine identity (INFISICAL_CI_CLIENT_ID / INFISICAL_CI_CLIENT_SECRET / INFISICAL_CI_PROJECT_ID) is not set -- cannot fetch publisher creds from the SSOT"
|
|
exit 1
|
|
fi
|
|
TOK=$(curl -fsS -X POST "$BASE/api/v1/auth/universal-auth/login" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"clientId\":\"$INFISICAL_CI_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CI_CLIENT_SECRET\"}" \
|
|
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=d.get("accessToken"); print(v if isinstance(v,str) and v else "")')
|
|
if [ -z "${TOK}" ]; then
|
|
echo "::error::Infisical universal-auth login returned no accessToken"
|
|
exit 1
|
|
fi
|
|
read_secret() {
|
|
curl -fsS "$BASE/api/v3/secrets/raw/$1?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&secretPath=$2" \
|
|
-H "Authorization: Bearer $TOK" \
|
|
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=(d.get("secret") or {}).get("secretValue"); print(v if isinstance(v,str) and v else "")'
|
|
}
|
|
MOLECULE_PYPI_GITEA_PUBLISHER_USER=$(read_secret MOLECULE_PYPI_GITEA_PUBLISHER_USER %2Fci%2Fgitea-pypi-publisher)
|
|
MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN=$(read_secret MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN %2Fci%2Fgitea-pypi-publisher)
|
|
if [ -z "${MOLECULE_PYPI_GITEA_PUBLISHER_USER}" ] || [ -z "${MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN}" ]; then
|
|
echo "::error::Infisical returned empty publisher creds at /ci/gitea-pypi-publisher (prod) -- failing loudly"
|
|
exit 1
|
|
fi
|
|
echo "::add-mask::$MOLECULE_PYPI_GITEA_PUBLISHER_USER"
|
|
echo "::add-mask::$MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN"
|
|
echo "MOLECULE_PYPI_GITEA_PUBLISHER_USER=$MOLECULE_PYPI_GITEA_PUBLISHER_USER" >> "$GITHUB_ENV"
|
|
echo "MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN=$MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN" >> "$GITHUB_ENV"
|
|
echo "Gitea PyPI publisher creds loaded from Infisical (user len=${#MOLECULE_PYPI_GITEA_PUBLISHER_USER}, token len=${#MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN})"
|
|
|
|
- name: Publish to Gitea package registry
|
|
env:
|
|
MOLECULE_PYPI_GITEA_PUBLISHER_USER: ${{ env.MOLECULE_PYPI_GITEA_PUBLISHER_USER }}
|
|
MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN: ${{ env.MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
if [ -z "${MOLECULE_PYPI_GITEA_PUBLISHER_USER:-}" ] || [ -z "${MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN:-}" ]; then
|
|
echo "::error::publisher creds were not exported by the Infisical fetch step"
|
|
exit 1
|
|
fi
|
|
# Idempotent publish: twine --skip-existing is rejected by the Gitea
|
|
# registry (UnsupportedConfiguration — broke runtime v0.3.18), so
|
|
# pre-check the simple index and skip when everything is present.
|
|
SIMPLE_BASE="${GITEA_PYPI_UPLOAD_URL%/}/simple"
|
|
missing=0
|
|
for f in dist/*; do
|
|
base=$(basename "$f")
|
|
pkg=$(printf %s "$base" | cut -d- -f1 | tr _ -)
|
|
if curl -fsS -u "$MOLECULE_PYPI_GITEA_PUBLISHER_USER:$MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN" \
|
|
"$SIMPLE_BASE/$pkg/" 2>/dev/null | grep -qF "$base"; then
|
|
echo "::notice::$base already in registry"
|
|
else
|
|
missing=1
|
|
fi
|
|
done
|
|
if [ "$missing" = "0" ]; then
|
|
echo "::notice::all dist files already published (re-run) -- skipping upload"
|
|
else
|
|
python -m twine upload \
|
|
--repository-url "$GITEA_PYPI_UPLOAD_URL" \
|
|
--username "$MOLECULE_PYPI_GITEA_PUBLISHER_USER" \
|
|
--password "$MOLECULE_PYPI_GITEA_PUBLISHER_TOKEN" \
|
|
dist/*
|
|
fi
|
|
|
|
- name: Verify install from Gitea registry
|
|
env:
|
|
PKG_VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
set -eu
|
|
python -m venv /tmp/lark-registry-smoke
|
|
PIP=/tmp/lark-registry-smoke/bin/pip
|
|
PY=/tmp/lark-registry-smoke/bin/python
|
|
$PIP install --upgrade --quiet pip
|
|
# Gitea index primary (hosts this package + the runtime dep), public
|
|
# PyPI as extra for lark-oapi/qrcode — same shape as ci.yml.
|
|
$PIP install \
|
|
--index-url "$GITEA_PYPI_SIMPLE_URL" \
|
|
--extra-index-url "https://pypi.org/simple/" \
|
|
--no-cache-dir \
|
|
"lark-channel-molecule==$PKG_VERSION"
|
|
INSTALLED=$($PIP show lark-channel-molecule | awk -F': ' '/^Version:/{print $2}')
|
|
if [ "$INSTALLED" != "$PKG_VERSION" ]; then
|
|
echo "::error::installed $INSTALLED, want $PKG_VERSION"
|
|
exit 1
|
|
fi
|
|
$PY -c "import lark_channel_molecule; from lark_channel_molecule import brand, bridge, daemon; print('lark-channel registry smoke OK')"
|
|
|
|
- name: Publish summary
|
|
if: always()
|
|
run: |
|
|
{
|
|
echo "## publish lark-channel-molecule $(date -u +%FT%TZ)"
|
|
echo
|
|
echo "**Version:** \`${{ steps.version.outputs.version }}\`"
|
|
echo "**Registry:** \`$GITEA_PYPI_SIMPLE_URL\`"
|
|
echo
|
|
echo "Public PyPI is intentionally not part of this release path."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|