Files
molecule-ai-workspace-templ…/.gitea/workflows/upstream-sync.yml
T
hongmingandClaude Fable 5 b5aa8cb764
CI / Template validation (static) (pull_request) Successful in 7s
meta-ci-advisory / meta (advisory) (pull_request) Successful in 8s
CI / Template validation (static) (push) Successful in 10s
Secret scan / secret-scan (pull_request) Successful in 13s
verify-providers-projection / Regenerate projection, fail on drift, assert registry ⊆ template (pull_request) Successful in 19s
CI / Adapter unit tests (push) Successful in 31s
CI / Adapter unit tests (pull_request) Successful in 35s
CI / Template validation (runtime) (pull_request) Successful in 1m12s
CI / T4 tier-4 conformance (live) (push) Successful in 1m11s
CI / Template validation (runtime) (push) Successful in 1m23s
CI / validate (push) Successful in 2s
CI / T4 tier-4 conformance (live) (pull_request) Successful in 1m33s
CI / validate (pull_request) Successful in 2s
fix(sync): close stale older-version bump PRs instead of deadlocking (review wf_7cb5003d #10)
An open bump PR for an older openclaw version blocked every newer
release indefinitely (a red/unreviewed PR would stall a security bump
forever). Same-version PRs still skip; older ones are closed with a
superseded comment and their branch deleted, then the fresh bump opens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:36:41 -07:00

173 lines
8.5 KiB
YAML

name: upstream-sync
# Daily openclaw freshness bot (2026-07-22, principal directive: "keep at
# least once a day sync so everything is up to date").
#
# Watches the public npm registry for a new openclaw release and opens a bump
# PR against the OPENCLAW_VERSION ARG in the Dockerfile. NOT a scheduled CI
# gate: molecule-core's zero-cron ratchet forbids timer-driven TEST runs
# (nobody blocks on a red nightly); this job runs no tests and gates nothing —
# it only surfaces a reviewable bump PR, which then goes through the normal
# per-PR CI like any human-authored change. Humans (or the org's agents) still
# gate the merge — same contract as the runtime .runtime-version propagate bot.
#
# Idempotent: skips when the pin already matches, when the sync branch already
# exists, or when a bump PR is already open. PRs are opened purely via the
# Gitea contents+pulls API (no git clone, so the bot token never lands in an
# on-disk clone URL) — same approach as runtime's propagate job.
#
# AUTH: RUNTIME_BOT_TOKEN from the Infisical SSOT (/shared/runtime-bot) via
# the INFISICAL_CI_* machine identity — the same bot credential that already
# opens .runtime-version bump PRs on this repo. Absent bootstrap creds degrade
# to report-only (config gap must not paint the repo red); present-but-broken
# fetch fails loudly.
on:
schedule:
- cron: '23 6 * * *'
workflow_dispatch:
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
permissions:
contents: read
concurrency:
group: upstream-sync
cancel-in-progress: false
jobs:
sync:
name: Check npm for a newer openclaw and open a bump PR
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Fetch RUNTIME_BOT_TOKEN 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
if [ -z "${INFISICAL_CI_CLIENT_ID:-}" ] || [ -z "${INFISICAL_CI_CLIENT_SECRET:-}" ] || [ -z "${INFISICAL_PROJECT_ID:-}" ]; then
echo "::warning::INFISICAL_CI_* bootstrap creds absent; sync degrades to report-only."
exit 0
fi
BASE="https://key.moleculesai.app"
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 failed with creds present — real infra fault." >&2
exit 1
fi
BOT=$(curl -fsS "$BASE/api/v3/secrets/raw/RUNTIME_BOT_TOKEN?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&secretPath=%2Fshared%2Fruntime-bot" \
-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 "")')
if [ -z "${BOT}" ]; then
echo "::error::Infisical returned empty RUNTIME_BOT_TOKEN at /shared/runtime-bot — failing loudly." >&2
exit 1
fi
echo "::add-mask::$BOT"
echo "BOT_TOKEN=$BOT" >> "$GITHUB_ENV"
- name: Compare npm latest against the Dockerfile pin; open bump PR
env:
BOT_TOKEN: ${{ env.BOT_TOKEN }}
API: ${{ env.GITHUB_SERVER_URL }}/api/v1
REPO: molecule-ai/molecule-ai-workspace-template-openclaw
run: |
set -euo pipefail
LATEST=$(curl -fsS --max-time 30 https://registry.npmjs.org/openclaw/latest \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["version"])')
CURRENT=$(sed -n 's/^ARG OPENCLAW_VERSION=//p' Dockerfile | head -1)
echo "npm latest=$LATEST pinned=$CURRENT"
if [ -z "$LATEST" ] || [ -z "$CURRENT" ]; then
echo "::error::could not resolve versions (latest='$LATEST' pinned='$CURRENT')"; exit 1
fi
if [ "$LATEST" = "$CURRENT" ]; then
echo "pin is current — nothing to do"; exit 0
fi
if [ -z "${BOT_TOKEN:-}" ]; then
echo "::warning::report-only (no BOT_TOKEN): openclaw $CURRENT -> $LATEST bump needed."
exit 0
fi
python3 - "$LATEST" "$CURRENT" <<'PYEOF'
import base64, json, os, sys, urllib.request
latest, current = sys.argv[1], sys.argv[2]
api = os.environ["API"]; repo = os.environ["REPO"]
tok = os.environ["BOT_TOKEN"]
branch = f"sync/openclaw-{latest}"
def req(method, path, body=None, ok=(200, 201)):
r = urllib.request.Request(
f"{api}{path}", method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": f"token {tok}",
"Content-Type": "application/json",
# Cloudflare bot-fight bans urllib's default UA
# at the edge (error 1010) — spoof the fleet's
# standard curl UA (see sync-providers-yaml.yml).
"User-Agent": "curl/8.4.0"})
try:
with urllib.request.urlopen(r) as resp:
return resp.status, json.load(resp)
except urllib.error.HTTPError as e:
return e.code, {}
# Idempotency: existing branch or open PR means a previous tick
# already surfaced this bump — nothing to do.
st, _ = req("GET", f"/repos/{repo}/branches/{branch}")
if st == 200:
print(f"branch {branch} already exists — skipping"); sys.exit(0)
# Stale-PR handling (review wf_7cb5003d #10): an open bump PR for
# the SAME version means a previous tick already surfaced this
# bump — skip. An open bump PR for an OLDER version must NOT
# block newer releases forever (a red/unreviewed 0.19.1 PR would
# otherwise stall a 0.19.2 security fix indefinitely): close it
# with a comment and fall through to open the fresh one.
st, prs = req("GET", f"/repos/{repo}/pulls?state=open")
for p in (prs if isinstance(prs, list) else []):
ref = p.get("head", {}).get("ref", "")
if not ref.startswith("sync/openclaw-"):
continue
if ref == branch:
print("bump PR for this version already open — skipping"); sys.exit(0)
num = p.get("number")
req("POST", f"/repos/{repo}/issues/{num}/comments", {
"body": f"Superseded by a newer upstream release ({latest}); closing so the daily sync can surface it.",
})
st, _ = req("PATCH", f"/repos/{repo}/pulls/{num}", {"state": "closed"})
print(f"closed stale bump PR #{num} ({ref}) -> {st}")
req("DELETE", f"/repos/{repo}/branches/{ref}")
st, meta = req("GET", f"/repos/{repo}/contents/Dockerfile?ref=main")
assert st == 200, f"contents GET {st}"
text = base64.b64decode(meta["content"]).decode()
needle = f"ARG OPENCLAW_VERSION={current}"
assert text.count(needle) == 1, "pin line not found exactly once"
text = text.replace(needle, f"ARG OPENCLAW_VERSION={latest}")
st, _ = req("PUT", f"/repos/{repo}/contents/Dockerfile", {
"message": f"chore(sync): bump openclaw {current} -> {latest} (daily upstream sync)",
"content": base64.b64encode(text.encode()).decode(),
"sha": meta["sha"], "branch": "main", "new_branch": branch,
})
assert st in (200, 201), f"contents PUT {st}"
st, pr = req("POST", f"/repos/{repo}/pulls", {
"title": f"chore(sync): bump openclaw {current} -> {latest}",
"head": branch, "base": "main",
"body": ("Automated daily upstream sync: npm reports openclaw "
f"{latest} (pinned: {current}). Per-PR CI gates the "
"merge as usual."),
})
assert st in (200, 201), f"pulls POST {st}"
print(f"opened bump PR #{pr.get('number')} for openclaw {latest}")
PYEOF