Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 31 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
# This workflow is managed by gh actions-lock.

name: release
name: Release

on:
push:
tags:
- "v*"
- v*

permissions:
contents: write
id-token: write
attestations: write
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
publish:
name: Publish
permissions:
contents: write
id-token: write
attestations: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
fetch-tags: true

- name: Verify tag
run: |
if [[ ! "$GITHUB_REF_NAME" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-rc\.[1-9][0-9]*)?$ ]]; then
echo "::error::Invalid release tag '$GITHUB_REF_NAME'; expected vX.Y.Z or vX.Y.Z-rc.N."
exit 1
fi
commit="$(git rev-list -n 1 "$GITHUB_REF_NAME")"
remote_commit="$(git ls-remote --tags origin "refs/tags/${GITHUB_REF_NAME}^{}" | awk 'NR == 1 { print $1 }')"
if [ "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" != "tag" ] || [ "$remote_commit" != "$commit" ]; then
echo "::error::Release tag '$GITHUB_REF_NAME' is not an annotated remote tag."
exit 1
Comment thread
nodeselector marked this conversation as resolved.
Outdated
fi

- uses: cli/gh-extension-precompile@v2.1.0
with:
generate_attestations: true
Expand Down
51 changes: 51 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Releasing `gh-actions-lock`

## Cutting a release

From a clean, current `main`, preview the next release:

```sh
RELEASE_DRY_RUN=1 script/release patch
```

Then cut it:

```sh
script/release patch
```

To cut a release candidate instead:

```sh
script/release patch --rc
```

The first candidate is `vX.Y.Z-rc.1`; repeating the same bump increments
`rc.N`. Run the bump without `--rc` to publish the stable `vX.Y.Z`.

Use `patch` for compatible fixes, `minor` for compatible additions, and `major`
for breaking changes.

The script validates the repository, checks the current branch and `origin/main`,
then pushes and verifies an annotated `vX.Y.Z` tag.
Comment thread
nodeselector marked this conversation as resolved.
Outdated

The tag workflow verifies the tag, then publishes the binaries, attestations,
and GitHub release.

## Dependabot compatibility

Before releasing, decide whether
[Dependabot's CLI integration](https://github.com/dependabot/dependabot-core/blob/main/github_actions/lib/dependabot/github_actions/lockfile/cli_engine.rb)
must move with the release. Update `dependabot-core` when the release changes:

- CLI flags used by Dependabot.
- Findings JSON or exit codes.
- Relocking behavior.
- The lockfile schema.

Dependabot
[pins the CLI version and binary checksums](https://github.com/dependabot/dependabot-core/blob/main/github_actions/Dockerfile)
and currently
[accepts only lockfile schema `v0.0.2`](https://github.com/dependabot/dependabot-core/blob/main/github_actions/lib/dependabot/github_actions/constants.rb).
Add schema support there before this CLI emits a new version. Unrelated releases
do not need a Dependabot bump.
150 changes: 150 additions & 0 deletions script/release
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
#
# Cut a release tag for the Go sub-module.
#
# Usage:
# script/release <patch|minor|major>
# script/release <patch|minor|major> --rc
#
# The next version is computed from the latest vX.Y.Z tag and the chosen
# bump. Maintainers run this locally to validate and push an annotated tag.
# The tag workflow publishes it.
#
# Environment:
# RELEASE_DRY_RUN=1 Compute and print the next version, then stop before
# tagging or pushing.
#
# Requires: git, go, make.

set -euo pipefail

TAG_PREFIX="v"
STABLE_TAG_RE='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
TAG_RE='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-rc\.[1-9][0-9]*)?$'

die() {
echo "error: $*" >&2
exit 1
}

# Run from the repository root regardless of the caller's cwd.
cd "$(dirname "$0")/.."

validate_tag() {
[[ "$1" =~ $TAG_RE ]] ||
die "invalid release tag '$1'; expected vX.Y.Z or vX.Y.Z-rc.N"
}

fetch_main() {
git fetch --quiet --tags origin refs/heads/main:refs/remotes/origin/main
}

require_current_main() {
local commit="$1"
local main
fetch_main
main="$(git rev-parse refs/remotes/origin/main)"
[ "$commit" = "$main" ] ||
die "release commit ${commit} is not current main ${main}"
}

require_clean() {
if [ -n "$(git status --porcelain)" ]; then
git --no-pager status --short >&2
die "working tree is not clean"
fi
}

require_remote_annotated_tag() {
local tag="$1"
local commit="$2"
local remote_commit
remote_commit="$(git ls-remote --tags origin "refs/tags/${tag}^{}" | awk 'NR == 1 { print $1 }')"
[ "$remote_commit" = "$commit" ] ||
die "remote tag ${tag} is missing, is not annotated, or does not point to ${commit}"
}

cut_tag() {
local bump="$1"
local release_candidate="$2"

local branch
branch="$(git symbolic-ref --quiet --short HEAD || true)"
[ "$branch" = "main" ] || die "releases must be cut from main (got ${branch:-detached HEAD})"
require_clean
require_current_main "$(git rev-parse HEAD)"

make fmt-check vet test test-stub

local latest="" t
while IFS= read -r t; do
[ -n "$t" ] || continue
if [[ "$t" =~ $STABLE_TAG_RE ]]; then
latest="$t"
break
fi
done < <(git tag --list "${TAG_PREFIX}*" --sort=-v:refname)
Comment thread
nodeselector marked this conversation as resolved.
Outdated

local base
if [ -z "$latest" ]; then
base="0.0.0"
echo "No existing ${TAG_PREFIX}* release tag; basing bump on v0.0.0."
else
base="${latest#"$TAG_PREFIX"}"
fi

local major rest minor patch version tag
major="${base%%.*}"
rest="${base#*.}"
minor="${rest%%.*}"
patch="${rest#*.}"
case "$bump" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac

version="v${major}.${minor}.${patch}"
tag="${TAG_PREFIX}${major}.${minor}.${patch}"
if [ "$release_candidate" = "1" ]; then
local candidate rc=0 n
while IFS= read -r candidate; do
[[ "$candidate" =~ $TAG_RE ]] || continue
n="${candidate##*.}"
[ "$n" -gt "$rc" ] && rc="$n"
done < <(git tag --list "${tag}-rc.*")
Comment thread
nodeselector marked this conversation as resolved.
Outdated
tag="${tag}-rc.$((rc + 1))"
fi
validate_tag "$tag"
git rev-parse -q --verify "refs/tags/${tag}" >/dev/null &&
die "tag ${tag} already exists"

echo "Cutting ${tag} (module version ${version}) at $(git rev-parse --short HEAD)"
if [ "${RELEASE_DRY_RUN:-}" = "1" ]; then
echo "RELEASE_DRY_RUN=1 set; stopping before tag and push."
return
fi

git tag -a "$tag" -m "$tag"
local commit
commit="$(git rev-parse "${tag}^{commit}")"
require_current_main "$commit"
git push origin "refs/tags/${tag}"
require_remote_annotated_tag "$tag" "$commit"
echo "Pushed ${tag}; the tag workflow will create the release."
}

case "${1:-}" in
patch | minor | major)
if [ "$#" -eq 1 ]; then
cut_tag "$1" 0
elif [ "$#" -eq 2 ] && [ "$2" = "--rc" ]; then
cut_tag "$1" 1
else
die "usage: script/release <patch|minor|major> [--rc]"
fi
;;
*)
die "usage: script/release <patch|minor|major> [--rc]"
;;
esac
48 changes: 48 additions & 0 deletions script/release-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
Comment thread
nodeselector marked this conversation as resolved.
set -euo pipefail

root="$(cd "$(dirname "$0")/.." && pwd)"
tmp="$(mktemp -d)"
trap 'rm -r "$tmp"' EXIT

latest=""
while IFS= read -r tag; do
if [[ "$tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
latest="$tag"
break
fi
done < <(git -C "$root" tag --list 'v*' --sort=-v:refname)
[[ -n "$latest" ]]
IFS=. read -r major minor patch <<< "${latest#v}"
next="v${major}.${minor}.$((patch + 1))"

git init --quiet --bare "$tmp/origin.git"
git clone --quiet "$tmp/origin.git" "$tmp/repo"
cd "$tmp/repo"
git config user.name test
git config user.email test@example.com
git switch --quiet -c main
mkdir script
cp "$root/script/release" script/release
touch tracked
git add .
git commit --quiet -m initial
git push --quiet -u origin main
git tag -a "$latest" -m "$latest"

mkdir "$tmp/bin"
printf '#!/bin/sh\nexit 0\n' >"$tmp/bin/make"
chmod +x "$tmp/bin/make"
export PATH="$tmp/bin:$PATH"

output="$(RELEASE_DRY_RUN=1 script/release patch --rc)"
[[ "$output" == *"Cutting ${next}-rc.1 "* ]]

git tag -a "${next}-rc.1" -m "${next}-rc.1"
output="$(RELEASE_DRY_RUN=1 script/release patch --rc)"
[[ "$output" == *"Cutting ${next}-rc.2 "* ]]

output="$(RELEASE_DRY_RUN=1 script/release patch)"
[[ "$output" == *"Cutting ${next} "* ]]

echo "release tag selection: ${latest} -> ${next}-rc.1 -> ${next}-rc.2 -> ${next}"