blob: 6cd62eb766c760362bfc5ecdc51ff367bbd74491 [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package task
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/go-github/v74/github"
"golang.org/x/build/internal/relui/groups"
wf "golang.org/x/build/internal/workflow"
"golang.org/x/mod/semver"
)
// ReleaseGovulncheckActionTasks provides workflow definitions and tasks for
// tagging the govulncheck-action repository.
type ReleaseGovulncheckActionTasks struct {
Gerrit DestructiveGerritClient // destructive needed to overwrite moving major tag (e.g., "v1")
GitHub GitHubClientInterface
ApproveAction func(*wf.TaskContext) error
}
// NewDefinition returns a workflow definition for releasing a new version of
// the govulncheck-action repository.
func (x *ReleaseGovulncheckActionTasks) NewDefinition() *wf.Definition {
wd := wf.New(wf.ACL{Groups: []string{groups.SecurityTeam}})
version := wf.Param(wd, wf.ParamDef[string]{
Name: "Version",
Example: "v1.0.0",
Check: CheckSemver,
})
latest := wf.Task0(wd, "find latest version", x.findLatestVersion)
validated := wf.Action2(wd, "validate version", x.validateVersion, version, latest)
commit := wf.Task2(wd, "read branch head", x.Gerrit.ReadBranchHead, wf.Const("govulncheck-action"), wf.Const("master"))
approved := wf.Action3(wd, "wait for approval", x.approveTagging, version, commit, latest, wf.After(validated))
tag := wf.Action2(wd, "tag repository", x.tagRepository, version, commit, wf.After(approved))
majorTag := wf.Action2(wd, "update major tag", x.updateMajorTag, version, commit, wf.After(tag))
awaitTag := wf.Action1(wd, "wait for GitHub tag sync", x.awaitGitHubTag, version, wf.After(majorTag))
githubRelease := wf.Action1(wd, "create GitHub release", x.createGitHubRelease, version, wf.After(awaitTag))
done := wf.Task1(wd, "await GitHub release", func(_ context.Context, v string) (string, error) {
return v, nil
}, version, wf.After(githubRelease))
wf.Output(wd, "tagged version", done)
return wd
}
func (x *ReleaseGovulncheckActionTasks) validateVersion(ctx *wf.TaskContext, new, latest string) error {
if latest == "" {
return nil
}
if semver.Compare(new, latest) <= 0 {
return fmt.Errorf("proposed version %q is not greater than latest version %q", new, latest)
}
majorNew, minorNew, patchNew, err := parseSemver(new)
if err != nil {
return err
}
majorLatest, minorLatest, patchLatest, err := parseSemver(latest)
if err != nil {
return err
}
if majorNew == majorLatest+1 {
if minorNew != 0 || patchNew != 0 {
return fmt.Errorf("proposed major jump %q must reset minor and patch to 0", new)
}
return nil
}
if majorNew == majorLatest {
if minorNew == minorLatest+1 {
if patchNew != 0 {
return fmt.Errorf("proposed minor jump %q must reset patch to 0", new)
}
return nil
}
if minorNew == minorLatest {
if patchNew != patchLatest+1 {
return fmt.Errorf("proposed patch %q is not exactly %d.%d.%d", new, majorLatest, minorLatest, patchLatest+1)
}
return nil
}
return fmt.Errorf("proposed minor version %d is not valid (latest minor is %d)", minorNew, minorLatest)
}
return fmt.Errorf("proposed major version %q is not valid (latest major is %q)", semver.Major(new), semver.Major(latest))
}
func parseSemver(v string) (major, minor, patch int, err error) {
// Trim any pre-release or build metadata for parsing base components
base := v
if i := strings.IndexAny(v, "-+"); i != -1 {
base = v[:i]
}
if _, err = fmt.Sscanf(base, "v%d.%d.%d", &major, &minor, &patch); err != nil {
return 0, 0, 0, fmt.Errorf("invalid semver %q: %v", v, err)
}
return major, minor, patch, nil
}
func parseMajor(v string) (int, error) {
var n int
if _, err := fmt.Sscanf(v, "v%d", &n); err != nil {
return 0, fmt.Errorf("invalid major version %q: %v", v, err)
}
return n, nil
}
func (x *ReleaseGovulncheckActionTasks) findLatestVersion(ctx *wf.TaskContext) (string, error) {
tags, err := x.Gerrit.ListTags(ctx, "govulncheck-action")
if err != nil {
return "", err
}
latest := ""
for _, t := range tags {
if !semver.IsValid(t) {
continue
}
if latest == "" || semver.Compare(t, latest) > 0 {
latest = t
}
}
if latest == "" {
ctx.Printf("No existing tags found.")
} else {
ctx.Printf("Latest version found: %s", latest)
}
return latest, nil
}
func (x *ReleaseGovulncheckActionTasks) approveTagging(ctx *wf.TaskContext, version string, commit string, latest string) error {
if latest != "" {
ctx.Printf("Latest version tag: %s", latest)
} else {
ctx.Printf("No previous version found.")
}
ctx.Printf("Tagging govulncheck-action as %s at commit %s.\n", version, commit)
ctx.Printf("This action requires approval.")
return x.ApproveAction(ctx)
}
func (x *ReleaseGovulncheckActionTasks) tagRepository(ctx *wf.TaskContext, version string, commit string) error {
ctx.Printf("Tagging govulncheck-action as %s at commit %s", version, commit)
ctx.DisableRetries()
return x.Gerrit.Tag(ctx, "govulncheck-action", version, commit)
}
func (x *ReleaseGovulncheckActionTasks) updateMajorTag(ctx *wf.TaskContext, version string, commit string) error {
major := semver.Major(version)
if major == "" {
return fmt.Errorf("invalid version %q: no major version", version)
}
ctx.Printf("Updating major tag %s to point to %s", major, commit)
ctx.DisableRetries() // Manual retries beyond this point.
return x.Gerrit.ForceTag(ctx, "govulncheck-action", major, commit)
}
func (x *ReleaseGovulncheckActionTasks) awaitGitHubTag(ctx *wf.TaskContext, tag string) error {
ctx.Printf("Waiting for tag %s to be mirrored to GitHub", tag)
tctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
defer cancel()
_, err := AwaitCondition(ctx, 15*time.Second, func() (string, bool, error) {
exists, err := x.GitHub.TagExists(tctx, "golang", "govulncheck-action", tag)
return "", exists, err
})
return err
}
func (x *ReleaseGovulncheckActionTasks) createGitHubRelease(ctx *wf.TaskContext, version string) error {
ctx.DisableRetries()
ctx.Printf("Creating GitHub release for %s", version)
_, err := x.GitHub.CreateRelease(ctx, "golang", "govulncheck-action", &github.RepositoryRelease{
TagName: github.Ptr(version),
Name: github.Ptr(version),
GenerateReleaseNotes: github.Ptr(true),
Draft: github.Ptr(false),
})
return err
}
// CheckSemver reports whether v is a valid semantic version.
func CheckSemver(v string) error {
if !semver.IsValid(v) {
return fmt.Errorf("invalid semver: %q", v)
}
return nil
}