Skip to content

Commit 523c6c2

Browse files
authored
Merge pull request #106 from github/nodeselector-document-release-compatibility
Add guarded release tooling and release candidates
2 parents 37645db + 26a6e76 commit 523c6c2

7 files changed

Lines changed: 331 additions & 9 deletions

File tree

.github/workflows/release.yml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
# This workflow is managed by gh actions-lock.
22

3-
name: release
3+
name: Release
4+
45
on:
56
push:
67
tags:
7-
- "v*"
8+
- v*
89

910
permissions:
10-
contents: write
11-
id-token: write
12-
attestations: write
11+
contents: read
12+
13+
concurrency:
14+
group: release-${{ github.ref }}
15+
cancel-in-progress: false
1316

1417
jobs:
15-
release:
18+
publish:
19+
name: Publish
20+
permissions:
21+
contents: write
22+
id-token: write
23+
attestations: write
1624
runs-on: ubuntu-latest
1725
steps:
1826
- uses: actions/checkout@v6.0.2
27+
with:
28+
fetch-depth: 0
29+
fetch-tags: true
30+
31+
- name: Verify tag
32+
run: script/verify-release-tag "$GITHUB_REF_NAME" v
33+
1934
- uses: cli/gh-extension-precompile@v2.1.0
2035
with:
2136
generate_attestations: true

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
exit 1
2828
fi
2929
- run: go vet ./...
30-
- run: go test -race -count=1 ./...
30+
- run: make test
3131

3232
integration-stub:
3333
runs-on: ubuntu-latest

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ EXT_DIR := $(XDG_DATA_HOME)/gh/extensions/$(EXT_NAME)
77

88
RUBY := $(shell command -v /opt/homebrew/opt/ruby/bin/ruby 2>/dev/null || echo ruby)
99

10-
.PHONY: build test vet fmt fmt-check test-integration test-shell test-live test-smoke test-stub test-real install reinstall uninstall
10+
.PHONY: build test test-release vet fmt fmt-check test-integration test-shell test-live test-smoke test-stub test-real install reinstall uninstall
1111

1212
build:
1313
go build -o $(BIN) ./cmd/gh-actions-lock
1414

15-
test:
15+
test: test-release
1616
go test -race -count=1 ./...
1717

18+
test-release:
19+
script/release-test
20+
1821
vet:
1922
go vet ./...
2023

RELEASING.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Releasing `gh-actions-lock`
2+
3+
## Cutting a release
4+
5+
From a clean, current `main`, preview the next release:
6+
7+
```sh
8+
RELEASE_DRY_RUN=1 script/release patch
9+
```
10+
11+
Then cut it:
12+
13+
```sh
14+
script/release patch
15+
```
16+
17+
To cut a release candidate instead:
18+
19+
```sh
20+
script/release patch --rc
21+
```
22+
23+
The first candidate is `vX.Y.Z-rc.1`; repeating the same bump increments
24+
`rc.N`. Run the bump without `--rc` to publish the stable `vX.Y.Z`.
25+
26+
Use `patch` for compatible fixes, `minor` for compatible additions, and `major`
27+
for breaking changes.
28+
29+
The script validates the repository, checks the current branch and `origin/main`,
30+
then pushes and verifies an annotated `vX.Y.Z` or `vX.Y.Z-rc.N` tag.
31+
32+
The tag workflow verifies the tag, then publishes the binaries, attestations,
33+
and GitHub release.
34+
35+
## Dependabot compatibility
36+
37+
Before releasing, decide whether
38+
[Dependabot's CLI integration](https://github.com/dependabot/dependabot-core/blob/main/github_actions/lib/dependabot/github_actions/lockfile/cli_engine.rb)
39+
must move with the release. Update `dependabot-core` when the release changes:
40+
41+
- CLI flags used by Dependabot.
42+
- Findings JSON or exit codes.
43+
- Relocking behavior.
44+
- The lockfile schema.
45+
46+
Dependabot
47+
[pins the CLI version and binary checksums](https://github.com/dependabot/dependabot-core/blob/main/github_actions/Dockerfile)
48+
and currently
49+
[accepts only lockfile schema `v0.0.2`](https://github.com/dependabot/dependabot-core/blob/main/github_actions/lib/dependabot/github_actions/constants.rb).
50+
Add schema support there before this CLI emits a new version. Unrelated releases
51+
do not need a Dependabot bump.

script/release

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Cut a release tag for the Go sub-module.
4+
#
5+
# Usage:
6+
# script/release <patch|minor|major>
7+
# script/release <patch|minor|major> --rc
8+
#
9+
# The next version is computed from the latest vX.Y.Z tag and the chosen
10+
# bump. Maintainers run this locally to validate and push an annotated tag.
11+
# The tag workflow publishes it.
12+
#
13+
# Environment:
14+
# RELEASE_DRY_RUN=1 Compute and print the next version, then stop before
15+
# tagging or pushing.
16+
#
17+
# Requires: git, go, make.
18+
19+
set -euo pipefail
20+
21+
TAG_PREFIX="v"
22+
STABLE_TAG_RE='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
23+
TAG_RE='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-rc\.[1-9][0-9]*)?$'
24+
25+
die() {
26+
echo "error: $*" >&2
27+
exit 1
28+
}
29+
30+
# Run from the repository root regardless of the caller's cwd.
31+
cd "$(dirname "$0")/.."
32+
33+
validate_tag() {
34+
[[ "$1" =~ $TAG_RE ]] ||
35+
die "invalid release tag '$1'; expected vX.Y.Z or vX.Y.Z-rc.N"
36+
}
37+
38+
fetch_main() {
39+
git fetch --quiet --no-tags origin refs/heads/main:refs/remotes/origin/main
40+
}
41+
42+
require_current_main() {
43+
local commit="$1"
44+
local main
45+
fetch_main
46+
main="$(git rev-parse refs/remotes/origin/main)"
47+
[ "$commit" = "$main" ] ||
48+
die "release commit ${commit} is not current main ${main}"
49+
}
50+
51+
require_clean() {
52+
if [ -n "$(git status --porcelain)" ]; then
53+
git --no-pager status --short >&2
54+
die "working tree is not clean"
55+
fi
56+
}
57+
58+
require_remote_annotated_tag() {
59+
local tag="$1"
60+
local commit="$2"
61+
local remote_commit
62+
remote_commit="$(git ls-remote --tags origin "refs/tags/${tag}^{}" | awk 'NR == 1 { print $1 }')"
63+
[ "$remote_commit" = "$commit" ] ||
64+
die "remote tag ${tag} is missing, is not annotated, or does not point to ${commit}"
65+
}
66+
67+
remote_tags() {
68+
git ls-remote --tags --refs origin "refs/tags/$1" |
69+
awk '{ sub("refs/tags/", "", $2); print $2 }' |
70+
sort -Vr
71+
}
72+
73+
cut_tag() {
74+
local bump="$1"
75+
local release_candidate="$2"
76+
77+
local branch
78+
branch="$(git symbolic-ref --quiet --short HEAD || true)"
79+
[ "$branch" = "main" ] || die "releases must be cut from main (got ${branch:-detached HEAD})"
80+
require_clean
81+
require_current_main "$(git rev-parse HEAD)"
82+
83+
make fmt-check vet test test-stub
84+
85+
local latest="" t
86+
while IFS= read -r t; do
87+
[ -n "$t" ] || continue
88+
if [[ "$t" =~ $STABLE_TAG_RE ]]; then
89+
latest="$t"
90+
break
91+
fi
92+
done < <(remote_tags "${TAG_PREFIX}*")
93+
94+
local base
95+
if [ -z "$latest" ]; then
96+
base="0.0.0"
97+
echo "No existing ${TAG_PREFIX}* release tag; basing bump on v0.0.0."
98+
else
99+
base="${latest#"$TAG_PREFIX"}"
100+
fi
101+
102+
local major rest minor patch version tag
103+
major="${base%%.*}"
104+
rest="${base#*.}"
105+
minor="${rest%%.*}"
106+
patch="${rest#*.}"
107+
case "$bump" in
108+
major) major=$((major + 1)); minor=0; patch=0 ;;
109+
minor) minor=$((minor + 1)); patch=0 ;;
110+
patch) patch=$((patch + 1)) ;;
111+
esac
112+
113+
version="v${major}.${minor}.${patch}"
114+
tag="${TAG_PREFIX}${major}.${minor}.${patch}"
115+
if [ "$release_candidate" = "1" ]; then
116+
local candidate rc=0 n
117+
while IFS= read -r candidate; do
118+
[[ "$candidate" =~ $TAG_RE ]] || continue
119+
n="${candidate##*.}"
120+
[ "$n" -gt "$rc" ] && rc="$n"
121+
done < <(remote_tags "${tag}-rc.*")
122+
tag="${tag}-rc.$((rc + 1))"
123+
fi
124+
validate_tag "$tag"
125+
git rev-parse -q --verify "refs/tags/${tag}" >/dev/null &&
126+
die "tag ${tag} already exists"
127+
128+
echo "Cutting ${tag} (module version ${version}) at $(git rev-parse --short HEAD)"
129+
if [ "${RELEASE_DRY_RUN:-}" = "1" ]; then
130+
echo "RELEASE_DRY_RUN=1 set; stopping before tag and push."
131+
return
132+
fi
133+
134+
git tag -a "$tag" -m "$tag"
135+
local commit
136+
commit="$(git rev-parse "${tag}^{commit}")"
137+
require_current_main "$commit"
138+
git push origin "refs/tags/${tag}"
139+
require_remote_annotated_tag "$tag" "$commit"
140+
echo "Pushed ${tag}; the tag workflow will create the release."
141+
}
142+
143+
case "${1:-}" in
144+
patch | minor | major)
145+
if [ "$#" -eq 1 ]; then
146+
cut_tag "$1" 0
147+
elif [ "$#" -eq 2 ] && [ "$2" = "--rc" ]; then
148+
cut_tag "$1" 1
149+
else
150+
die "usage: script/release <patch|minor|major> [--rc]"
151+
fi
152+
;;
153+
*)
154+
die "usage: script/release <patch|minor|major> [--rc]"
155+
;;
156+
esac

script/release-test

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
root="$(cd "$(dirname "$0")/.." && pwd)"
5+
tmp="$(mktemp -d)"
6+
trap 'rm -r "$tmp"' EXIT
7+
8+
git init --quiet --bare "$tmp/origin.git"
9+
git clone --quiet "$tmp/origin.git" "$tmp/repo"
10+
cd "$tmp/repo"
11+
git config user.name test
12+
git config user.email test@example.com
13+
git switch --quiet -c main
14+
mkdir script
15+
cp "$root/script/release" script/release
16+
cp "$root/script/verify-release-tag" script/verify-release-tag
17+
touch tracked
18+
git add .
19+
git commit --quiet -m initial
20+
git push --quiet -u origin main
21+
git tag -a v1.2.3 -m v1.2.3
22+
git push --quiet origin refs/tags/v1.2.3
23+
git tag -a v99.0.0 -m v99.0.0
24+
25+
mkdir "$tmp/bin"
26+
printf '#!/bin/sh\nexit 0\n' >"$tmp/bin/make"
27+
chmod +x "$tmp/bin/make"
28+
export PATH="$tmp/bin:$PATH"
29+
30+
output="$(RELEASE_DRY_RUN=1 script/release patch --rc)"
31+
[[ "$output" == *"Cutting v1.2.4-rc.1 "* ]]
32+
33+
git tag -a v1.2.4-rc.1 -m v1.2.4-rc.1
34+
git push --quiet origin refs/tags/v1.2.4-rc.1
35+
git tag -a v1.2.4-rc.99 -m v1.2.4-rc.99
36+
output="$(RELEASE_DRY_RUN=1 script/release patch --rc)"
37+
[[ "$output" == *"Cutting v1.2.4-rc.2 "* ]]
38+
39+
output="$(RELEASE_DRY_RUN=1 script/release patch)"
40+
[[ "$output" == *"Cutting v1.2.4 "* ]]
41+
42+
ancestor="$(git rev-parse HEAD)"
43+
tree="$(git rev-parse "${ancestor}^{tree}")"
44+
descendant="$(printf 'descendant\n' | git -c user.name=test -c user.email=test@example.com commit-tree "$tree" -p "$ancestor")"
45+
git update-ref refs/heads/main "$descendant"
46+
git push --quiet origin refs/heads/main
47+
script/verify-release-tag v1.2.3 v
48+
git tag -a go/v1.2.3 -m go/v1.2.3 "$ancestor"
49+
git push --quiet origin refs/tags/go/v1.2.3
50+
script/verify-release-tag go/v1.2.3 go/v
51+
52+
if script/verify-release-tag v1.2.3 invalid 2>/dev/null; then
53+
echo "expected invalid prefix to fail verification" >&2
54+
exit 1
55+
fi
56+
57+
unrelated="$(printf 'unrelated\n' | git -c user.name=test -c user.email=test@example.com commit-tree "$tree")"
58+
git tag -a v1.2.5 -m v1.2.5 "$unrelated"
59+
git push --quiet origin refs/tags/v1.2.5
60+
if script/verify-release-tag v1.2.5 v 2>/dev/null; then
61+
echo "expected unrelated commit to fail ancestry check" >&2
62+
exit 1
63+
fi
64+
65+
echo "release tag selection and verification: ok"

script/verify-release-tag

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
tag="${1:-}"
5+
prefix="${2:-}"
6+
7+
case "$prefix" in
8+
v | go/v) ;;
9+
*)
10+
echo "error: invalid release tag prefix '$prefix'; expected v or go/v" >&2
11+
exit 1
12+
;;
13+
esac
14+
15+
tag_re="^${prefix}(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-rc\.[1-9][0-9]*)?$"
16+
if [[ ! "$tag" =~ $tag_re ]]; then
17+
echo "error: invalid release tag '$tag'; expected ${prefix}X.Y.Z or ${prefix}X.Y.Z-rc.N" >&2
18+
exit 1
19+
fi
20+
21+
commit="$(git rev-list -n 1 "$tag")"
22+
remote_commit="$(git ls-remote --tags origin "refs/tags/${tag}^{}" | awk 'NR == 1 { print $1 }')"
23+
if [ "$(git cat-file -t "refs/tags/$tag")" != "tag" ] || [ "$remote_commit" != "$commit" ]; then
24+
echo "error: release tag '$tag' is not an annotated remote tag" >&2
25+
exit 1
26+
fi
27+
28+
git fetch --quiet --no-tags origin refs/heads/main:refs/remotes/origin/main
29+
if ! git merge-base --is-ancestor "$commit" refs/remotes/origin/main; then
30+
echo "error: release tag '$tag' does not point to a commit in origin/main" >&2
31+
exit 1
32+
fi

0 commit comments

Comments
 (0)