From 0204fedbbf8d148d51b96a3625cbb34a8a82346d Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Tue, 21 Jul 2026 11:28:09 -0600 Subject: [PATCH 1/3] Workflow to finalize Wasmtime upgrade Signed-off-by: Brian Hardock --- .github/workflows/finalize-wasmtime.yml | 269 ++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 .github/workflows/finalize-wasmtime.yml diff --git a/.github/workflows/finalize-wasmtime.yml b/.github/workflows/finalize-wasmtime.yml new file mode 100644 index 0000000000..1d132b9879 --- /dev/null +++ b/.github/workflows/finalize-wasmtime.yml @@ -0,0 +1,269 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +# Finalizes an in-flight "Bump wasmtime (prerelease)" PR once the targeted +# wasmtime version has been published to crates.io. +# +# It complements bump-wasmtime.yml: that workflow points Spin at a wasmtime +# release *branch* (a git dependency) so integration can begin before wasmtime +# ships. This workflow watches crates.io and, once the release is published: +# 1. Rewrites the bump PR's git/branch dependency back to the crates.io +# version and pushes it to the PR branch. +# 2. Backports the entire bump PR onto the last Spin release branch and opens +# a PR that also bumps the patch version, ready to be tagged as a release. +name: Finalize wasmtime release +on: + schedule: + - cron: 0 13 * * * # Daily; polls crates.io for the published wasmtime release + workflow_dispatch: + inputs: + pr_number: + description: Bump-wasmtime PR number to finalize (auto-detected if empty) + required: false + default: "" + crates_version: + description: Published wasmtime crates.io version to pin (auto-detected if empty) + required: false + default: "" + release_branch: + description: Spin release branch to backport into, e.g. v4.0 (auto-detected if empty) + required: false + default: "" + +# Serialize runs so overlapping crons don't race on the same branches/PRs +concurrency: ${{ github.workflow }} + +permissions: + contents: write + pull-requests: write + +jobs: + finalize-wasmtime: + name: Finalize wasmtime to crates.io release + runs-on: "ubuntu-22.04" + if: ${{ github.repository == 'spinframework/spin' }} + env: + GH_TOKEN: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + WASMTIME_GIT: https://github.com/bytecodealliance/wasmtime + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + fetch-depth: 0 + + - name: Discover bump PR, published version, and release branch + id: discover + shell: bash + env: + INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} + INPUT_CRATES_VERSION: ${{ github.event.inputs.crates_version }} + INPUT_RELEASE_BRANCH: ${{ github.event.inputs.release_branch }} + run: | + set -eu + + # --- Locate the open bump-wasmtime PR (or use the dispatch override) --- + pr_number="${INPUT_PR_NUMBER:-}" + pr_branch="" + if [ -n "${pr_number}" ]; then + pr_branch=$(gh pr view "${pr_number}" --json headRefName --jq '.headRefName') + else + # NOTE: assumes at most one open bump PR (branch sort is lexical). + prs=$(gh pr list --state open --json number,headRefName \ + --jq 'map(select(.headRefName | startswith("bump-wasmtime/prerelease-")))') + pr_number=$(echo "${prs}" | jq -r 'sort_by(.headRefName) | last | .number // empty') + pr_branch=$(echo "${prs}" | jq -r 'sort_by(.headRefName) | last | .headRefName // empty') + fi + + if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then + echo "No open bump-wasmtime PR found; nothing to finalize." + echo "proceed=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + echo "Found bump PR #${pr_number} (${pr_branch})" + + # --- Derive the wasmtime major version from the PR branch name --- + prerelease_version="${pr_branch#bump-wasmtime/prerelease-}" + major="${prerelease_version%%.*}" + echo "Prerelease targets wasmtime ${prerelease_version} (major ${major})" + + # --- Determine the published crates.io version (highest stable major.*) --- + crates_version="${INPUT_CRATES_VERSION:-}" + if [ -z "${crates_version}" ]; then + crates_json=$(curl -sSL -H 'User-Agent: spinframework-ci (spin@spinframework.dev)' \ + "https://crates.io/api/v1/crates/wasmtime") + crates_version=$(echo "${crates_json}" \ + | jq -r '.versions[] | select(.yanked==false) | .num' \ + | grep -E "^${major}\.[0-9]+\.[0-9]+$" \ + | sort -V | tail -n1) + fi + + if [ -z "${crates_version}" ]; then + echo "wasmtime ${major}.x is not published on crates.io yet; will retry on the next run." + echo "proceed=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + echo "Published wasmtime crates.io version: ${crates_version}" + + # --- Determine the last Spin release branch (highest vX.Y) --- + release_branch="${INPUT_RELEASE_BRANCH:-}" + if [ -z "${release_branch}" ]; then + release_branch=$(git ls-remote --heads origin 'v[0-9]*.[0-9]*' \ + | awk '{print $2}' | sed 's#refs/heads/##' \ + | grep -E '^v[0-9]+\.[0-9]+$' | sort -V | tail -n1) + fi + if [ -z "${release_branch}" ]; then + echo "Could not determine a Spin release branch." + echo "proceed=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + echo "Last Spin release branch: ${release_branch}" + + # --- Compute the next patch version from the release branch --- + current_version=$(git show "origin/${release_branch}:Cargo.toml" \ + | grep -E '^version = "' | head -n1 | sed -E 's/version = "([^"]+)"/\1/') + if [ -z "${current_version}" ]; then + echo "Could not read the version from origin/${release_branch}:Cargo.toml." + echo "proceed=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + major_s="${current_version%%.*}" + rest="${current_version#*.}" + minor_s="${rest%%.*}" + patch_s="${rest#*.}" + patch_num="${patch_s%%[-+]*}" # strip any pre-release/build suffix + next_version="${major_s}.${minor_s}.$((patch_num + 1))" + echo "Release branch is at ${current_version}; patch release will be ${next_version}" + + backport_branch="backport-wasmtime-${crates_version}-to-${release_branch}" + backport_exists=false + if git ls-remote --exit-code --heads origin "${backport_branch}" >/dev/null 2>&1; then + backport_exists=true + fi + + { + echo "proceed=true" + echo "pr_number=${pr_number}" + echo "pr_branch=${pr_branch}" + echo "crates_version=${crates_version}" + echo "release_branch=${release_branch}" + echo "next_version=${next_version}" + echo "backport_branch=${backport_branch}" + echo "backport_exists=${backport_exists}" + } >>"$GITHUB_OUTPUT" + + # Import GPG key for signing commits (matches bump-wasmtime.yml) + - name: Import GPG key + if: steps.discover.outputs.proceed == 'true' + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - name: Point bump PR at the crates.io release + if: steps.discover.outputs.proceed == 'true' + shell: bash + env: + PR_BRANCH: ${{ steps.discover.outputs.pr_branch }} + CRATES_VERSION: ${{ steps.discover.outputs.crates_version }} + run: | + set -eu + git fetch origin "${PR_BRANCH}" + git checkout -B "${PR_BRANCH}" "origin/${PR_BRANCH}" + + if ! grep -q 'bytecodealliance/wasmtime' Cargo.toml examples/spin-timer/Cargo.toml; then + echo "Bump PR already points at a crates.io release; skipping swap." + exit 0 + fi + + # Replace the git/branch reference with the published crates.io version. + # This is the inverse of the rewrite performed by bump-wasmtime.yml. + sed -i -E \ + "s#git = \"${WASMTIME_GIT}\", branch = \"release-[0-9][0-9.]*\"#version = \"${CRATES_VERSION}\"#g" \ + Cargo.toml examples/spin-timer/Cargo.toml + # Collapse a bare wasmtime table back to the plain-version form + sed -i -E \ + "s#^wasmtime = \{ version = \"${CRATES_VERSION}\" \}#wasmtime = \"${CRATES_VERSION}\"#" \ + Cargo.toml + + if grep -q 'bytecodealliance/wasmtime' Cargo.toml examples/spin-timer/Cargo.toml; then + echo "::error::Some wasmtime git references remain after the rewrite; the Cargo.toml format may have changed. Update the dependency manually." + exit 1 + fi + + cargo update + cargo update --manifest-path examples/spin-timer/Cargo.toml + + git commit -a -m "Point wasmtime at crates.io v${CRATES_VERSION}" + git push origin "HEAD:${PR_BRANCH}" + + - name: Backport and prepare the patch release + if: ${{ steps.discover.outputs.proceed == 'true' && steps.discover.outputs.backport_exists == 'false' }} + shell: bash + env: + PR_NUMBER: ${{ steps.discover.outputs.pr_number }} + CRATES_VERSION: ${{ steps.discover.outputs.crates_version }} + RELEASE_BRANCH: ${{ steps.discover.outputs.release_branch }} + NEXT_VERSION: ${{ steps.discover.outputs.next_version }} + BACKPORT_BRANCH: ${{ steps.discover.outputs.backport_branch }} + run: | + set -eu + + git fetch origin "${RELEASE_BRANCH}" + git fetch origin "refs/pull/${PR_NUMBER}/head" + git checkout -B "${BACKPORT_BRANCH}" "origin/${RELEASE_BRANCH}" + + # Cherry-pick every commit from the bump PR onto the release branch + # (same approach as .github/gh-backport.sh, run non-interactively). + commits=$(gh pr view "${PR_NUMBER}" --json commits --jq '.commits[].oid') + if ! git cherry-pick -x ${commits}; then + git cherry-pick --abort || true + echo "::error::Cherry-pick of #${PR_NUMBER} onto ${RELEASE_BRANCH} hit conflicts. Backport manually with .github/gh-backport.sh ${PR_NUMBER} ${RELEASE_BRANCH}" + exit 1 + fi + + # Bump the patch version and refresh the workspace lockfile + sed -i -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+([^\"]*)\"/version = \"${NEXT_VERSION}\"/" Cargo.toml + cargo update -w + git commit -a -m "Bump Spin to ${NEXT_VERSION}" + + git push origin "${BACKPORT_BRANCH}" + + title="[Backport ${RELEASE_BRANCH}] wasmtime v${CRATES_VERSION} + Spin ${NEXT_VERSION} patch release" + body=$(printf '%s\n' \ + "Backports #${PR_NUMBER} to \`${RELEASE_BRANCH}\` and prepares the \`${NEXT_VERSION}\` patch release." \ + "" \ + "- Pins wasmtime to the published crates.io release \`v${CRATES_VERSION}\`." \ + "- Bumps the Spin version to \`${NEXT_VERSION}\`." \ + "" \ + "Once merged, tag \`${NEXT_VERSION}\` on \`${RELEASE_BRANCH}\` to trigger the Release workflow." \ + "" \ + "> [!WARNING]" \ + "> This backports the entire wasmtime bump. Confirm that shipping these changes as a patch release is appropriate before merging.") + gh pr create \ + --base "${RELEASE_BRANCH}" \ + --head "${BACKPORT_BRANCH}" \ + --title "${title}" \ + --body "${body}" + + - name: Summary + if: steps.discover.outputs.proceed == 'true' + shell: bash + env: + PR_NUMBER: ${{ steps.discover.outputs.pr_number }} + PR_BRANCH: ${{ steps.discover.outputs.pr_branch }} + CRATES_VERSION: ${{ steps.discover.outputs.crates_version }} + RELEASE_BRANCH: ${{ steps.discover.outputs.release_branch }} + NEXT_VERSION: ${{ steps.discover.outputs.next_version }} + BACKPORT_BRANCH: ${{ steps.discover.outputs.backport_branch }} + BACKPORT_EXISTS: ${{ steps.discover.outputs.backport_exists }} + run: | + { + echo "### Finalize wasmtime release" + echo "" + echo "- Bump PR: #${PR_NUMBER} (\`${PR_BRANCH}\`)" + echo "- crates.io version: \`${CRATES_VERSION}\`" + echo "- Release branch: \`${RELEASE_BRANCH}\`" + echo "- Patch release: \`${NEXT_VERSION}\`" + echo "- Backport branch: \`${BACKPORT_BRANCH}\` (already existed: ${BACKPORT_EXISTS})" + } >>"$GITHUB_STEP_SUMMARY" From 3b614e72764cd631f897b2c3bd99b5f8e555cbcd Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Tue, 21 Jul 2026 15:32:14 -0600 Subject: [PATCH 2/3] split up the workflow Signed-off-by: Brian Hardock --- .github/workflows/finalize-wasmtime.yml | 274 +++++------------------- scripts/wasmtime-release.sh | 234 ++++++++++++++++++++ 2 files changed, 287 insertions(+), 221 deletions(-) create mode 100755 scripts/wasmtime-release.sh diff --git a/.github/workflows/finalize-wasmtime.yml b/.github/workflows/finalize-wasmtime.yml index 1d132b9879..68748e17df 100644 --- a/.github/workflows/finalize-wasmtime.yml +++ b/.github/workflows/finalize-wasmtime.yml @@ -1,23 +1,33 @@ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json # Finalizes an in-flight "Bump wasmtime (prerelease)" PR once the targeted -# wasmtime version has been published to crates.io. +# wasmtime version has been published to crates.io. Complements bump-wasmtime.yml, +# which points Spin at a wasmtime release *branch* (a git dependency) so +# integration can begin before wasmtime ships. # -# It complements bump-wasmtime.yml: that workflow points Spin at a wasmtime -# release *branch* (a git dependency) so integration can begin before wasmtime -# ships. This workflow watches crates.io and, once the release is published: -# 1. Rewrites the bump PR's git/branch dependency back to the crates.io -# version and pushes it to the PR branch. -# 2. Backports the entire bump PR onto the last Spin release branch and opens -# a PR that also bumps the patch version, ready to be tagged as a release. +# Run in two deliberate phases (implemented in ./scripts/wasmtime-release.sh): +# action: update Rewrite the open bump PR's git dependency to the published +# crates.io version and push it to the PR branch. A human +# reviews/amends and merges that PR. +# action: backport After the bump PR has merged, backport it onto the last +# Spin release branch, bump the patch version, and open a +# release PR. +# +# NOTE: Manual dispatch only for now; automation can be added once we trust it. name: Finalize wasmtime release on: - schedule: - - cron: 0 13 * * * # Daily; polls crates.io for the published wasmtime release workflow_dispatch: inputs: + action: + description: Which phase to run + type: choice + required: true + default: update + options: + - update + - backport pr_number: - description: Bump-wasmtime PR number to finalize (auto-detected if empty) + description: Bump-wasmtime PR number (auto-detected if empty) required: false default: "" crates_version: @@ -29,7 +39,7 @@ on: required: false default: "" -# Serialize runs so overlapping crons don't race on the same branches/PRs +# Serialize runs so a dispatch can't race with a previous one concurrency: ${{ github.workflow }} permissions: @@ -37,122 +47,22 @@ permissions: pull-requests: write jobs: - finalize-wasmtime: - name: Finalize wasmtime to crates.io release + update: + name: Update bump PR to crates.io release + if: ${{ github.repository == 'spinframework/spin' && github.event.inputs.action == 'update' }} runs-on: "ubuntu-22.04" - if: ${{ github.repository == 'spinframework/spin' }} env: GH_TOKEN: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} - WASMTIME_GIT: https://github.com/bytecodealliance/wasmtime + PR_NUMBER: ${{ github.event.inputs.pr_number }} + CRATES_VERSION: ${{ github.event.inputs.crates_version }} steps: - uses: actions/checkout@v6 with: token: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} fetch-depth: 0 - - name: Discover bump PR, published version, and release branch - id: discover - shell: bash - env: - INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} - INPUT_CRATES_VERSION: ${{ github.event.inputs.crates_version }} - INPUT_RELEASE_BRANCH: ${{ github.event.inputs.release_branch }} - run: | - set -eu - - # --- Locate the open bump-wasmtime PR (or use the dispatch override) --- - pr_number="${INPUT_PR_NUMBER:-}" - pr_branch="" - if [ -n "${pr_number}" ]; then - pr_branch=$(gh pr view "${pr_number}" --json headRefName --jq '.headRefName') - else - # NOTE: assumes at most one open bump PR (branch sort is lexical). - prs=$(gh pr list --state open --json number,headRefName \ - --jq 'map(select(.headRefName | startswith("bump-wasmtime/prerelease-")))') - pr_number=$(echo "${prs}" | jq -r 'sort_by(.headRefName) | last | .number // empty') - pr_branch=$(echo "${prs}" | jq -r 'sort_by(.headRefName) | last | .headRefName // empty') - fi - - if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then - echo "No open bump-wasmtime PR found; nothing to finalize." - echo "proceed=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - echo "Found bump PR #${pr_number} (${pr_branch})" - - # --- Derive the wasmtime major version from the PR branch name --- - prerelease_version="${pr_branch#bump-wasmtime/prerelease-}" - major="${prerelease_version%%.*}" - echo "Prerelease targets wasmtime ${prerelease_version} (major ${major})" - - # --- Determine the published crates.io version (highest stable major.*) --- - crates_version="${INPUT_CRATES_VERSION:-}" - if [ -z "${crates_version}" ]; then - crates_json=$(curl -sSL -H 'User-Agent: spinframework-ci (spin@spinframework.dev)' \ - "https://crates.io/api/v1/crates/wasmtime") - crates_version=$(echo "${crates_json}" \ - | jq -r '.versions[] | select(.yanked==false) | .num' \ - | grep -E "^${major}\.[0-9]+\.[0-9]+$" \ - | sort -V | tail -n1) - fi - - if [ -z "${crates_version}" ]; then - echo "wasmtime ${major}.x is not published on crates.io yet; will retry on the next run." - echo "proceed=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - echo "Published wasmtime crates.io version: ${crates_version}" - - # --- Determine the last Spin release branch (highest vX.Y) --- - release_branch="${INPUT_RELEASE_BRANCH:-}" - if [ -z "${release_branch}" ]; then - release_branch=$(git ls-remote --heads origin 'v[0-9]*.[0-9]*' \ - | awk '{print $2}' | sed 's#refs/heads/##' \ - | grep -E '^v[0-9]+\.[0-9]+$' | sort -V | tail -n1) - fi - if [ -z "${release_branch}" ]; then - echo "Could not determine a Spin release branch." - echo "proceed=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - echo "Last Spin release branch: ${release_branch}" - - # --- Compute the next patch version from the release branch --- - current_version=$(git show "origin/${release_branch}:Cargo.toml" \ - | grep -E '^version = "' | head -n1 | sed -E 's/version = "([^"]+)"/\1/') - if [ -z "${current_version}" ]; then - echo "Could not read the version from origin/${release_branch}:Cargo.toml." - echo "proceed=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - major_s="${current_version%%.*}" - rest="${current_version#*.}" - minor_s="${rest%%.*}" - patch_s="${rest#*.}" - patch_num="${patch_s%%[-+]*}" # strip any pre-release/build suffix - next_version="${major_s}.${minor_s}.$((patch_num + 1))" - echo "Release branch is at ${current_version}; patch release will be ${next_version}" - - backport_branch="backport-wasmtime-${crates_version}-to-${release_branch}" - backport_exists=false - if git ls-remote --exit-code --heads origin "${backport_branch}" >/dev/null 2>&1; then - backport_exists=true - fi - - { - echo "proceed=true" - echo "pr_number=${pr_number}" - echo "pr_branch=${pr_branch}" - echo "crates_version=${crates_version}" - echo "release_branch=${release_branch}" - echo "next_version=${next_version}" - echo "backport_branch=${backport_branch}" - echo "backport_exists=${backport_exists}" - } >>"$GITHUB_OUTPUT" - # Import GPG key for signing commits (matches bump-wasmtime.yml) - name: Import GPG key - if: steps.discover.outputs.proceed == 'true' uses: crazy-max/ghaction-import-gpg@v6 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} @@ -160,110 +70,32 @@ jobs: git_user_signingkey: true git_commit_gpgsign: true - - name: Point bump PR at the crates.io release - if: steps.discover.outputs.proceed == 'true' - shell: bash - env: - PR_BRANCH: ${{ steps.discover.outputs.pr_branch }} - CRATES_VERSION: ${{ steps.discover.outputs.crates_version }} - run: | - set -eu - git fetch origin "${PR_BRANCH}" - git checkout -B "${PR_BRANCH}" "origin/${PR_BRANCH}" - - if ! grep -q 'bytecodealliance/wasmtime' Cargo.toml examples/spin-timer/Cargo.toml; then - echo "Bump PR already points at a crates.io release; skipping swap." - exit 0 - fi - - # Replace the git/branch reference with the published crates.io version. - # This is the inverse of the rewrite performed by bump-wasmtime.yml. - sed -i -E \ - "s#git = \"${WASMTIME_GIT}\", branch = \"release-[0-9][0-9.]*\"#version = \"${CRATES_VERSION}\"#g" \ - Cargo.toml examples/spin-timer/Cargo.toml - # Collapse a bare wasmtime table back to the plain-version form - sed -i -E \ - "s#^wasmtime = \{ version = \"${CRATES_VERSION}\" \}#wasmtime = \"${CRATES_VERSION}\"#" \ - Cargo.toml - - if grep -q 'bytecodealliance/wasmtime' Cargo.toml examples/spin-timer/Cargo.toml; then - echo "::error::Some wasmtime git references remain after the rewrite; the Cargo.toml format may have changed. Update the dependency manually." - exit 1 - fi - - cargo update - cargo update --manifest-path examples/spin-timer/Cargo.toml - - git commit -a -m "Point wasmtime at crates.io v${CRATES_VERSION}" - git push origin "HEAD:${PR_BRANCH}" - - - name: Backport and prepare the patch release - if: ${{ steps.discover.outputs.proceed == 'true' && steps.discover.outputs.backport_exists == 'false' }} - shell: bash - env: - PR_NUMBER: ${{ steps.discover.outputs.pr_number }} - CRATES_VERSION: ${{ steps.discover.outputs.crates_version }} - RELEASE_BRANCH: ${{ steps.discover.outputs.release_branch }} - NEXT_VERSION: ${{ steps.discover.outputs.next_version }} - BACKPORT_BRANCH: ${{ steps.discover.outputs.backport_branch }} - run: | - set -eu - - git fetch origin "${RELEASE_BRANCH}" - git fetch origin "refs/pull/${PR_NUMBER}/head" - git checkout -B "${BACKPORT_BRANCH}" "origin/${RELEASE_BRANCH}" + - name: Update bump PR to the crates.io release + run: ./scripts/wasmtime-release.sh update - # Cherry-pick every commit from the bump PR onto the release branch - # (same approach as .github/gh-backport.sh, run non-interactively). - commits=$(gh pr view "${PR_NUMBER}" --json commits --jq '.commits[].oid') - if ! git cherry-pick -x ${commits}; then - git cherry-pick --abort || true - echo "::error::Cherry-pick of #${PR_NUMBER} onto ${RELEASE_BRANCH} hit conflicts. Backport manually with .github/gh-backport.sh ${PR_NUMBER} ${RELEASE_BRANCH}" - exit 1 - fi - - # Bump the patch version and refresh the workspace lockfile - sed -i -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+([^\"]*)\"/version = \"${NEXT_VERSION}\"/" Cargo.toml - cargo update -w - git commit -a -m "Bump Spin to ${NEXT_VERSION}" - - git push origin "${BACKPORT_BRANCH}" + backport: + name: Backport bump PR and prepare patch release + if: ${{ github.repository == 'spinframework/spin' && github.event.inputs.action == 'backport' }} + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + PR_NUMBER: ${{ github.event.inputs.pr_number }} + CRATES_VERSION: ${{ github.event.inputs.crates_version }} + RELEASE_BRANCH: ${{ github.event.inputs.release_branch }} + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + fetch-depth: 0 - title="[Backport ${RELEASE_BRANCH}] wasmtime v${CRATES_VERSION} + Spin ${NEXT_VERSION} patch release" - body=$(printf '%s\n' \ - "Backports #${PR_NUMBER} to \`${RELEASE_BRANCH}\` and prepares the \`${NEXT_VERSION}\` patch release." \ - "" \ - "- Pins wasmtime to the published crates.io release \`v${CRATES_VERSION}\`." \ - "- Bumps the Spin version to \`${NEXT_VERSION}\`." \ - "" \ - "Once merged, tag \`${NEXT_VERSION}\` on \`${RELEASE_BRANCH}\` to trigger the Release workflow." \ - "" \ - "> [!WARNING]" \ - "> This backports the entire wasmtime bump. Confirm that shipping these changes as a patch release is appropriate before merging.") - gh pr create \ - --base "${RELEASE_BRANCH}" \ - --head "${BACKPORT_BRANCH}" \ - --title "${title}" \ - --body "${body}" + # Import GPG key for signing commits (matches bump-wasmtime.yml) + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true - - name: Summary - if: steps.discover.outputs.proceed == 'true' - shell: bash - env: - PR_NUMBER: ${{ steps.discover.outputs.pr_number }} - PR_BRANCH: ${{ steps.discover.outputs.pr_branch }} - CRATES_VERSION: ${{ steps.discover.outputs.crates_version }} - RELEASE_BRANCH: ${{ steps.discover.outputs.release_branch }} - NEXT_VERSION: ${{ steps.discover.outputs.next_version }} - BACKPORT_BRANCH: ${{ steps.discover.outputs.backport_branch }} - BACKPORT_EXISTS: ${{ steps.discover.outputs.backport_exists }} - run: | - { - echo "### Finalize wasmtime release" - echo "" - echo "- Bump PR: #${PR_NUMBER} (\`${PR_BRANCH}\`)" - echo "- crates.io version: \`${CRATES_VERSION}\`" - echo "- Release branch: \`${RELEASE_BRANCH}\`" - echo "- Patch release: \`${NEXT_VERSION}\`" - echo "- Backport branch: \`${BACKPORT_BRANCH}\` (already existed: ${BACKPORT_EXISTS})" - } >>"$GITHUB_STEP_SUMMARY" + - name: Backport bump PR and open patch release PR + run: ./scripts/wasmtime-release.sh backport diff --git a/scripts/wasmtime-release.sh b/scripts/wasmtime-release.sh new file mode 100755 index 0000000000..25e085df07 --- /dev/null +++ b/scripts/wasmtime-release.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash + +# Finalize an in-flight "Bump wasmtime (prerelease)" once wasmtime is published +# to crates.io. Complements bump-wasmtime.yml, which points Spin at a wasmtime +# release *branch* (a git dependency) so integration can begin before wasmtime +# ships. +# +# Subcommands: +# update Rewrite the open bump PR's wasmtime git dependency to the published +# crates.io version and push the change to the PR branch. A human then +# reviews (and possibly amends) that PR before it is merged. +# backport After the bump PR has merged to main, backport it onto the last Spin +# release branch, bump the patch version, and open a release PR. +# +# The two phases are intentionally separate so the bump PR can be reviewed and +# merged before the backport/patch release is kicked off. +# +# Configuration via environment (all optional; auto-discovered when unset): +# PR_NUMBER Bump-wasmtime PR number +# CRATES_VERSION Published wasmtime crates.io version to pin +# RELEASE_BRANCH Spin release branch to backport into (e.g. v4.0) +# +# Requires: gh (authenticated), jq, curl, cargo, git, and GNU sed. Commits are +# signed when git is configured with a signing key (as it is in CI). + +set -euo pipefail + +WASMTIME_GIT="https://github.com/bytecodealliance/wasmtime" +WASMTIME_REPO="bytecodealliance/wasmtime" + +log() { echo "[wasmtime-release] $*"; } +die() { + echo "[wasmtime-release] error: $*" >&2 + exit 1 +} + +# Most recently opened bump-wasmtime PR in the given state (open|merged). +# Echoes " ", or nothing if there is no match. +find_bump_pr() { + local state="$1" + gh pr list --state "$state" --json number,headRefName --limit 50 | + jq -r '[.[] | select(.headRefName | startswith("bump-wasmtime/prerelease-"))] + | sort_by(.number) | last + | if . == null then "" else "\(.number) \(.headRefName)" end' +} + +# Wasmtime major version encoded in a "bump-wasmtime/prerelease-" branch. +major_from_branch() { + local version="${1#bump-wasmtime/prerelease-}" + echo "${version%%.*}" +} + +# Highest published, non-yanked, stable major.* wasmtime version on crates.io. +latest_crates_version() { + local major="$1" nums + nums=$(curl -sSL -H 'User-Agent: spinframework-ci (spin@spinframework.dev)' \ + "https://crates.io/api/v1/crates/wasmtime" | + jq -r '.versions[] | select(.yanked==false) | .num') || return 0 + printf '%s\n' "$nums" | grep -E "^${major}\.[0-9]+\.[0-9]+$" | sort -V | tail -n1 || true +} + +# Highest vX.Y Spin release branch on origin. +latest_release_branch() { + git ls-remote --heads origin 'v[0-9]*.[0-9]*' | + awk '{print $2}' | sed 's#refs/heads/##' | + grep -E '^v[0-9]+\.[0-9]+$' | sort -V | tail -n1 || true +} + +# Wasmtime version currently pinned in a ref's workspace Cargo.toml. +wasmtime_version_on_ref() { + local ref="$1" + git show "${ref}:Cargo.toml" | + grep -E '^wasmtime = ' | head -n1 | + grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true +} + +# Next patch version for a release branch, read from its Cargo.toml. +next_patch_version() { + local branch="$1" current + current=$(git show "origin/${branch}:Cargo.toml" | + grep -E '^version = "' | head -n1 | sed -E 's/version = "([^"]+)"/\1/') + [ -n "$current" ] || die "could not read the version from origin/${branch}:Cargo.toml" + local major="${current%%.*}" rest="${current#*.}" + local minor="${rest%%.*}" patch="${rest#*.}" + patch="${patch%%[-+]*}" # strip any pre-release/build suffix + echo "${major}.${minor}.$((patch + 1))" +} + +# Rewrite wasmtime git/branch dependencies back to a crates.io version. This is +# the inverse of the rewrite performed by bump-wasmtime.yml. +rewrite_to_crates_io() { + local version="$1" + shift + sed -i -E \ + "s#git = \"${WASMTIME_GIT}\", branch = \"release-[0-9][0-9.]*\"#version = \"${version}\"#g" \ + "$@" + # Collapse a bare wasmtime table back to the plain-version form. + sed -i -E "s#^wasmtime = \{ version = \"${version}\" \}#wasmtime = \"${version}\"#" Cargo.toml +} + +cmd_update() { + local pr_number="${PR_NUMBER:-}" pr_branch="" crates_version="${CRATES_VERSION:-}" + + if [ -n "$pr_number" ]; then + pr_branch=$(gh pr view "$pr_number" --json headRefName --jq '.headRefName') + else + read -r pr_number pr_branch <<<"$(find_bump_pr open)" || true + fi + if [ -z "$pr_number" ] || [ -z "$pr_branch" ]; then + die "no open bump-wasmtime PR found" + fi + log "Finalizing bump PR #${pr_number} (${pr_branch})" + + if [ -z "$crates_version" ]; then + crates_version=$(latest_crates_version "$(major_from_branch "$pr_branch")") + fi + [ -n "$crates_version" ] || die "wasmtime is not published on crates.io yet" + log "Published crates.io version: ${crates_version}" + + git fetch origin "$pr_branch" + git checkout -B "$pr_branch" "origin/${pr_branch}" + + if ! grep -q "$WASMTIME_REPO" Cargo.toml examples/spin-timer/Cargo.toml; then + log "Bump PR already points at a crates.io release; nothing to do." + return 0 + fi + + rewrite_to_crates_io "$crates_version" Cargo.toml examples/spin-timer/Cargo.toml + + if grep -q "$WASMTIME_REPO" Cargo.toml examples/spin-timer/Cargo.toml; then + die "wasmtime git references remain after the rewrite; the Cargo.toml format may have changed" + fi + + cargo update + cargo update --manifest-path examples/spin-timer/Cargo.toml + + git commit -a -m "Update wasmtime to v${crates_version}" + git push origin "HEAD:${pr_branch}" + log "Pushed crates.io update to ${pr_branch}" +} + +cmd_backport() { + local pr_number="${PR_NUMBER:-}" pr_branch="" crates_version="${CRATES_VERSION:-}" + local release_branch="${RELEASE_BRANCH:-}" + + if [ -n "$pr_number" ]; then + pr_branch=$(gh pr view "$pr_number" --json headRefName --jq '.headRefName') + else + # Most recent bump PR regardless of state; the merge check below rejects it + # if it hasn't landed yet, so we never backport a stale one. + read -r pr_number pr_branch <<<"$(find_bump_pr all)" || true + fi + if [ -z "$pr_number" ] || [ -z "$pr_branch" ]; then + die "no bump-wasmtime PR found" + fi + + # Backport what actually landed on main, so only run once the PR has merged. + local merge_commit + merge_commit=$(gh pr view "$pr_number" --json mergeCommit --jq '.mergeCommit.oid // empty') + [ -n "$merge_commit" ] || die "PR #${pr_number} has not been merged yet" + log "Backporting merged bump PR #${pr_number} (merge commit ${merge_commit})" + + # Read the wasmtime version that actually landed on main (what we're backporting). + git fetch origin main + if [ -z "$crates_version" ]; then + crates_version=$(wasmtime_version_on_ref origin/main) + fi + [ -n "$crates_version" ] || die "could not read the wasmtime version pinned on main" + + if [ -z "$release_branch" ]; then + release_branch=$(latest_release_branch) + fi + [ -n "$release_branch" ] || die "could not determine a Spin release branch" + git fetch origin "$release_branch" + + local next_version + next_version=$(next_patch_version "$release_branch") + log "Release branch ${release_branch}; patch release will be ${next_version}" + + local backport_branch="backport-wasmtime-${crates_version}-to-${release_branch}" + if git ls-remote --exit-code --heads origin "$backport_branch" >/dev/null 2>&1; then + log "Backport branch ${backport_branch} already exists; nothing to do." + return 0 + fi + + git checkout -B "$backport_branch" "origin/${release_branch}" + + # Cherry-pick the PR as it landed on main. Spin uses both squash and merge + # commits, so add -m 1 when the merge point has more than one parent. + local cherry_pick=(git cherry-pick -x) + if [ "$(git rev-list --parents -n1 "$merge_commit" | wc -w)" -ge 3 ]; then + cherry_pick+=(-m 1) + fi + if ! "${cherry_pick[@]}" "$merge_commit"; then + git cherry-pick --abort || true + die "cherry-pick of #${pr_number} onto ${release_branch} hit conflicts; backport manually with .github/gh-backport.sh ${pr_number} ${release_branch}" + fi + + # Bump the patch version and refresh the workspace lockfile. + sed -i -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+([^\"]*)\"/version = \"${next_version}\"/" Cargo.toml + cargo update -w + git commit -a -m "Bump Spin to ${next_version}" + + git push origin "$backport_branch" + + local title body + title="[Backport ${release_branch}] wasmtime v${crates_version} + Spin ${next_version} patch release" + body=$(printf '%s\n' \ + "Backports #${pr_number} to \`${release_branch}\` and prepares the \`${next_version}\` patch release." \ + "" \ + "- Pins wasmtime to the published crates.io release \`v${crates_version}\`." \ + "- Bumps the Spin version to \`${next_version}\`." \ + "" \ + "Once merged, tag \`${next_version}\` on \`${release_branch}\` to trigger the Release workflow." \ + "" \ + "> [!WARNING]" \ + "> This backports the entire wasmtime bump. Confirm that shipping these changes as a patch release is appropriate before merging.") + gh pr create --base "$release_branch" --head "$backport_branch" --title "$title" --body "$body" + log "Opened backport/patch PR into ${release_branch}" +} + +case "${1:-}" in + update) + shift + cmd_update "$@" + ;; + backport) + shift + cmd_backport "$@" + ;; + *) + die "usage: $0 {update|backport}" + ;; +esac From b4d36e622e6f464242ff88eb450fa539b82c677f Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Wed, 22 Jul 2026 15:48:14 -0600 Subject: [PATCH 3/3] 2 workflows -- update & backport Signed-off-by: Brian Hardock --- .github/workflows/finalize-wasmtime.yml | 101 ------------------------ .github/workflows/wasmtime-backport.yml | 56 +++++++++++++ .github/workflows/wasmtime-update.yml | 57 +++++++++++++ 3 files changed, 113 insertions(+), 101 deletions(-) delete mode 100644 .github/workflows/finalize-wasmtime.yml create mode 100644 .github/workflows/wasmtime-backport.yml create mode 100644 .github/workflows/wasmtime-update.yml diff --git a/.github/workflows/finalize-wasmtime.yml b/.github/workflows/finalize-wasmtime.yml deleted file mode 100644 index 68748e17df..0000000000 --- a/.github/workflows/finalize-wasmtime.yml +++ /dev/null @@ -1,101 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json - -# Finalizes an in-flight "Bump wasmtime (prerelease)" PR once the targeted -# wasmtime version has been published to crates.io. Complements bump-wasmtime.yml, -# which points Spin at a wasmtime release *branch* (a git dependency) so -# integration can begin before wasmtime ships. -# -# Run in two deliberate phases (implemented in ./scripts/wasmtime-release.sh): -# action: update Rewrite the open bump PR's git dependency to the published -# crates.io version and push it to the PR branch. A human -# reviews/amends and merges that PR. -# action: backport After the bump PR has merged, backport it onto the last -# Spin release branch, bump the patch version, and open a -# release PR. -# -# NOTE: Manual dispatch only for now; automation can be added once we trust it. -name: Finalize wasmtime release -on: - workflow_dispatch: - inputs: - action: - description: Which phase to run - type: choice - required: true - default: update - options: - - update - - backport - pr_number: - description: Bump-wasmtime PR number (auto-detected if empty) - required: false - default: "" - crates_version: - description: Published wasmtime crates.io version to pin (auto-detected if empty) - required: false - default: "" - release_branch: - description: Spin release branch to backport into, e.g. v4.0 (auto-detected if empty) - required: false - default: "" - -# Serialize runs so a dispatch can't race with a previous one -concurrency: ${{ github.workflow }} - -permissions: - contents: write - pull-requests: write - -jobs: - update: - name: Update bump PR to crates.io release - if: ${{ github.repository == 'spinframework/spin' && github.event.inputs.action == 'update' }} - runs-on: "ubuntu-22.04" - env: - GH_TOKEN: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} - PR_NUMBER: ${{ github.event.inputs.pr_number }} - CRATES_VERSION: ${{ github.event.inputs.crates_version }} - steps: - - uses: actions/checkout@v6 - with: - token: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} - fetch-depth: 0 - - # Import GPG key for signing commits (matches bump-wasmtime.yml) - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - - - name: Update bump PR to the crates.io release - run: ./scripts/wasmtime-release.sh update - - backport: - name: Backport bump PR and prepare patch release - if: ${{ github.repository == 'spinframework/spin' && github.event.inputs.action == 'backport' }} - runs-on: "ubuntu-22.04" - env: - GH_TOKEN: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} - PR_NUMBER: ${{ github.event.inputs.pr_number }} - CRATES_VERSION: ${{ github.event.inputs.crates_version }} - RELEASE_BRANCH: ${{ github.event.inputs.release_branch }} - steps: - - uses: actions/checkout@v6 - with: - token: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} - fetch-depth: 0 - - # Import GPG key for signing commits (matches bump-wasmtime.yml) - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - - - name: Backport bump PR and open patch release PR - run: ./scripts/wasmtime-release.sh backport diff --git a/.github/workflows/wasmtime-backport.yml b/.github/workflows/wasmtime-backport.yml new file mode 100644 index 0000000000..8f36a48853 --- /dev/null +++ b/.github/workflows/wasmtime-backport.yml @@ -0,0 +1,56 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +# Phase 2 of finalizing a wasmtime upgrade: after the updated bump PR has merged +# to main, backport it onto the last Spin release branch and open a PR that also +# bumps the patch version. Once that PR merges, tag the release to cut it. +# +# Phase 1 (point the bump PR at crates.io) lives in wasmtime-update.yml. +# Both phases are implemented in ./scripts/wasmtime-release.sh. +# +# NOTE: Manual dispatch only for now; automation can be added once we trust it. +name: Backport wasmtime bump PR +on: + workflow_dispatch: + inputs: + pr_number: + description: Bump-wasmtime PR number to backport (auto-detected if empty) + required: false + default: "" + release_branch: + description: Spin release branch to backport into, e.g. v4.0 (auto-detected if empty) + required: false + default: "" + +# Serialize runs so a dispatch can't race with a previous one +concurrency: ${{ github.workflow }} + +permissions: + contents: write + pull-requests: write + +jobs: + backport: + name: Backport bump PR and prepare patch release + if: ${{ github.repository == 'spinframework/spin' }} + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + PR_NUMBER: ${{ github.event.inputs.pr_number }} + RELEASE_BRANCH: ${{ github.event.inputs.release_branch }} + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + fetch-depth: 0 + + # Import GPG key for signing commits (matches bump-wasmtime.yml) + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - name: Backport bump PR and open patch release PR + run: ./scripts/wasmtime-release.sh backport diff --git a/.github/workflows/wasmtime-update.yml b/.github/workflows/wasmtime-update.yml new file mode 100644 index 0000000000..d40c442b61 --- /dev/null +++ b/.github/workflows/wasmtime-update.yml @@ -0,0 +1,57 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +# Phase 1 of finalizing a wasmtime upgrade: point the in-flight +# "Bump wasmtime (prerelease)" PR at the published crates.io release, replacing +# the temporary git dependency. Run this once wasmtime has shipped the version +# the bump PR targets; a human then reviews and merges the PR. +# +# Phase 2 (backport + patch release) lives in wasmtime-backport.yml. +# Both phases are implemented in ./scripts/wasmtime-release.sh. +# +# NOTE: Manual dispatch only for now; automation can be added once we trust it. +name: Update wasmtime bump PR +on: + workflow_dispatch: + inputs: + pr_number: + description: Bump-wasmtime PR number (auto-detected if empty) + required: false + default: "" + crates_version: + description: Published wasmtime crates.io version to pin (auto-detected if empty) + required: false + default: "" + +# Serialize runs so a dispatch can't race with a previous one +concurrency: ${{ github.workflow }} + +permissions: + contents: write + pull-requests: write + +jobs: + update: + name: Update bump PR to crates.io release + if: ${{ github.repository == 'spinframework/spin' }} + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + PR_NUMBER: ${{ github.event.inputs.pr_number }} + CRATES_VERSION: ${{ github.event.inputs.crates_version }} + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.SPINFRAMEWORKBOT_PR_PAT }} + fetch-depth: 0 + + # Import GPG key for signing commits (matches bump-wasmtime.yml) + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - name: Update bump PR to the crates.io release + run: ./scripts/wasmtime-release.sh update