284f4802ab
ci / gmail channel — unit tests (pull_request) Successful in 12s
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>
104 lines
4.7 KiB
YAML
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"
|