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
126 changes: 85 additions & 41 deletions .github/workflows/bump-version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,32 @@ on:
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.1.0-dev" # the current version, kept current by scripts/bump-interface-version.sh
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
# 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 All @@ -29,7 +45,7 @@ jobs:
echo "${PWD}/target/tools/bin" >> "${GITHUB_PATH}"
- name: Bump version
# also fetches the wit dependencies for the new version, and builds and tests the components
run: scripts/bump-interface-version.sh "${VERSION}"
run: scripts/bump-version.sh "${VERSION}"
env:
VERSION: ${{ inputs.version }}
- name: Collect changes
Expand All @@ -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 @@ -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}"
22 changes: 17 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches:
- "**"
- "!bump-version/**"
- "!dependabot/**"
tags:
- 'v[0-9]+\.[0-9]+\.[0-9]+-?**'
Expand All @@ -28,12 +27,12 @@ jobs:
run: git diff --exit-code .
- name: Check crate version matches the interface version
# the crates inherit the workspace version, bumped with the interface by
# scripts/bump-interface-version.sh
# scripts/bump-version.sh
run: |
interface_version=$( sed -n "s/^package ${GITHUB_REPOSITORY/\//:}@\(.*\);$/\1/p" wit/worlds.wit )
workspace_version=$( sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"$/\1/p' Cargo.toml )
if [ "${workspace_version}" != "${interface_version}" ] ; then
echo "::error::the workspace version ${workspace_version} in Cargo.toml does not match the interface version ${interface_version}, see scripts/bump-interface-version.sh"
echo "::error::the workspace version ${workspace_version} in Cargo.toml does not match the interface version ${interface_version}, see scripts/bump-version.sh"
exit 1
fi
- name: Build components
Expand Down Expand Up @@ -198,16 +197,29 @@ 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 | Image |"
echo "| --- | --- |"
while read -r file image ; do
echo "| \`${file}\` | \`${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
dist/*
fail_on_unmatched_files: true
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ members = [
]

# the version of the cli and library crates, kept in step with the interface package by
# scripts/bump-interface-version.sh
# scripts/bump-version.sh
[workspace.package]
version = "0.1.0-dev"

Expand Down
57 changes: 4 additions & 53 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,6 @@ ${COMPONENTS_DIR}/interface.wasm: wit/deps README.md | $(call tool,wkg)
.PHONY: wit
wit: wit/deps components/wit/deps

.PHONY: bump-interface-version ## Bump the interface package and crate versions, e.g. INTERFACE_VERSION=0.1.0
bump-interface-version:
ifndef INTERFACE_VERSION
$(error INTERFACE_VERSION is undefined)
endif
scripts/bump-interface-version.sh $(INTERFACE_VERSION)

# the wit dependencies are fetched rather than committed, see .gitignore
wit/deps: wkg.toml $(shell find wit -type f -name "*.wit" -not -path "*/deps/*") | $(call tool,wkg)
$(WKG) fetch --config $(WKG_CONFIG)
Expand All @@ -182,6 +175,8 @@ components/wit/deps: wit/deps components/wkg.toml $(shell find components/wit -t

# sign published components with cosign, `SIGN=false` to push without signing, e.g. to a local registry
SIGN ?= true
# append each published file and its image to this file, e.g. `factory.wasm ghcr.io/componentized/constants/factory:0.1.0@sha256:...`
PUBLISH_LOG ?=

# 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)
Expand All @@ -191,49 +186,5 @@ 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 := $(if $(filter %.debug.wasm,$(FILE)),$(COMPONENT) (debug),$(COMPONENT)))
@$(eval DESCRIPTION := $(shell head -n 3 "$(README)" | tail -n 1))
@$(eval REVISION := $(shell git rev-parse HEAD)$(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}))

# a debug build identical to the release build adds nothing, e.g. components without debug info
@$(eval SKIP := $(if $(filter %.debug.wasm,$(FILE)),$(shell cmp -s "${COMPONENTS_DIR}/${COMPONENT_FILE}" "${COMPONENTS_DIR}/${COMPONENT}/${COMPONENT}.wasm" && echo true)))

# a failed push is recorded rather than stopping make, so the group is always closed before failing
@$(eval FAILED := ${COMPONENTS_DIR}/.publish-${FILE}.failed)
@rm -f "${FAILED}"

@$(if $(SKIP),echo "Not publishing ${FILE} as it is identical to ${COMPONENT}.wasm",echo "::group::${FILE} -> ${IMAGE}")
@$(if $(SKIP),exit 0 ;) \
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.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}") \
|| touch "${FAILED}"
@$(if $(SKIP),,echo "::endgroup::")
@if [ -f "${FAILED}" ] ; then rm -f "${FAILED}" ; echo "Failed to publish ${FILE}" >&2 ; exit 1 ; fi
@VERSION="$(VERSION)" REPOSITORY="$(REPOSITORY)" COMPONENTS_DIR="$(COMPONENTS_DIR)" SIGN="$(SIGN)" PUBLISH_LOG="$(PUBLISH_LOG)" \
scripts/publish.sh $*
6 changes: 3 additions & 3 deletions scripts/bump-interface-version.sh → scripts/bump-version.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash

# Bump the version of the interface package, e.g. componentized:constants, and of the crates.
# Bump the version of the wit interface package, and of the crates.
#
# scripts/bump-interface-version.sh <new-version>
# 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
Expand Down Expand Up @@ -62,7 +62,7 @@ fi
# the bump-version workflow offers the current version as the default for the next bump, checked
# before changing anything
workflow=.github/workflows/bump-version.yaml
workflow_default="default: \"${old}\" # the current version, kept current by scripts/bump-interface-version.sh"
workflow_default="default: \"${old}\" # the current version, kept current by scripts/bump-version.sh"
if ! grep -qF "$workflow_default" "$workflow"; then
echo "unable to find the current version as the default in ${workflow}, expected: ${workflow_default}" >&2
exit 1
Expand Down
Loading
Loading