feat(release): automate release preparation
This commit is contained in:
83
.gitea/workflows/prepare-release.yml
Normal file
83
.gitea/workflows/prepare-release.yml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
name: Prepare Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: Semantic version to release, with or without leading v.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: docker.io/catthehacker/ubuntu:act-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26.1'
|
||||||
|
check-latest: true
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
|
- name: Prepare release files
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
./script/prepare-release.sh "$RELEASE_VERSION"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
- name: Configure git author
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "gitea-actions[bot]"
|
||||||
|
git config user.email "gitea-actions[bot]@users.noreply.local"
|
||||||
|
|
||||||
|
- name: Commit release changes and push tag
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
normalized_version="${RELEASE_VERSION#v}"
|
||||||
|
tag="v${normalized_version}"
|
||||||
|
|
||||||
|
if git rev-parse "$tag" >/dev/null 2>&1; then
|
||||||
|
echo "Tag ${tag} already exists" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$GITHUB_SERVER_URL" in
|
||||||
|
https://*)
|
||||||
|
authed_remote="https://oauth2:${RELEASE_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git"
|
||||||
|
;;
|
||||||
|
http://*)
|
||||||
|
authed_remote="http://oauth2:${RELEASE_TOKEN}@${GITHUB_SERVER_URL#http://}/${GITHUB_REPOSITORY}.git"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported GITHUB_SERVER_URL: ${GITHUB_SERVER_URL}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
git remote set-url origin "$authed_remote"
|
||||||
|
git add changelog.md internal/homesick/version/version.go
|
||||||
|
git commit -m "release: prepare ${tag}"
|
||||||
|
git tag "$tag"
|
||||||
|
git push origin HEAD
|
||||||
|
git push origin "$tag"
|
||||||
33
cmd/releaseprep/main.go
Normal file
33
cmd/releaseprep/main.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/releaseprep"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
version := flag.String("version", "", "semantic version to release, with or without leading v")
|
||||||
|
date := flag.String("date", "", "release date in YYYY-MM-DD format")
|
||||||
|
root := flag.String("root", ".", "repository root to update")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *version == "" || *date == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: releaseprep --version <version> --date <YYYY-MM-DD> [--root <dir>]")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
absRoot, err := filepath.Abs(*root)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "resolve root: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := releaseprep.Prepare(absRoot, *version, *date); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "prepare release: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
90
internal/releaseprep/releaseprep.go
Normal file
90
internal/releaseprep/releaseprep.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package releaseprep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionPattern = regexp.MustCompile(`const String = "[^"]+"`)
|
||||||
|
|
||||||
|
func Prepare(rootDir, version, releaseDate string) error {
|
||||||
|
normalizedVersion := strings.TrimPrefix(strings.TrimSpace(version), "v")
|
||||||
|
if normalizedVersion == "" {
|
||||||
|
return fmt.Errorf("version must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseDate = strings.TrimSpace(releaseDate)
|
||||||
|
if releaseDate == "" {
|
||||||
|
return fmt.Errorf("release date must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updateVersionFile(rootDir, normalizedVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updateChangelog(rootDir, normalizedVersion, releaseDate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVersionFile(rootDir, version string) error {
|
||||||
|
path := filepath.Join(rootDir, "internal", "homesick", "version", "version.go")
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read version file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := versionPattern.ReplaceAllString(string(contents), fmt.Sprintf(`const String = %q`, version))
|
||||||
|
if updated == string(contents) {
|
||||||
|
return fmt.Errorf("version constant not found in %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write version file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateChangelog(rootDir, version, releaseDate string) error {
|
||||||
|
path := filepath.Join(rootDir, "changelog.md")
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read changelog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text := string(contents)
|
||||||
|
unreleasedHeader := "## [Unreleased]\n"
|
||||||
|
start := strings.Index(text, unreleasedHeader)
|
||||||
|
if start == -1 {
|
||||||
|
return fmt.Errorf("unreleased section not found in changelog")
|
||||||
|
}
|
||||||
|
|
||||||
|
afterHeader := start + len(unreleasedHeader)
|
||||||
|
nextSectionRelative := strings.Index(text[afterHeader:], "\n## [")
|
||||||
|
if nextSectionRelative == -1 {
|
||||||
|
nextSectionRelative = len(text[afterHeader:])
|
||||||
|
}
|
||||||
|
nextSectionStart := afterHeader + nextSectionRelative
|
||||||
|
unreleasedBody := strings.TrimLeft(text[afterHeader:nextSectionStart], "\n")
|
||||||
|
|
||||||
|
newSection := fmt.Sprintf("## [%s] - %s\n", version, releaseDate)
|
||||||
|
if strings.TrimSpace(unreleasedBody) != "" {
|
||||||
|
newSection += "\n" + unreleasedBody
|
||||||
|
if !strings.HasSuffix(newSection, "\n") {
|
||||||
|
newSection += "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := text[:afterHeader] + "\n" + newSection + text[nextSectionStart:]
|
||||||
|
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write changelog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
3
justfile
3
justfile
@@ -19,3 +19,6 @@ behavior:
|
|||||||
|
|
||||||
behavior-verbose:
|
behavior-verbose:
|
||||||
./script/run-behavior-suite-docker.sh --verbose
|
./script/run-behavior-suite-docker.sh --verbose
|
||||||
|
|
||||||
|
prepare-release version:
|
||||||
|
./script/prepare-release.sh "{{version}}"
|
||||||
|
|||||||
12
script/prepare-release.sh
Executable file
12
script/prepare-release.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "usage: $0 <version>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
release_date="$(date -u +%F)"
|
||||||
|
|
||||||
|
go run ./cmd/releaseprep --root "$repo_root" --version "$1" --date "$release_date"
|
||||||
Reference in New Issue
Block a user