From 84a20ef3dbf303b0ba0a0144f69613070f46cffa Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Tue, 11 Aug 2026 17:31:30 +0800 Subject: [PATCH] ci(release): give the bindings a release trigger after a core release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wren-core-py and wren-core-wasm depend on the Rust core by path alone, and release-please attributes commits to packages by file path. An engine change under core/wren-core is therefore attributed to wren-semantic-core and never to the bindings: no release PR is opened for them, and the core release commit is a chore commit release-please skips by design, so it cannot be the trigger either. The engine change reaches crates.io and never reaches PyPI or npm. Move the binding lockfile refresh out of the release commit — where release-please performed it through extra-files — into a workflow that runs after the core release. It relocks both bindings against the released crates and opens a PR whose fix commit touches both binding directories, which is exactly what release-please needs in order to open their release PRs. This reverses the direction of "ci(core): sync downstream lockfile during Rust releases" deliberately. Doing the sync inside the release commit keeps the tree tidy, but it also means the only commit that records the new engine version is one release-please skips — so the bindings can never release. No workflow builds the bindings with cargo --locked, so the lag between the release commit and the sync PR breaks no job, and the bindings' release tags are cut after the sync PR merges, so a published wheel still ships a synced lockfile. Verified against a simulated 0.3.1 -> 0.4.0 core release: the refresh step rewrites exactly the three linked crates in both lockfiles and nothing else, and the assertion step passes on the refreshed locks while failing on a stale one. --- .../workflows/release-bindings-after-core.yml | 171 ++++++++++++++++++ .github/workflows/release-please.yml | 16 ++ release-please-config.json | 34 +--- 3 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/release-bindings-after-core.yml diff --git a/.github/workflows/release-bindings-after-core.yml b/.github/workflows/release-bindings-after-core.yml new file mode 100644 index 0000000000..e27ce6e309 --- /dev/null +++ b/.github/workflows/release-bindings-after-core.yml @@ -0,0 +1,171 @@ +name: Release bindings after a core release + +# wren-core-py and wren-core-wasm depend on the Rust core by path alone, and +# release-please attributes commits to packages by file path. An engine change +# under core/wren-core is therefore attributed to wren-semantic-core and never +# to the bindings, so no release PR is ever opened for them — and the release +# commit that bumps the core crates is a chore commit release-please skips by +# design, so it cannot serve as the trigger either. +# +# This workflow closes that gap. After the core crates release, it refreshes the +# two binding lockfiles against the new core version and opens a PR whose commit +# touches both binding directories. Merging that PR is what gives the bindings a +# release line of their own. +# +# The lockfile refresh used to happen inside the release commit (release-please +# extra-files). It was moved here deliberately: anything the release commit +# writes cannot trigger the next release. No workflow builds the bindings with +# `cargo --locked`, so the lag between the release commit and this PR breaks +# nothing, and the bindings' release tags are cut after this PR merges, so a +# published wheel still ships a synced lockfile. + +on: + workflow_call: + inputs: + version: + description: "Released wren-core version (e.g. 0.3.1)" + required: true + type: string + # Manual fallback: re-run if the automatic run failed or was skipped. + # Leave version blank to use the release tracked in the repo. + workflow_dispatch: + inputs: + version: + description: "wren-core version to sync to; blank = .release-please-manifest.json" + required: false + type: string + +permissions: + contents: write + pull-requests: write + +# Serialize runs so two can't push the same branch at once. Constant key (not +# per-version) because a blank-dispatch version is only known at run time. +concurrency: + group: release-bindings-after-core + cancel-in-progress: false + +jobs: + sync-and-open-pr: + if: ${{ github.repository == 'Canner/WrenAI' }} + runs-on: ubuntu-latest + steps: + - name: Checkout main + uses: actions/checkout@v4 + with: + # The release job takes minutes; main may have moved. Base the PR on + # main tip. + ref: main + # Keep the write token out of .git/config; the push re-authenticates. + persist-credentials: false + + - name: Resolve and validate version + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + # workflow_call always passes the released version; manual runs may + # omit it and fall back to the release tracked in the repo. + version="${INPUT_VERSION:-$(jq -r '."core/wren-core"' .release-please-manifest.json)}" + # Guard a malformed version out of the assertion, branch name, and PR. + if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Unsupported version: ${version}. Expected X.Y.Z." + exit 1 + fi + echo "Syncing bindings to wren-core ${version}" + echo "VERSION=${version}" >> "$GITHUB_ENV" + + - name: Refresh binding lockfiles + run: | + for dir in core/wren-core-py core/wren-core-wasm; do + # Re-resolve against the bumped path crates. cargo rewrites only + # what must change, so registry dependencies keep their locked + # versions and the diff stays limited to the core crates. + cargo metadata --manifest-path "${dir}/Cargo.toml" --format-version 1 >/dev/null + done + + - name: Assert the lockfiles record the released version + env: + RELEASED_VERSION: ${{ env.VERSION }} + shell: python + run: | + import os, sys, tomllib + + released = os.environ["RELEASED_VERSION"] + # The three crates are linked to a single version by release-please, + # so every one of them must read as the version just released. + tracked = {"wren-semantic-core", "wren-core-base", "wren-manifest-macro"} + failed = False + for path in ("core/wren-core-py/Cargo.lock", "core/wren-core-wasm/Cargo.lock"): + with open(path, "rb") as handle: + packages = tomllib.load(handle)["package"] + found = {p["name"]: p["version"] for p in packages if p["name"] in tracked} + missing = tracked - found.keys() + if missing: + print(f"::error::{path} is missing {', '.join(sorted(missing))}") + failed = True + for name, version in sorted(found.items()): + if version != released: + print(f"::error::{path}: {name} is {version}, expected {released}") + failed = True + sys.exit(1 if failed else 0) + + - name: Detect changes + id: diff + run: | + if git diff --quiet -- core/wren-core-py/Cargo.lock core/wren-core-wasm/Cargo.lock; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Report a no-op + if: steps.diff.outputs.changed == 'false' + run: | + echo "::warning::Binding lockfiles already record wren-core ${VERSION};" \ + "no commit to open, so no binding release will be triggered." + + # Opened with GITHUB_TOKEN, like sync-wren-core-py-lock.yml. GitHub puts + # the pull_request runs of a token-created PR in an approval-required + # state, so whoever reviews it also clicks "Approve and run workflows" + # once. A PAT or App installation token would remove that click at the + # cost of a long-lived credential in the repo; the click is cheaper. + - name: Open the binding release PR + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + BRANCH="fix/release-bindings-wren-core-${VERSION}" + TITLE="fix(bindings): build against wren-core ${VERSION}" + # If a PR is already open for this bump, leave it untouched. + if [ "$(gh pr list --repo "${GITHUB_REPOSITORY}" --state open --head "${BRANCH}" --json number --jq 'length')" != "0" ]; then + echo "Open PR for ${BRANCH} already exists; leaving it untouched." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "${BRANCH}" + git add core/wren-core-py/Cargo.lock core/wren-core-wasm/Cargo.lock + git commit -m "${TITLE}" + # No open PR: --force only overwrites a leftover branch from a closed + # PR. Auth the push explicitly since credentials aren't persisted. + git push --force \ + "https://x-access-token:${GH_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" \ + "${BRANCH}" + cat > "${RUNNER_TEMP}/pr-body.md" <