From dd1bc814f39511e08b226faf726faa0c36d57c96 Mon Sep 17 00:00:00 2001 From: Greg Lin Date: Fri, 17 Jul 2026 18:14:26 -0500 Subject: [PATCH] Automate releases and remove pre-v4 multi-module remnants Add a GitHub Actions release workflow: push a semver tag, or run it with `-f version=vX.Y.Z` to validate the version, create and push the tag, and publish the release from CI. Both paths generate the changelog with `gh release create --generate-notes`, so the manual step is gone. The repo has been a single Go module (github.com/rstudio/platform-lib/v4) since v4, so remove the tooling and docs that assumed per-module go.mod files and per-module tags: - Delete the chart/tags scripts (latest-tags.sh, generate-chart-data.sh, generate-chart.sh), their justfile targets, and the .chart ignore/clean entries. The chart graphed dependencies between modules that no longer exist under a single module. - Rewrite the top-level release and testing sections and the pkg READMEs for the single module, and fix test examples that pointed at packages and import paths that no longer exist. --- .github/workflows/release.yml | 66 +++++++++++ .gitignore | 3 - README.md | 42 +++---- justfile | 6 - pkg/README.md | 21 +--- .../internal/integration_test/README.md | 11 +- pkg/rsnotify/README.md | 2 +- .../internal/integration_test/README.md | 11 +- scripts/generate-chart-data.sh | 109 ------------------ scripts/generate-chart.sh | 33 ------ scripts/latest-tags.sh | 37 ------ 11 files changed, 104 insertions(+), 237 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100755 scripts/generate-chart-data.sh delete mode 100755 scripts/generate-chart.sh delete mode 100755 scripts/latest-tags.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5d8060fe --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: Release + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + inputs: + version: + description: 'Version tag to release, e.g. v4.3.1' + required: true + type: string + +permissions: + contents: write + +jobs: + Release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Resolve tag + id: tag + env: + EVENT_NAME: ${{ github.event_name }} + REF_NAME: ${{ github.ref_name }} + INPUT_VERSION: ${{ inputs.version }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_VERSION" + else + tag="$REF_NAME" + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Create and push tag + if: github.event_name == 'workflow_dispatch' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag }} + HEAD_REF: ${{ github.ref }} + run: | + if [ "$HEAD_REF" != "refs/heads/main" ]; then + echo "Dispatch releases must run from main; got $HEAD_REF" >&2 + exit 1 + fi + if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "Version must look like v1.2.3; got '$TAG'" >&2 + exit 1 + fi + if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists" >&2 + exit 1 + fi + git tag "$TAG" + git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/$TAG" + + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag }} + run: gh release create "$TAG" --generate-notes --verify-tag diff --git a/.gitignore b/.gitignore index f2fffe46..5174101a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,3 @@ tempvendor # Database data - -# Charts temp dir -.chart diff --git a/README.md b/README.md index c1b5360d..99ec9e77 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,9 @@ run the integration and end-to-end tests. ## Testing -We test multiple modules by default. Therefore, if you wish to test a specific -package, you must specify the `MODULE` env variable. See the examples below. +`just test` runs the Go unit tests for the whole module. Any arguments are +passed through to `go test`, so a package pattern scopes the run to specific +packages. Examples: @@ -67,23 +68,17 @@ Examples: # Run all Go tests just test -# Run all Go tests twice -just test -count 2 ./... - # Run all tests once (no cached results) just test -count 1 ./... # Run with verbose output just test -v ./... -# Run the "TestNewDebugLog" test twice with verbose output -MODULE=pkg/rslog just test -v -count 2 github.com/rstudio/platform-lib/pkg/rslog/debug -testify.m=TestNewDebugLog - -# Run the LocalNotifySuite suite tests with verbose output -MODULE=pkg/rsnotify just test -v github.com/rstudio/platform-lib/pkg/rsnotify/locallistener -check.f=LocalNotifySuite +# Test a single package subtree +just test ./pkg/rslog/... -# Run the PgxNotifySuite suite tests with docker-compose -MODULE=pkg/rsnotify just test-integration -v github.com/rstudio/platform-lib/pkg/rsnotify/pgxlistener -check.f=PgxNotifySuite +# Run one gocheck suite in a package, verbose +just test -v ./pkg/rsnotify/listeners/local/... -check.f=LocalNotifySuite ``` ### Testing with Docker @@ -102,23 +97,30 @@ just test-integration To update `NOTICE.md` with a list of licenses from third-party Go modules, use the `just licenses` target. This requires Python 3. -## Versioning +## Release -Follow semantic versioning guidelines. To release a new version, we simply -create and push a tag. +The repo is a single Go module (`github.com/rstudio/platform-lib/v4`), so a +single tag versions the whole library. Follow semantic versioning. + +To release, tag the merge commit on `main` and push it. The `Release` workflow +(`.github/workflows/release.yml`) then publishes a GitHub Release with an +auto-generated changelog. ```shell -git tag v0.1.2 -git push origin v0.1.2 +git tag v4.3.0 +git push origin v4.3.0 ``` -The `rslog` package is versioned separately. To release a new `rslog` version: +Or run the workflow manually to create the tag and release in one step, +without touching local git: ```shell -git tag pkg/rslog/v1.6.1 -git push origin pkg/rslog/v1.6.1 +gh workflow run release.yml -f version=v4.3.1 ``` +A major bump (e.g. v3 to v4) also requires updating the `/vN` suffix in the +module path and all import paths, not just the tag. + ## Badges * [Go Report Card](https://goreportcard.com/) - It will scan your code with diff --git a/justfile b/justfile index 2cc86362..fd4e0fde 100644 --- a/justfile +++ b/justfile @@ -72,13 +72,7 @@ bash: clean: rm -rf out/ rm -rf data/ - rm -rf .chart/ # generate Go dependencies' licenses file licenses: ./scripts/go-licenses.sh - -# enumerate latest tags for each module -tags: versions -versions: - ./scripts/latest-tags.sh diff --git a/pkg/README.md b/pkg/README.md index d99945e9..5254fa5b 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -16,20 +16,9 @@ It's also a way to group Go code in one place when your root directory contains lots of non-Go components and directories making it easier to run various Go tools. -## Go Modules +## Go Module -Each subdirectory of `/pkg` is a separate Go module and should contain its own -`go.mod`. You can release an individual module easily by creating a tag that -contains the subdirectory. For example, to release `rslog` `1.0.1`, you would -create a tag like this: - -```bash -git tag pkg/rslog/v1.0.1 -git push origin --tags -``` - -Before creating a tag, ensure that the `go.mod` has been updated with the -correct dependency versions for other `platform-lib` packages. For example, -the local listener module at `/pkg/rsnotify/listeners/local` depends upon the -`/pkg/rsnotify` module, so we must ensure that the local listener's `go.mod` -references the correct `rsnotify` version. +Everything under `/pkg` is part of the single root module +(`github.com/rstudio/platform-lib/v4`) and is versioned by the repo's top-level +tags. See the "Release" section of the top-level [README](../README.md) for how +releases are cut. diff --git a/pkg/rscache/internal/integration_test/README.md b/pkg/rscache/internal/integration_test/README.md index cb05207f..e8958693 100644 --- a/pkg/rscache/internal/integration_test/README.md +++ b/pkg/rscache/internal/integration_test/README.md @@ -3,10 +3,9 @@ ## Description Includes unit tests that exercise the cache against a file storage server -implemented in `pkg/rsstorage/servers/file`. Since these tests must import -the file server module, we package them separately so other -applications don't need to import all the implementations and their -dependencies. +implemented in `pkg/rsstorage/servers/file`. Since these tests import the +file server implementation, they live in this `internal` package so other +applications don't pull in the implementations and their dependencies. These tests can be run with: @@ -14,6 +13,6 @@ These tests can be run with: # All tests just test -# All tests in this module -MODULE=pkg/rscache/internal/integration_test just test +# Just these tests +just test ./pkg/rscache/internal/integration_test/... ``` diff --git a/pkg/rsnotify/README.md b/pkg/rsnotify/README.md index f9e5524d..aed22b0e 100644 --- a/pkg/rsnotify/README.md +++ b/pkg/rsnotify/README.md @@ -18,7 +18,7 @@ specific notification that matches a filter. ## Implementations -Each listener implementation is a separate Go module. +Each listener implementation is a separate package. ### listeners/local diff --git a/pkg/rsstorage/internal/integration_test/README.md b/pkg/rsstorage/internal/integration_test/README.md index d76bad31..0f37f0e8 100644 --- a/pkg/rsstorage/internal/integration_test/README.md +++ b/pkg/rsstorage/internal/integration_test/README.md @@ -3,10 +3,9 @@ ## Description Includes unit tests that exercise all the storage servers -implemented in `pkg/rsstorage/servers/*`. Since these tests must import -all the implementation modules, we package them separately so other -applications don't need to import all the implementations and their -dependencies. +implemented in `pkg/rsstorage/servers/*`. Since these tests import all the +storage server implementations, they live in this `internal` package so other +applications don't pull in those implementations and their dependencies. These tests can be run with: @@ -14,6 +13,6 @@ These tests can be run with: # All tests just test-integration -# All tests in this module -MODULE=pkg/rsstorage/internal/integration_test just test-integration +# Just these tests +just test-integration ./pkg/rsstorage/internal/integration_test/... ``` diff --git a/scripts/generate-chart-data.sh b/scripts/generate-chart-data.sh deleted file mode 100755 index b8741761..00000000 --- a/scripts/generate-chart-data.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash - -# Constants -MODDIR=".chart" -MODFILE="${MODDIR}/go.mod" -GODIR="${MODDIR}/cmd/chart" -GOFILE="$GODIR/main.go" -GOFILETEST="$GODIR/main_test.go" -GOVER='1.17' -COPYRIGHT='// Copyright (C) 2022 by RStudio, PBC.' - -# Enumerate module latest versions -mods=$(./scripts/latest-tags.sh) - -# Set the field separator to new line -IFS=$'\n' - -# Generates a list of required platform-lib dependencies -# for the go.mod. -function requires { - for mod in ${mods}; do - echo " github.com/rstudio/platform-lib/${mod}" - done -} - -# Build the go.mod file -function generate_go_mod { -cat < ${MODFILE} -module github.com/rstudio/platform-lib/chart - -go $GOVER - -require ( -$(requires) -) -EOT -} - -# Generates a list of imports for main.go -function imports { - ( - cd $MODDIR || exit - for mod in ${mods}; do - # Get the module and version - ver=$(echo "${mod}" | cut -d' ' -f2) - mod=$(echo "${mod}" | cut -d' ' -f1) - - # Populate Go cache - go get "github.com/rstudio/platform-lib/${mod}@$ver" - - # Generate import. We have to use `go list` to list importable - # packages in the module since some modules have nothing in - # the root directory. For example, - # "github.com/rstudio/platform-lib/v2/pkg/rsqueue" cannot be imported, - # but "github.com/rstudio/platform-lib/v2/pkg/rsqueue/agent" can be. - import=$(go list "github.com/rstudio/platform-lib/${mod}/..." | head -1) - # Occasionally, we'll end up with an empty/blank import. I'm not sure - # why, but it seems safe to ignore it. - if [[ "$import" != "" ]]; then - echo " _ \"$import\"" - fi - done - ) -} - -# Build the main.go file -function generate_main { -cat < ${GOFILE} -package main - -${COPYRIGHT} - -import ( -$(imports | sort) -) - -func main() {} -EOT - -cat < ${GOFILETEST} -package main - -${COPYRIGHT} - -import ( - "testing" - - "gopkg.in/check.v1" -) - -func TestPackage(t *testing.T) { check.TestingT(t) } -EOT -} - -# Create required directories -mkdir -p ${GODIR} - -# Generate go.mod and cmd/chart/main.go -generate_go_mod -generate_main - -# Format go file -go fmt ${GOFILE} - -# Tidy modules -( - cd ${MODDIR} || exit - go mod tidy -) diff --git a/scripts/generate-chart.sh b/scripts/generate-chart.sh deleted file mode 100755 index a7709546..00000000 --- a/scripts/generate-chart.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# Constants -MODDIR=".chart" - -# Install `gomod` if needed. -if ! which gomod; then - go install github.com/Helcaraxan/gomod@v0.6.2 -fi - -( - cd $MODDIR || exit - - # Generate graph data. - rm -f graph.dot.1 - gomod graph -a "rdeps(github.com/rstudio/platform-lib/pkg/...)" -o graph.dot.1 - - # grep: Remove references to the temp "chart" package. - # sed: Remove the "github.com/rstudio/platform-lib/pkg/" prefix. - < graph.dot.1 \ - grep -v github.com/rstudio/platform-lib/chart | \ - sed -e 's/github.com\/rstudio\/platform-lib\/pkg\///g' \ - > graph.dot -) - -# graphviz required -if ! which dot; then - echo "please install graphviz" - exit 1 -fi - -# Generate graph -dot -Tsvg -Kcirco -ograph.svg $MODDIR/graph.dot diff --git a/scripts/latest-tags.sh b/scripts/latest-tags.sh deleted file mode 100755 index c70145ad..00000000 --- a/scripts/latest-tags.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -# Refresh tags -git fetch --tags - -# Find all `go.mod` files and trim the leading "./" and the trailing "/go.mod". -# This leaves us with the package name, like "pkg/rsqueue/impls/database". -mods=$(find ./pkg -name go.mod | sed -e 's/^\.\///' | sed -e 's/\/go\.mod//') - -function versions_for_module { - mod=$1 - - # Get tags matching the module - vers=$(git tag -l | grep $mod) - - # For each version in the module - for ver in $vers; do - # Trim off everything after and including the last slash. - modonly=$(echo $ver | sed 's/\(.*\)\/.*/\1/') - - # If the trimmed module matches the module, then get the versions. - # This prevents false positive matches with submodules. For example, - # "pkg/rsnotify" will not match "pkg/rsnotify/listeners/local". - if [[ $modonly == $mod ]]; then - - # The version is everything after the last "/". - veronly=$(echo $ver | rev | cut -d/ -f1 | rev) - echo $modonly $veronly - fi - done -} - -# For each module, find the latest version -for mod in $mods; do - # List all versions for the module, sort, and print the last one. - versions_for_module $mod | sort | tail -1 -done