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
21 changes: 17 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
name: Release

on:
push:
tags:
- "v*"
release:
types:
- published
workflow_dispatch:
inputs:
tag:
description: "Release tag to build, such as v0.2.0"
required: true

permissions:
contents: write
Expand All @@ -25,9 +29,12 @@ jobs:
goarch: arm64
env:
CGO_ENABLED: "0"
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
steps:
- name: Check out code
uses: actions/checkout@v6
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}

- name: Set up Go
uses: actions/setup-go@v6
Expand All @@ -46,8 +53,13 @@ jobs:
mkdir -p dist
binary_name="actupdate"
asset_dir="dist/actupdate_${GOOS}_${GOARCH}"
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "release tag must be a stable semver tag like v0.2.0" >&2
exit 1
fi
version="${RELEASE_TAG#v}"
mkdir -p "${asset_dir}"
go build -o "${asset_dir}/${binary_name}" ./cmd/actupdate
go build -ldflags "-X main.version=${version}" -o "${asset_dir}/${binary_name}" ./cmd/actupdate
Comment thread
qartik marked this conversation as resolved.
tar -C dist -czf "${asset_dir}.tar.gz" "actupdate_${GOOS}_${GOARCH}"

- name: Upload release artifact
Expand All @@ -74,4 +86,5 @@ jobs:
- name: Publish GitHub release assets
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.event.release.tag_name || inputs.tag }}
files: release-assets/*.tar.gz
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ GITHUB_TOKEN="$(gh auth token)" actupdate

## Releases

Push a tag like `v0.1.0` to trigger the release workflow. It builds and uploads
tarballs for:
Create and publish a GitHub release with a tag like `v0.2.0` from
<https://github.com/qartik/actupdate/releases/new>. The release workflow builds
the binaries from that tag, injects the tag version into `actupdate version`,
and uploads tarballs for:

- `linux/amd64`
- `linux/arm64`
Expand All @@ -72,6 +74,12 @@ tarballs for:
Each release asset is named like `actupdate_linux_amd64.tar.gz` and contains the
`actupdate` binary.

If a release build needs to be rerun, dispatch the release workflow manually and
provide the existing release tag.

Source builds report Go build metadata, such as a tagged version, pseudo-version,
or `devel-<commit>` fallback when no release version is injected.

## Verification Rules

- Only stable semver tags are considered
Expand Down
46 changes: 44 additions & 2 deletions cmd/actupdate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"os"
"path/filepath"
"runtime/debug"
"strings"
"time"

Expand All @@ -21,7 +22,7 @@ import (
"golang.org/x/term"
)

const version = "0.1.0"
var version string

const (
exitOK = iota
Expand All @@ -31,6 +32,7 @@ const (
)

const maxCooldownDays = int64(math.MaxInt64 / int64(24*time.Hour))
const shortRevisionLength = 12

type cliOptions struct {
Repo string
Expand All @@ -54,7 +56,7 @@ func run(args []string, in io.Reader, out, errOut io.Writer, httpClient *http.Cl
return exitInvalidInput
}
if opts == nil {
fmt.Fprintln(out, version)
fmt.Fprintln(out, displayVersion())
return exitOK
}

Expand Down Expand Up @@ -210,6 +212,46 @@ func resolveToken(explicit string) string {
return os.Getenv("GH_TOKEN")
}

func displayVersion() string {
if version != "" {
return version
}
if info, ok := debug.ReadBuildInfo(); ok {
if buildVersion := versionFromBuildInfo(info); buildVersion != "" {
return buildVersion
}
}
return "unknown"
}

func versionFromBuildInfo(info *debug.BuildInfo) string {
if info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
}

var revision string
modified := false
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
revision = setting.Value
case "vcs.modified":
modified = setting.Value == "true"
}
}
if revision == "" {
return ""
}
if len(revision) > shortRevisionLength {
revision = revision[:shortRevisionLength]
}
out := "devel-" + revision
if modified {
out += "+dirty"
}
return out
}

func promptConfirm(in io.Reader, out io.Writer) (bool, error) {
fmt.Fprint(out, "Apply these updates? [Y/n]: ")
reader := bufio.NewReader(in)
Expand Down
36 changes: 35 additions & 1 deletion cmd/actupdate/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime/debug"
"strings"
"testing"
"time"
)

func withVersion(t *testing.T, value string) {
t.Helper()
previous := version
version = value
t.Cleanup(func() {
version = previous
})
}

func TestParseArgsCooldownDays(t *testing.T) {
opts, err := parseArgs([]string{"--cooldown-days", "7"})
if err != nil {
Expand All @@ -36,16 +46,40 @@ func TestParseArgsRejectsOverflowingCooldownDays(t *testing.T) {
}

func TestRunVersion(t *testing.T) {
withVersion(t, "1.2.3")

var stdout bytes.Buffer
exitCode := run([]string{"version"}, strings.NewReader(""), &stdout, &bytes.Buffer{}, http.DefaultClient, "")
if exitCode != exitOK {
t.Fatalf("expected exit 0, got %d", exitCode)
}
if got := strings.TrimSpace(stdout.String()); got != version {
if got := strings.TrimSpace(stdout.String()); got != "1.2.3" {
t.Fatalf("unexpected version output: %q", got)
}
}

func TestVersionFromBuildInfoUsesModuleVersion(t *testing.T) {
got := versionFromBuildInfo(&debug.BuildInfo{
Main: debug.Module{Version: "v0.2.0"},
})
if got != "v0.2.0" {
t.Fatalf("unexpected version: %q", got)
}
}

func TestVersionFromBuildInfoUsesVCSRevisionForDevelopmentBuild(t *testing.T) {
got := versionFromBuildInfo(&debug.BuildInfo{
Main: debug.Module{Version: "(devel)"},
Settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "0123456789abcdef"},
{Key: "vcs.modified", Value: "true"},
},
})
if got != "devel-0123456789ab+dirty" {
t.Fatalf("unexpected version: %q", got)
}
}

func TestRunHelpWithOtherFlags(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
Expand Down