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
68 changes: 68 additions & 0 deletions .github/actions/release/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# `release` action

Prepares a _draft_ GitHub release from a merged release Pull Request (PR): the
tag is the version in the title of the merge commit, the target is that commit
and the notes are its description.

## Usage

```yaml
on:
pull_request:
types: [closed]
branches: [main]

permissions: {}

jobs:
draft-release:
if: |
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'releases/')
runs-on: ubuntu-latest
permissions:
# Required to create the draft release.
contents: write

steps:
# Pin to a commit of `mozilla/addons`.
- uses: mozilla/addons/.github/actions/release@43401a931ebc09f2e511deed766171b0105414bc
with:
repo: ${{ github.repository }}
sha: ${{ github.event.pull_request.merge_commit_sha }}
github_token: ${{ secrets.GITHUB_TOKEN }}
```

All three inputs are required. Outputs: `version` and `url`.

## Release process

A release starts with a PR from a `releases/*` branch, with a single commit
that bumps the version, mentions it in its title and has the release notes as
its description. Versions follow [semantic versioning](https://semver.org/).

Here is an example for an `npm` package:

1. Create the release PR (`npm version` takes `minor`, `patch` or `major`, and
`--no-git-tag-version` only updates `package.json` and `package-lock.json`):

```
version=$(npm version minor --no-git-tag-version)
git switch -c "releases/$version"
git add package.json package-lock.json
# Write the release notes in the description of this commit, also the commit
# title might just be "$version" or something more fancy, but it must have
# the version number in it.
git commit --edit --message ":arrow_up: release $version"
git push -u origin "releases/$version"
```

2. Open the PR: `gh pr create --fill`

3. Get it reviewed, then squash and merge it, keeping the release notes in the
description of the merge commit (GitHub pre-fills them). The draft release
is prepared, but nothing has been released yet!

4. Open the draft release and click _Publish release_. This creates the tag,
which makes CI publish the package to npm. That allows other workflows to be
triggered as well.
90 changes: 90 additions & 0 deletions .github/actions/release/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Draft Release
description: Prepare a draft GitHub release from a merged release pull request

# Nothing is released until someone publishes the draft. See the README for the
# release process and for a workflow calling this action, which needs
# `contents: write`.

inputs:
repo:
required: true
description: The name of the repository
sha:
required: true
description: >
The merge commit to release: it is the target of the release, its title
has the version and its description has the release notes.
github_token:
required: true
description: The GitHub token

outputs:
version:
description: The version of the draft release
value: ${{ steps.release.outputs.version }}
url:
description: The URL of the draft release
value: ${{ steps.release.outputs.url }}

runs:
using: composite
steps:
- name: Prepare the draft release
id: release
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
GH_REPO: ${{ inputs.repo }}
sha: ${{ inputs.sha }}
run: |
set -o pipefail

# The response is not piped directly into `jq` so that a failing
# `gh api` call fails this step, instead of looking like a commit
# without a description.
commit=$(gh api "repos/$GH_REPO/commits/$sha")
parents=$(printf '%s' "$commit" | jq '.parents | length')
message=$(printf '%s' "$commit" | jq -r .commit.message)
title=$(printf '%s\n' "$message" | head -n 1)

# The title is the one of the pull request followed by its number,
# e.g. ":arrow_up: release 1.2.3 (#123)". That number cannot be
# mistaken for the version, as it has no dots.
version=$(printf '%s' "$title" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?' | head -n 1 || true)

if [ -z "$version" ]; then
echo "::error::no version found in the merge commit title: $title"
exit 1
fi

# The notes are the description of the squash-merge commit, i.e., the
# notes someone confirmed when merging, even when the release branch
# ended up with more than one commit. A "real" merge commit has no
# such description, so we let GitHub generate the notes in that case.
notes=""
if [ "$parents" -eq 1 ]; then
notes=$(printf '%s\n' "$message" | sed '1d' | sed '/./,$!d')
else
echo "::warning::the release pull request has not been squash-merged."
fi

if [ -z "$notes" ]; then
echo "::warning::no release notes found in the merge commit, generating them instead."
gh release create "$version" --draft --target "$sha" --title "$version" --generate-notes
else
printf '%s' "$notes" | gh release create "$version" --draft --target "$sha" --title "$version" --notes-file -
fi

url=$(gh release view "$version" --json url --jq .url)

echo "version=$version" >> "$GITHUB_OUTPUT"
echo "url=$url" >> "$GITHUB_OUTPUT"
cat "$GITHUB_OUTPUT"

{
echo "### The draft release for \`$version\` is ready"
echo ""
echo "Nothing has been released yet. Publishing this draft creates the \`$version\` tag, which publishes the package:"
echo ""
echo "$url"
} >> "$GITHUB_STEP_SUMMARY"
Loading