Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 192 additions & 148 deletions .github/workflows/bump-version.yaml
Original file line number Diff line number Diff line change
@@ -1,159 +1,203 @@
name: Bump version

on:
workflow_dispatch:
inputs:
version:
description: The new version of the interface package and crates, e.g. 0.1.0 or 0.2.0-dev
required: true
type: string
default: "0.0.0-dev" # the current version, kept current by scripts/bump-version.sh
workflow_dispatch:
inputs:
version:
description: The new version of the interface package, e.g. 0.1.0 or 0.2.0-dev
required: true
type: string
default: "0.1.0-dev" # the current version, kept current by scripts/bump-version.sh

jobs:
# bumps the version with read only access, the changes are handed to the pull-request job as a
# patch so the third party actions used to build never run with write access
bump:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions-rust-lang/setup-rust-toolchain@v2
- name: Install cargo binstall
uses: cargo-bins/cargo-binstall@main
- name: Install tools
run: |
make tools
make -s tools-path >> "${GITHUB_PATH}"
- name: Bump version
# also fetches the wit dependencies for the new version, and builds and tests the components
run: scripts/bump-version.sh "${VERSION}"
env:
VERSION: ${{ inputs.version }}
- name: Collect changes
run: |
git add --all
git diff --cached --binary > bump-version.patch
- name: Upload changes
uses: actions/upload-artifact@v7
with:
name: bump-version.patch
path: bump-version.patch
if-no-files-found: error
retention-days: 1
# bumps the version with read only access, the changes are handed to the push job as a patch so
# the third party actions used to build never run with write access
bump:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check version
# a semver version without a leading `v`, the tag adds it. Build metadata is used to tag the
# debug builds of components, e.g. 0.1.0+debug, and isn't allowed in the version itself.
run: |
identifier='(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)'
semver="^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-${identifier}(\.${identifier})*)?$"
if ! [[ "${VERSION}" =~ ${semver} ]] ; then
echo "::error::the version '${VERSION}' is not a valid semver version, e.g. 0.1.0 or 0.2.0-dev"
exit 1
fi
if [[ "${VERSION}" == *+* ]] ; then
echo "::error::the version '${VERSION}' must not contain build metadata"
exit 1
fi
env:
VERSION: ${{ inputs.version }}
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions-rust-lang/setup-rust-toolchain@v2
- name: Install cargo binstall
uses: cargo-bins/cargo-binstall@main
- name: Install tools
run: |
make tools
make -s tools-path >> "${GITHUB_PATH}"
- name: Bump version
# also fetches the wit dependencies for the new version, and builds and tests the components
run: scripts/bump-version.sh "${VERSION}"
env:
VERSION: ${{ inputs.version }}
- name: Collect changes
run: |
git add --all
git diff --cached --binary > bump-version.patch
- name: Upload changes
uses: actions/upload-artifact@v7
with:
name: bump-version.patch
path: bump-version.patch
if-no-files-found: error
retention-days: 1

# opens the pull request using only first party actions and the gh cli
pull-request:
needs:
- bump
runs-on: ubuntu-latest
# the branch and pull request are created with a token for the custodian GitHub App rather than
# the GITHUB_TOKEN, which can't change workflow files and doesn't trigger the CI workflow
permissions:
contents: read
# pushes the bump to the branch the workflow was run on, using only first party actions and the gh
# cli
push:
needs:
- bump
# the bump is a new commit on the branch, a tag can't be moved forward
if: github.ref_type == 'branch'
runs-on: ubuntu-latest
# the commit is pushed with a token for the custodian GitHub App rather than the GITHUB_TOKEN,
# which can't change workflow files and doesn't trigger the CI workflow. The app must be allowed
# to bypass the branch's ruleset, the commit is pushed without the status checks it requires,
# the bump job built and tested the changes instead.
permissions:
contents: read
env:
VERSION: ${{ inputs.version }}
steps:
- name: Check custodian app credentials
run: |
if [ -z "${CLIENT_ID}" ] || [ -z "${PRIVATE_KEY}" ] ; then
echo "::error::the CUSTODIAN_CLIENT_ID and CUSTODIAN_PRIVATE_KEY secrets must be available to this repository, the private key of the custodian GitHub App is needed to create a token"
exit 1
fi
env:
CLIENT_ID: ${{ secrets.CUSTODIAN_CLIENT_ID }}
PRIVATE_KEY: ${{ secrets.CUSTODIAN_PRIVATE_KEY }}
- name: Create custodian app token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.CUSTODIAN_CLIENT_ID }}
private-key: ${{ secrets.CUSTODIAN_PRIVATE_KEY }}
# only this repository, with only the permissions the bump needs
repositories: ${{ github.event.repository.name }}
permission-contents: write
# the bump changes the default version in this workflow
permission-workflows: write
# a release runs this workflow again, to bump to the next dev version
permission-actions: write
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Download changes
uses: actions/download-artifact@v8
with:
name: bump-version.patch
path: ${{ runner.temp }}
- name: Read current version
# the checkout is before the bump, the wit package's version is the current version
run: |
current=$( sed -n 's/^package componentized:oci@\(.*\);$/\1/p' wit/worlds.wit )
echo "CURRENT_VERSION=${current}" >> "${GITHUB_ENV}"
- name: Commit changes
# the commit is created with the REST API, as the app's token can't push. The patch is applied
# locally only to find the changed files and their modes.
env:
VERSION: ${{ inputs.version }}
steps:
- name: Check custodian app credentials
run: |
if [ -z "${CLIENT_ID}" ] || [ -z "${PRIVATE_KEY}" ] ; then
echo "::error::the CUSTODIAN_CLIENT_ID and CUSTODIAN_PRIVATE_KEY secrets must be available to this repository, the private key of the custodian GitHub App is needed to create a token"
exit 1
fi
env:
CLIENT_ID: ${{ secrets.CUSTODIAN_CLIENT_ID }}
PRIVATE_KEY: ${{ secrets.CUSTODIAN_PRIVATE_KEY }}
- name: Create custodian app token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.CUSTODIAN_CLIENT_ID }}
private-key: ${{ secrets.CUSTODIAN_PRIVATE_KEY }}
# only this repository, with only the permissions the bump needs
repositories: ${{ github.event.repository.name }}
permission-contents: write
permission-pull-requests: write
# the bump changes the default version in this workflow
permission-workflows: write
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Download changes
uses: actions/download-artifact@v8
with:
name: bump-version.patch
path: ${{ runner.temp }}
- name: Read current version
# the checkout is before the bump, the crates' workspace version is the current version
run: |
current=$( sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"$/\1/p' Cargo.toml )
echo "CURRENT_VERSION=${current}" >> "${GITHUB_ENV}"
- name: Commit changes
# the commit is created with the REST API, as the app's token can't push. The patch is applied
# locally only to find the changed files and their modes.
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
branch="bump-version/${VERSION}"
api="repos/${GITHUB_REPOSITORY}"
base=$( git rev-parse HEAD )
git apply --index "${RUNNER_TEMP}/bump-version.patch"
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
api="repos/${GITHUB_REPOSITORY}"
base=$( git rev-parse HEAD )
git apply --index "${RUNNER_TEMP}/bump-version.patch"

# a blob for each changed file, or a null sha for a deleted file
entries="${RUNNER_TEMP}/tree-entries.json"
echo '[]' > "${entries}"
git diff --cached --no-renames --name-status "${base}" | while IFS=$'\t' read -r status path ; do
if [ "${status}" = "D" ] ; then
entry=$( jq -n --arg path "${path}" '{path: $path, mode: "100644", type: "blob", sha: null}' )
else
mode=$( git ls-files --stage -- "${path}" | cut -d' ' -f1 )
sha=$( base64 < "${path}" | tr -d '\n' | jq -Rs '{encoding: "base64", content: .}' | gh api --method POST "${api}/git/blobs" --input - --jq .sha )
entry=$( jq -n --arg path "${path}" --arg mode "${mode}" --arg sha "${sha}" '{path: $path, mode: $mode, type: "blob", sha: $sha}' )
fi
jq --argjson entry "${entry}" '. + [$entry]' "${entries}" > "${entries}.tmp" && mv "${entries}.tmp" "${entries}"
echo "${status} ${path}"
done
tree=$( jq --arg base "$( git rev-parse "${base}^{tree}" )" '{base_tree: $base, tree: .}' "${entries}" | gh api --method POST "${api}/git/trees" --input - --jq .sha )
# a blob for each changed file, or a null sha for a deleted file
entries="${RUNNER_TEMP}/tree-entries.json"
echo '[]' > "${entries}"
git diff --cached --no-renames --name-status "${base}" | while IFS=$'\t' read -r status path ; do
if [ "${status}" = "D" ] ; then
entry=$( jq -n --arg path "${path}" '{path: $path, mode: "100644", type: "blob", sha: null}' )
else
mode=$( git ls-files --stage -- "${path}" | cut -d' ' -f1 )
sha=$( base64 < "${path}" | tr -d '\n' | jq -Rs '{encoding: "base64", content: .}' | gh api --method POST "${api}/git/blobs" --input - --jq .sha )
entry=$( jq -n --arg path "${path}" --arg mode "${mode}" --arg sha "${sha}" '{path: $path, mode: $mode, type: "blob", sha: $sha}' )
fi
jq --argjson entry "${entry}" '. + [$entry]' "${entries}" > "${entries}.tmp" && mv "${entries}.tmp" "${entries}"
echo "${status} ${path}"
done
tree=$( jq --arg base "$( git rev-parse "${base}^{tree}" )" '{base_tree: $base, tree: .}' "${entries}" | gh api --method POST "${api}/git/trees" --input - --jq .sha )

# authored and signed off (DCO) by the user who triggered the workflow, with their GitHub
# noreply email so the commit is attributed to them without exposing their email address.
# Committed by the custodian app's bot, which made the commit on their behalf. The commit is
# unsigned, GitHub only signs commits it attributes entirely to the app.
name=$( gh api "users/${GITHUB_ACTOR}" --jq '.name // .login' )
name="${name:-${GITHUB_ACTOR}}"
email="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"
bot="${APP_SLUG}[bot]"
bot_email="$( gh api "users/${bot}" --jq .id )+${bot}@users.noreply.github.com"
commit=$( jq -n \
--arg message "$( printf 'Bump version from %s to %s\n\nSigned-off-by: %s <%s>' "${CURRENT_VERSION}" "${VERSION}" "${name}" "${email}" )" \
--arg tree "${tree}" --arg parent "${base}" --arg name "${name}" --arg email "${email}" \
--arg bot "${bot}" --arg bot_email "${bot_email}" \
'{message: $message, tree: $tree, parents: [$parent], author: {name: $name, email: $email}, committer: {name: $bot, email: $bot_email}}' \
| gh api --method POST "${api}/git/commits" --input - --jq .sha )
echo "created commit ${commit}"
# authored, committed and signed off (DCO) by the custodian app's bot. GitHub only signs
# commits it attributes entirely to the app, so the author and committer are left for GitHub
# to fill in. The user who triggered the workflow is credited as a co-author, with their
# GitHub noreply email so their email address isn't exposed. A run started by the app after a
# release has no one else to credit.
bot="${APP_SLUG}[bot]"
bot_email="$( gh api "users/${bot}" --jq .id )+${bot}@users.noreply.github.com"
message=$( printf 'Bump version from %s to %s\n\nSigned-off-by: %s <%s>' "${CURRENT_VERSION}" "${VERSION}" "${bot}" "${bot_email}" )
if [ "${GITHUB_ACTOR}" != "${bot}" ] ; then
name=$( gh api "users/${GITHUB_ACTOR}" --jq '.name // .login' )
name="${name:-${GITHUB_ACTOR}}"
email="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"
message=$( printf '%s\nCo-authored-by: %s <%s>' "${message}" "${name}" "${email}" )
fi
read -r commit verified < <( jq -n \
--arg message "${message}" \
--arg tree "${tree}" --arg parent "${base}" \
'{message: $message, tree: $tree, parents: [$parent]}' \
| gh api --method POST "${api}/git/commits" --input - --jq '"\(.sha) \(.verification.verified)"' )
echo "created commit ${commit}"
# the branch requires signed commits, fail before pushing rather than after
if [ "${verified}" != "true" ] ; then
echo "::error::the commit '${commit}' was not created, or was not signed by GitHub"
exit 1
fi

# points the branch at the commit, replacing the branch left by an earlier run for the same version
if gh api "${api}/git/ref/heads/${branch}" --silent 2> /dev/null ; then
gh api --method PATCH "${api}/git/refs/heads/${branch}" -f sha="${commit}" -F force=true --silent
else
gh api --method POST "${api}/git/refs" -f ref="refs/heads/${branch}" -f sha="${commit}" --silent
fi
- name: Open pull request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
branch="bump-version/${VERSION}"
if [ -n "$( gh pr list --head "${branch}" --state open --json number --jq '.[].number' )" ] ; then
echo "A pull request for ${branch} is already open, updated by the new commit"
exit 0
fi
gh pr create \
--base "${GITHUB_REF_NAME}" \
--head "${branch}" \
--title "Bump version from \`${CURRENT_VERSION}\` to \`${VERSION}\`" \
--body "Bumps the wit package and crates from \`${CURRENT_VERSION}\` to \`${VERSION}\`.
# a release, rather than a pre-release, is tagged, e.g. v0.1.0. Tags are immutable, check the
# tag is free before pushing the branch, so a release isn't left without its tag
tag=""
if [[ "${VERSION}" != *-* ]] ; then
tag="v${VERSION}"
if gh api "${api}/git/ref/tags/${tag}" --silent 2> /dev/null ; then
echo "::error::the tag ${tag} already exists"
exit 1
fi
fi

Triggered by @${GITHUB_ACTOR} from the [Bump version](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) workflow."
# fast forwards the branch to the commit, failing rather than losing commits pushed to the
# branch since the workflow started
if ! gh api --method PATCH "${api}/git/refs/heads/${GITHUB_REF_NAME}" -f sha="${commit}" -F force=false --silent ; then
echo "::error::failed to push ${commit} to ${GITHUB_REF_NAME}, either the branch moved since ${base} and the workflow should be run again, or the custodian app is not allowed to bypass the branch's ruleset"
exit 1
fi
echo "pushed ${commit} to ${GITHUB_REF_NAME}"

# a lightweight tag, pushed with the app's token so the CI workflow runs for it and drafts
# the release
if [ -n "${tag}" ] ; then
gh api --method POST "${api}/git/refs" -f ref="refs/tags/${tag}" -f sha="${commit}" --silent
echo "tagged ${commit} as ${tag}"
fi
- name: Bump to the next dev version
# after a release, the branch moves on to a pre-release of the next patch version, e.g. 0.1.0
# is followed by 0.1.1-dev. Run with the app's token, the GITHUB_TOKEN can't start workflows.
if: ${{ !contains(inputs.version, '-') }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
IFS=. read -r major minor patch <<< "${VERSION}"
next="${major}.${minor}.$(( patch + 1 ))-dev"
gh workflow run bump-version.yaml --repo "${GITHUB_REPOSITORY}" --ref "${GITHUB_REF_NAME}" -f version="${next}"
echo "started the ${GITHUB_WORKFLOW} workflow for ${next} on ${GITHUB_REF_NAME}"
Loading
Loading