Files
lark-channel-molecule/.gitea/workflows/publish.yml
T
hongming-ceo-delegated aca01aeb63 ci: publish-on-merge — auto-release cuts v<version> tag on green main, publish.yml ships to the org PyPI registry
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>
2026-07-02 20:38:12 -07:00

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"