diff --git a/.github/workflows/extract-changelog.sh b/.github/workflows/extract-changelog.sh new file mode 100755 index 0000000000..d429ebd069 --- /dev/null +++ b/.github/workflows/extract-changelog.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# Print the CHANGELOG.md entry for one version, to use as release notes. +# The file uses a setext heading: the bare version, then a line of dashes. + +set -e + +version="$1" +if [ -z "$version" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +notes=$(awk -v version="$version" ' + found { + if ($0 ~ /^---+$/ && previous != "") { previous = ""; exit } + if (previous != "") print previous + previous = $0 + next + } + $0 ~ /^---+$/ && previous == version { + found = 1 + previous = "" + next + } + { previous = $0 } + END { if (found && previous != "") print previous } +' CHANGELOG.md) + +if [ -z "$notes" ]; then + echo "no CHANGELOG.md entry for version $version" >&2 + exit 1 +fi + +echo "$notes" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..8f31e3867c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,158 @@ +# Publish a GitHub release from the artifacts that CI already built. +# +# The Linux, macOS and Windows workflows build every file that a release needs +# when the release branch is pushed. This workflow collects the artifacts of +# those runs for the tagged commit, so what ships is what was tested. +# +# The release is a draft, so the notes can be reviewed before publication. +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release, for example v0.42.0' + required: true + +concurrency: + group: release-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false + +permissions: + contents: write + actions: read + +jobs: + release: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ inputs.tag || github.ref_name }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.tag || github.ref_name }} + fetch-depth: 0 + - name: Read the version + id: version + # The release file names come from goenv/version.go, not from the tag, + # so the two must agree. + run: | + version=$(./.github/workflows/tinygo-extract-version.sh | cut -d= -f2-) + case "$version" in + *-dev) + echo "::error::goenv/version.go has development version $version" + exit 1 + ;; + esac + if [ "v$version" != "$TAG" ]; then + echo "::error::tag $TAG does not match version $version in goenv/version.go" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Wait for the CI runs of this commit + id: runs + # A pull request run has the merge commit as its head, so look at push + # runs only. + run: | + sha=$(git rev-parse HEAD) + for workflow in linux.yml build-macos.yml windows.yml; do + id= + for _ in $(seq 20); do + id=$(gh run list --workflow "$workflow" --commit "$sha" --event push --limit 1 --json databaseId --jq '.[0].databaseId') + if [ -n "$id" ]; then + break + fi + echo "waiting for $workflow to start on $sha" + sleep 30 + done + if [ -z "$id" ]; then + echo "::error::no $workflow run for commit $sha" + exit 1 + fi + echo "$workflow: run $id" + gh run watch "$id" --exit-status > /dev/null || true + conclusion=$(gh run view "$id" --json conclusion --jq .conclusion) + if [ "$conclusion" != "success" ]; then + echo "::error::$workflow run $id concluded with $conclusion" + exit 1 + fi + case "$workflow" in + linux.yml) echo "linux=$id" >> "$GITHUB_OUTPUT" ;; + build-macos.yml) echo "macos=$id" >> "$GITHUB_OUTPUT" ;; + windows.yml) echo "windows=$id" >> "$GITHUB_OUTPUT" ;; + esac + done + # The build jobs upload with `archive: false`, so the artifact is the + # release file itself. skip-decompress keeps it that way. The Windows + # release is a .zip, which a download would otherwise unpack. + - name: Download the Linux artifacts + uses: actions/download-artifact@v8 + with: + run-id: ${{ steps.runs.outputs.linux }} + github-token: ${{ github.token }} + path: artifacts + skip-decompress: true + - name: Download the macOS artifacts + uses: actions/download-artifact@v8 + with: + run-id: ${{ steps.runs.outputs.macos }} + github-token: ${{ github.token }} + path: artifacts + skip-decompress: true + - name: Download the Windows artifacts + uses: actions/download-artifact@v8 + with: + run-id: ${{ steps.runs.outputs.windows }} + github-token: ${{ github.token }} + path: artifacts + skip-decompress: true + - name: Collect the release files + # A build that stopped uploading must not give a half complete release, + # so list the files that are expected. The search is by file name, not + # by artifact name, because the artifact names are not uniform. + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + version=$VERSION + mkdir -p dist + missing=0 + for file in \ + "tinygo$version.linux-amd64.tar.gz" "tinygo_${version}_amd64.deb" \ + "tinygo$version.linux-arm.tar.gz" "tinygo_${version}_armhf.deb" \ + "tinygo$version.linux-arm64.tar.gz" "tinygo_${version}_arm64.deb" \ + "tinygo$version.darwin-amd64.tar.gz" \ + "tinygo$version.darwin-arm64.tar.gz" \ + "tinygo$version.windows-amd64.zip"; do + found=$(find artifacts -type f -name "$file" | head -1) + if [ -z "$found" ]; then + echo "::error::missing release file $file" + missing=1 + else + mv "$found" "dist/$file" + fi + done + ls -l dist + exit $missing + - name: Extract the release notes + env: + VERSION: ${{ steps.version.outputs.version }} + run: ./.github/workflows/extract-changelog.sh "$VERSION" > notes.md + - name: Create the draft release + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + version=$VERSION + set -- + case "$version" in + *-*) set -- --prerelease ;; + esac + gh release create "$TAG" --draft --verify-tag "$@" \ + --title "$version" \ + --notes-file notes.md \ + dist/* diff --git a/BUILDING.md b/BUILDING.md index 3b55e27bb1..98c8f49b8d 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -119,3 +119,35 @@ the following command (for example in ~/lib): TinyGo will get extracted to a `tinygo` directory. You can then call it with: ./tinygo/bin/tinygo + +## Publish a release + +The `Release` workflow (`.github/workflows/release.yml`) publishes releases. It +does not build anything. The Linux, macOS and Windows workflows already build +every file that a release needs when the `release` branch is pushed, so the +release workflow collects the artifacts of those runs for the tagged commit. +What ships is what was tested. + + 1. On the `dev` branch, set `const version` in `goenv/version.go` to the new + version (without a `v` prefix), and add the entry to `CHANGELOG.md`. + 2. Merge `dev` into the `release` branch. + 3. Tag that commit and push the tag: + + git tag v0.42.0 + git push origin v0.42.0 + + The tag must be `v` plus the version in `goenv/version.go`, because the + release file names come from that constant. + 4. The workflow waits for the Linux, macOS and Windows runs of the tagged + commit, collects their nine files, and creates a **draft** release. The + release notes come from the `CHANGELOG.md` entry for that version. + 5. Review the draft release and publish it. + 6. On the `dev` branch, set `goenv/version.go` to the next `-dev` version. + +To release again after a failure, delete the draft release and start the +workflow from the Actions tab with the tag as its input. + +GitHub keeps a SHA-256 digest of every published file. The digest is not shown +on the release page, but it can be printed with: + + gh release view v0.42.0 --json assets --jq '.assets[] | "\(.digest) \(.name)"'