Files
gmail-channel-molecule/.gitea/workflows/auto-release.yml
T
hongming-ceo-delegated 284f4802ab
ci / gmail channel — unit tests (pull_request) Successful in 12s
fix: release the Cloudflare UA fix, and close the same hole in onboarding
runtime#360 ("channel daemons never deliver on docker-less/pull boxes") is not a
daemon-supervisor bug, a secrets bug, or a PLATFORM_URL bug. The daemon runs. Its
POSTs are 403'd at the Cloudflare edge, because the box runs a wheel that predates
the fix for exactly that.

The chain:

  1. pyproject version has been 0.1.0 since 2026-07-25, and nothing bumps it.
  2. auto-release.yml cuts v<pyproject version> only "if untagged". v0.1.0 already
     existed, so every merge to main since printed
       ::notice::v0.1.0 already exists -- nothing to release
     and exited 0 GREEN.
  3. So the only published wheel is 0.1.0 (2026-07-26T02:27:20Z, built from
     d2c3a8b) — which predates PR #2, the browser-User-Agent fix (2026-07-27
     21:33Z).
  4. Every install therefore pulls a bridge.py that sends the default
     "Python-urllib" UA.
  5. On a docker/push box PLATFORM_URL is a local address — no WAF in path — so it
     works end to end. On a docker-less/pull box PLATFORM_URL is the public
     https://<tenant>.moleculesai.app, the WAF 403s it, and every inbound A2A POST
     is silently dropped.

That asymmetry is why Cloudflare was ruled out in #360: the docker reproduction
genuinely has no WAF in path. The direction that was not tested is where the
defect lived.

Verified against the live edge 2026-07-28 — same request, same second, only the
User-Agent differing:

    Python-urllib/3.11  -> 403 text/plain
    python-httpx/0.27.0 -> 200 application/json
    curl/8.4.0          -> 200 application/json

(httpx passes, which is why the runtime's own httpx calls were unaffected and only
this urllib-based plugin broke.)

Three changes:

* Version 0.1.0 -> 0.1.1 (pyproject, plugin.yaml, and the SKILL.md install pin,
  which is what installs actually resolve). This is what ships PR #2's fix.

* onboard._platform_post now sends the same UA. PR #2 fixed bridge.py; this
  identical call — the one that writes the GMAIL_* secrets and self-restarts the
  workspace — was left on the default UA, so onboarding also 403s on precisely the
  docker-less boxes. Same bug, one file over.

* auto-release.yml no longer exits 0 when the tag exists but main has moved past
  it. It now compares the tagged sha to HEAD and fails loudly, naming the fix
  (bump the version). The green "nothing to release" is what hid an unshipped
  production fix for a full day.

Negative controls, each seen failing first: removing the onboard UA reds both new
tests; feeding the real shas (v0.1.0 -> d2c3a8b, main -> 1e3bb0b) through the new
release gate exits 1 where the old logic exited 0 green.

The structural guard initially passed with the header deleted, because it matched
the explanatory comment rather than the code — it now strips comments before
asserting. Caught by negative-controlling the test itself.

25 tests pass.

Refs molecule-ai-workspace-runtime#360

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:47:40 -07:00

104 lines
4.7 KiB
YAML

name: auto-release
# Publish-on-merge: pyproject.toml [project].version is the source of truth
# (bumped IN the PR); a green merge to main cuts the tag `v<that version>` if it
# does not exist yet, and publish.yml takes the tag from there. CI never pushes a
# commit to main (tag-ref writes bypass branch push protection), and an
# already-tagged version is a loud no-op, so re-runs / non-bump merges are safe.
#
# Gitea has no `workflow_run` trigger, so the merge-blocking unit gate is re-run
# inline here and the tag is cut only on green. The gate mirrors ci.yml — stdlib
# tests, public-PyPI-only deps — so it never couples the release to the private
# Gitea index / Infisical.
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: auto-release
cancel-in-progress: false
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Install test deps (public PyPI only)
run: pip install --quiet pytest pyyaml
- name: Byte-compile
run: python -m py_compile gmail_channel_molecule/*.py
- name: Re-run the merge-blocking gate (unit tests)
run: pytest tests/ -q
release:
runs-on: ubuntu-latest
needs: gate
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Cut tag v<pyproject version> if untagged
env:
RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }}
GITEA_URL: https://git.moleculesai.app
REPO: molecule-ai/gmail-channel-molecule
run: |
set -eu
if [ -z "${RELEASE_BOT_TOKEN:-}" ]; then
echo "::error::RELEASE_BOT_TOKEN org secret is not available -- cannot cut the release tag"
exit 1
fi
VERSION=$(python3 - <<'PY'
import tomllib
with open("pyproject.toml", "rb") as f:
print(tomllib.load(f)["project"]["version"])
PY
)
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+([a-z0-9.+-]+)?$'; then
echo "::error::pyproject version $VERSION does not look like a PEP 440 release"
exit 1
fi
TAG="v$VERSION"
STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "Authorization: token $RELEASE_BOT_TOKEN" -H "User-Agent: curl/8.4.0" \
"$GITEA_URL/api/v1/repos/$REPO/tags/$TAG")
if [ "$STATUS" = "200" ]; then
# The tag exists. That is only benign when main is STILL AT that tag.
# If main has moved on, every commit since is unreleased — and this
# step used to exit 0 green and say "nothing to release", which is how
# the Cloudflare User-Agent fix (PR #2) sat unpublished on main while
# every install kept pulling the broken v0.1.0 wheel. That silence cost
# a production channel outage and a misdirected investigation
# (runtime#360). Bumping the version in pyproject.toml is the release
# action; not bumping it must be loud, not green.
TAGGED_SHA=$(curl -sS -H "Authorization: token $RELEASE_BOT_TOKEN" \
-H "User-Agent: curl/8.4.0" \
"$GITEA_URL/api/v1/repos/$REPO/tags/$TAG" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print((d.get("commit") or {}).get("sha",""))')
if [ "$TAGGED_SHA" = "$GITHUB_SHA" ]; then
echo "::notice::$TAG already exists at this exact commit -- nothing to release"
exit 0
fi
echo "::error::$TAG already exists but points at ${TAGGED_SHA:-<unknown>}, while main is at $GITHUB_SHA."
echo "::error::Every commit since $TAG is UNRELEASED: the published wheel does not contain it."
echo "::error::Bump [project].version in pyproject.toml (currently $VERSION) so a new tag can be cut."
exit 1
fi
BODY=$(printf '{"tag_name":"%s","target":"%s","message":"auto-release %s (green merge to main)"}' \
"$TAG" "$GITHUB_SHA" "$TAG")
CODE=$(curl -sS -o /tmp/tag-out.json -w "%{http_code}" -X POST \
-H "Authorization: token $RELEASE_BOT_TOKEN" -H "User-Agent: curl/8.4.0" \
-H "Content-Type: application/json" \
"$GITEA_URL/api/v1/repos/$REPO/tags" -d "$BODY")
if [ "$CODE" != "201" ]; then
echo "::error::tag creation failed (HTTP $CODE): $(cat /tmp/tag-out.json)"
exit 1
fi
echo "::notice::cut $TAG at $GITHUB_SHA -- publish.yml takes it from here"