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
128 changes: 86 additions & 42 deletions .github/workflows/bump-version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,35 @@ 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
description: The new version of the interface package, 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

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
# 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
Expand Down Expand Up @@ -44,13 +60,18 @@ jobs:
if-no-files-found: error
retention-days: 1

# opens the pull request using only first party actions and the gh cli
pull-request:
# 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 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
# 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:
Expand All @@ -74,9 +95,10 @@ jobs:
# 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
# a release runs this workflow again, to bump to the next dev version
permission-actions: write
- uses: actions/checkout@v7
with:
persist-credentials: false
Expand All @@ -86,9 +108,9 @@ jobs:
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
# the checkout is before the bump, the wit package's version is the current version
run: |
current=$( sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"$/\1/p' Cargo.toml )
current=$( sed -n 's/^package componentized:component@\(.*\);$/\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
Expand All @@ -97,7 +119,6 @@ jobs:
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"
Expand All @@ -118,42 +139,65 @@ jobs:
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"
# 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"
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 )
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
# 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

# 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
- name: Open pull request
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: |
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}\`.

Triggered by @${GITHUB_ACTOR} from the [Bump version](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) workflow."
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}"
18 changes: 16 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,30 @@ jobs:
if: startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && contains(steps.interface_version.outputs.VERSION, '-'))
run: make publish
env:
PUBLISH_LOG: "${{ runner.temp }}/published.txt"
REPOSITORY: "ghcr.io/${{ github.repository }}"
VERSION: "${{ case(github.ref == 'refs/heads/main', steps.interface_version.outputs.VERSION, steps.tag_version.outputs.VERSION) }}"
- name: Draft release notes
if: startsWith(github.ref, 'refs/tags/')
run: |
{
echo "## Published components"
echo
echo "| File | Size | Image |"
echo "| --- | --: | --- |"
while read -r file bytes image ; do
size=$( numfmt --to=iec-i --suffix=B --format=%.1f "${bytes}" )
echo "| \`${file}\` | ${size} | \`${image}\` |"
done < "${RUNNER_TEMP}/published.txt"
} > "${RUNNER_TEMP}/release-notes.md"
cat "${RUNNER_TEMP}/release-notes.md"
- name: Draft GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
draft: true
body_path: ${{ runner.temp }}/release-notes.md
files: |
target/components/*.wasm
target/components/*/*.wasm
components.tar
fail_on_unmatched_files: true
token: ${{ secrets.GITHUB_TOKEN }}
49 changes: 7 additions & 42 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -168,51 +168,16 @@ $(foreach dir,$(WKG_DIRS),$(eval $(call FETCH_WIT,$(dir))))
# sign published components with cosign, `SIGN=false` to push without signing, e.g. to a local registry
SIGN ?= true

# the files that can be published, e.g. gate.wasm, published from target/components/gate/gate.wasm
PUBLISH_FILES := interface.wasm $(foreach component,$(filter-out dep-% test-%,$(COMPONENTS)),$(component).wasm $(component).debug.wasm)
# append each published file, its size in bytes and its image to this file, e.g. `oci-loader.wasm 116633 ghcr.io/componentized/component/oci-loader:0.1.0@sha256:...`
PUBLISH_LOG ?=

# the files that can be published, e.g. oci-loader.wasm, published from target/components/oci-loader/oci-loader.wasm
PUBLISH_FILES := interface.wasm $(foreach component,$(filter-out dep-% internal-% test-%,$(COMPONENTS)),$(component).wasm $(component).debug.wasm)

.PHONY: publish ## Publish each component in the target/components directory
publish: $(addprefix publish-,$(PUBLISH_FILES))

.PHONY: $(addprefix publish-,$(PUBLISH_FILES))
$(addprefix publish-,$(PUBLISH_FILES)): publish-%: | $(call tool,wkg)
ifndef VERSION
$(error VERSION is undefined)
endif
ifndef REPOSITORY
$(error REPOSITORY is undefined)
endif
@$(eval FILE := $(@:publish-%=%))
@$(eval COMPONENT := $(patsubst %.wasm,%,$(patsubst %.debug.wasm,%,$(FILE))))
# components are in a directory of their own, the interface is not, e.g. gate/gate.wasm and interface.wasm
@$(eval COMPONENT_FILE := $(if $(filter interface.wasm,$(FILE)),$(FILE),$(COMPONENT)/$(FILE)))
@$(eval README := ${COMPONENTS_DIR}/$(dir $(COMPONENT_FILE))README.md)
@$(eval TITLE := $(subst /,:,$(GITHUB_REPOSITORY))$(if $(filter interface,$(COMPONENT)),,-$(COMPONENT))$(if $(filter %.debug.wasm,$(FILE)), (debug)))
@$(eval DESCRIPTION := $(shell head -n 3 "$(README)" | tail -n 1))
@$(eval COMMIT := $(shell git rev-parse HEAD))
@$(eval README_DIR := $(if $(wildcard components/$(COMPONENT)/README.md),/components/$(COMPONENT)))
@$(eval URL := https://github.com/${GITHUB_REPOSITORY}/tree/${COMMIT}${README_DIR})
@$(eval REVISION := ${COMMIT}$(shell git diff --quiet HEAD || echo "+dirty"))
@$(eval COMPONENT_VERSION := $(if $(filter %.debug.wasm,$(FILE)),${VERSION}+debug,${VERSION}))
@$(eval TAG := $(patsubst v%,%,$(subst +,_,$(COMPONENT_VERSION))))
@$(eval IMAGE := $(if $(filter interface.wasm,$(FILE)),${REPOSITORY}:${TAG},${REPOSITORY}/${COMPONENT}:${TAG}))

@echo "::group::${FILE} -> ${IMAGE}"
@set -o pipefail ; \
DIGEST=$$( \
wkg oci push \
--annotation "org.opencontainers.image.title=${TITLE}" \
--annotation "org.opencontainers.image.description=${DESCRIPTION}" \
--annotation "org.opencontainers.image.version=${COMPONENT_VERSION}" \
--annotation "org.opencontainers.image.url=${URL}" \
--annotation "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}.git" \
--annotation "org.opencontainers.image.revision=${REVISION}" \
--annotation "org.opencontainers.image.licenses=Apache-2.0" \
"${IMAGE}" \
"${COMPONENTS_DIR}/${COMPONENT_FILE}" \
2>&1 \
| tee /dev/stderr \
| grep -o 'sha256:[a-f0-9]\{64\}' \
) && \
$(if $(filter true,$(SIGN)),cosign sign --yes "${IMAGE}@$${DIGEST}",echo "Not signing ${IMAGE}@$${DIGEST}, SIGN=${SIGN}")
@echo "::endgroup::"
@VERSION="$(VERSION)" REPOSITORY="$(REPOSITORY)" COMPONENTS_DIR="$(COMPONENTS_DIR)" SIGN="$(SIGN)" PUBLISH_LOG="$(PUBLISH_LOG)" \
scripts/publish.sh $*
18 changes: 4 additions & 14 deletions scripts/bump-version.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
#!/usr/bin/env bash

# Bump the version of the wit interface package, and of the crates.
# Bump the version of the wit interface package.
#
# scripts/bump-version.sh <new-version>
#
# Updates the package declaration and every reference to the package in tracked files, then
# refreshes the generated wit dependencies. The crates share the interface's version: the
# workspace version the crates inherit, and the workspace's requirement on the library, move to the
# new version too. Items whose `@since` names an unreleased (prerelease)
# refreshes the generated wit dependencies. Items whose `@since` names an unreleased (prerelease)
# version move to the new version, since they were never published under the old one. Items
# released under the old version keep their `@since`.
#
Expand All @@ -19,8 +17,6 @@ set -euo pipefail
cd "$(dirname "$0")/.."

PACKAGE="${PACKAGE:-componentized:$(basename $(git rev-parse --show-toplevel))}"
# the library crate, the workspace's requirement on it moves to the new version
LIBRARY="${LIBRARY:-componentized-constants}"
SEMVER='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'

new="${1:-}"
Expand Down Expand Up @@ -70,7 +66,7 @@ fi

old_re="${old//./\\.}"
# references to the package or one of its interfaces, an interface named for a keyword is escaped
# with `%`, e.g. `componentized:constants/%u8@0.1.0`
# with `%`, e.g. `componentized:component/wit@0.1.0`
ref_re="${PACKAGE}(/%?[a-z0-9-]+)?"
# the fetched wit dependencies and the wkg.lock files are left to `make wit`, wkg replaces the
# dependencies and updates the locks for the new version
Expand All @@ -90,16 +86,10 @@ if [[ "$old" == *-* ]]; then
done
fi

# the version in the [workspace.package] section, inherited by the crates
perl -pi -e 'if (/^\[workspace\.package\]/ .. /^\[(?!workspace\.package\])/) { s/^version = "[^"]*"/version = "'"${new}"'"/ }' Cargo.toml
echo "updated the workspace version in Cargo.toml"
perl -pi -e 's/^(\Q'"${LIBRARY}"'\E = \{.*\bversion = ")[^"]*(")/${1}'"${new}"'${2}/' Cargo.toml
echo "updated the ${LIBRARY} requirement in Cargo.toml"

perl -pi -e 's{^(\s+)\Q'"${workflow_default}"'\E$}{${1}'"${workflow_default/\"${old}\"/\"${new}\"}"'}' "$workflow"
echo "updated the default version in ${workflow}"

# regenerate the wit dependencies for the new version
make wit components test

echo "bumped ${PACKAGE} from ${old} to ${new}"
echo "bumped ${PACKAGE} from ${old} to ${new}"
Loading
Loading