Skip to content

Commit

Permalink
ci: automate release issue creation (#12741)
Browse files Browse the repository at this point in the history
* feat: automate release issue creation

* chore: run go mod tidy

* chore: organize imports in the release cmd

* fix: layout format given to the date parser

* ci: automate release issue creation from v1.32.0 learnings (#12749)

* ci: automate release issue creation from v1.32.0 learnings

* Self-review feedback from looking at #12749

* Fix String contains ordering bugs.

* chore: run gofmt

---------

Co-authored-by: galargh <[email protected]>

* fix: update workflow input parameters

* ci: use custom field types in the create issue workflow

* Update cmd/release/main.go

Co-authored-by: Rod Vagg <[email protected]>

* Update cmd/release/main.go

Co-authored-by: Rod Vagg <[email protected]>

* Incorporating rvagg@ comments

* Incorporating more feedback

---------

Co-authored-by: Steve Loeppky <[email protected]>
Co-authored-by: Rod Vagg <[email protected]>
  • Loading branch information
3 people authored Dec 6, 2024
1 parent b37410b commit dff8bfd
Show file tree
Hide file tree
Showing 6 changed files with 465 additions and 94 deletions.
70 changes: 70 additions & 0 deletions .github/workflows/create-release-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Create Release Issue

on:
workflow_dispatch:
inputs:
type:
description: "What's the type of the release?"
required: true
type: choice
options:
- node
- miner
- both
tag:
description: "What's the tag of the release? (e.g., 1.30.1)"
required: true
level:
description: "What's the level of the release?"
required: true
default: 'warning'
type: choice
options:
- major
- minor
- patch
network-upgrade:
description: "What's the version of the network upgrade this release is related to? (e.g. 25)"
required: false
discussion-link:
description: "What's a link to the GitHub Discussions topic for the network upgrade?"
required: false
changelog-link:
description: "What's a link to the Lotus CHANGELOG entry for the network upgrade?"
required: false
rc1-date:
description: "What's the expected shipping date for RC1?"
required: false
stable-date:
description: "What's the expected shipping date for the stable release?"
required: false

defaults:
run:
shell: bash

permissions:
contents: read
issues: write

jobs:
create-issue:
name: Create Release Issue
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/install-go
- env:
GITHUB_TOKEN: ${{ github.token }}
run: |
go run cmd/release/main.go create-issue \
--create-on-github true \
--type "${{ github.event.inputs.type }}" \
--tag "${{ github.event.inputs.tag }}" \
--level "${{ github.event.inputs.level }}" \
--network-upgrade "${{ github.event.inputs.network-upgrade }}" \
--discussion-link "${{ github.event.inputs.discussion-link }}" \
--changelog-link "${{ github.event.inputs.changelog-link }}" \
--rc1-date "${{ github.event.inputs.rc1-date }}" \
--stable-date "${{ github.event.inputs.stable-date }}" \
--repo "filecoin-project/lotus"
18 changes: 14 additions & 4 deletions cmd/release/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Lotus Release Tool

The Lotus Release Tool is a CLI (Command Line Interface) utility designed to facilitate interactions with the Lotus Node and Miner metadata. This tool allows users to retrieve version information in either JSON or text format. The Lotus Release Tool was developed as a part of the [2024Q2 release process automation initiative](https://github.com/filecoin-project/lotus/issues/12010) and is used in the GitHub Actions workflows related to the release process.
The Lotus Release Tool is a CLI (Command Line Interface) utility designed to facilitate interactions with the Lotus Node and Miner metadata. This tool allows users to retrieve version information in either JSON or text format. The Lotus Release Tool was initially developed as a part of the [2024Q2 release process automation initiative](https://github.com/filecoin-project/lotus/issues/12010) and is used in the GitHub Actions workflows related to the release process.

## Features

- List all projects with their expected version information.
- Create a new release issue from a template.
- Output logs in JSON or text format.

## Installation
Expand All @@ -26,17 +27,26 @@ The `release` tool provides several commands and options to interact with the Lo
```sh
./release list-projects
```
- **Create Issue**: Create a new release issue from a template.
```sh
./release create-issue
```

### Options
### Global Options

- **--json**: Format output as JSON.
```sh
./release --json list-projects
./release --json
```

## Example
## Examples

List Lotus Node and Lotus Miner version information with JSON formatted output:
```sh
./release --json list-projects
```

Create a new release issue from a template:
```sh
./release create-issue --type node --tag 1.30.1 --level patch --network-upgrade --discussion-link https://github.com/filecoin-project/lotus/discussions/12010 --changelog-link https://github.com/filecoin-project/lotus/blob/v1.30.1/CHANGELOG.md --rc1-date 2023-04-01 --rc1-precision day --rc1-confidence confirmed --stable-date 2023-05-01 --stable-precision week --stable-confidence estimated
```
244 changes: 244 additions & 0 deletions cmd/release/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"net/url"
"os"
"os/exec"
"regexp"
"strconv"
"strings"

masterminds "github.com/Masterminds/semver/v3"
"github.com/Masterminds/sprig/v3"
"github.com/google/go-github/v66/github"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"golang.org/x/mod/semver"
Expand Down Expand Up @@ -124,6 +134,8 @@ func getProject(name, version string) project {
}
}

const releaseDateStringPattern = `^(Week of )?\d{4}-\d{2}-\d{2}( \(estimate\))?$`

func main() {
app := &cli.App{
Name: "release",
Expand Down Expand Up @@ -163,6 +175,238 @@ func main() {
return nil
},
},
{
Name: "create-issue",
Usage: "Create a new release issue from the template",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "create-on-github",
Usage: "Whether to create the issue on github rather than print the issue content. $GITHUB_TOKEN must be set.",
Value: false,
},
&cli.StringFlag{
Name: "type",
Usage: "What's the type of the release? (Options: node, miner, both)",
Value: "both",
Required: true,
},
&cli.StringFlag{
Name: "tag",
Usage: "What's the tag of the release? (e.g., 1.30.1)",
Required: true,
},
&cli.StringFlag{
Name: "level",
Usage: "What's the level of the release? (Options: major, minor, patch)",
Value: "patch",
Required: true,
},
&cli.StringFlag{
Name: "network-upgrade",
Usage: "What's the version of the network upgrade this release is related to? (e.g., 25)",
Required: false,
},
&cli.StringFlag{
Name: "discussion-link",
Usage: "What's a link to the GitHub Discussions topic for the network upgrade?",
Required: false,
},
&cli.StringFlag{
Name: "changelog-link",
Usage: "What's a link to the Lotus CHANGELOG entry for the network upgrade?",
Required: false,
},
&cli.StringFlag{
Name: "rc1-date",
Usage: fmt.Sprintf("What's the expected shipping date for RC1? (Pattern: '%s'))", releaseDateStringPattern),
Value: "TBD",
Required: false,
},
&cli.StringFlag{
Name: "stable-date",
Usage: fmt.Sprintf("What's the expected shipping date for the stable release? (Pattern: '%s'))", releaseDateStringPattern),
Value: "TBD",
Required: false,
},
&cli.StringFlag{
Name: "repo",
Usage: "Which full repository name (i.e., OWNER/REPOSITORY) to create the issue under.",
Value: "filecoin-project/lotus",
Required: false,
},
},
Action: func(c *cli.Context) error {
// Read and validate the flag values
createOnGitHub := c.Bool("create-on-github")

releaseType := c.String("type")
switch releaseType {
case "node":
releaseType = "Node"
case "miner":
releaseType = "Miner"
case "both":
releaseType = "Node and Miner"
default:
return fmt.Errorf("invalid value for the 'type' flag. Allowed values are 'node', 'miner', and 'both'")
}

releaseTag := c.String("tag")
releaseVersion, err := masterminds.StrictNewVersion(releaseTag)
if err != nil {
return fmt.Errorf("invalid value for the 'tag' flag. Must be a valid semantic version (e.g. 1.30.1)")
}

releaseLevel := c.String("level")
if releaseLevel != "major" && releaseLevel != "minor" && releaseLevel != "patch" {
return fmt.Errorf("invalid value for the 'level' flag. Allowed values are 'major', 'minor', and 'patch'")
}

networkUpgrade := c.String("network-upgrade")
discussionLink := c.String("discussion-link")
if networkUpgrade != "" {
_, err := strconv.ParseUint(networkUpgrade, 10, 64)
if err != nil {
return fmt.Errorf("invalid value for the 'network-upgrade' flag. Must be a valid uint (e.g. 23)")
}
if discussionLink != "" {
_, err := url.ParseRequestURI(discussionLink)
if err != nil {
return fmt.Errorf("invalid value for the 'discussion-link' flag. Must be a valid URL")
}
}
}

changelogLink := c.String("changelog-link")
if changelogLink != "" {
_, err := url.ParseRequestURI(changelogLink)
if err != nil {
return fmt.Errorf("invalid value for the 'changelog-link' flag. Must be a valid URL")
}
}

rc1Date := c.String("rc1-date")
releaseDateStringRegexp := regexp.MustCompile(releaseDateStringPattern)
if rc1Date != "TBD" {
matches := releaseDateStringRegexp.FindStringSubmatch(rc1Date)
if matches == nil {
return fmt.Errorf("rc1-date must be of form %s", releaseDateStringPattern)
}
}

stableDate := c.String("stable-date")
if stableDate != "TBD" {
matches := releaseDateStringRegexp.FindStringSubmatch(stableDate)
if matches == nil {
return fmt.Errorf("stable-date must be of form %s", releaseDateStringPattern)
}
}

repoFullName := c.String("repo")
repoRegexp := regexp.MustCompile(`^([^/]+)/([^/]+)$`)
matches := repoRegexp.FindStringSubmatch(repoFullName)
if matches == nil {
return fmt.Errorf("invalid repository name format. Must be 'owner/repo'")
}
repoOwner := matches[1]
repoName := matches[2]

// Prepare template data
data := map[string]any{
"CreateOnGitHub": createOnGitHub,
"Type": releaseType,
"Tag": releaseVersion.String(),
"NextTag": releaseVersion.IncPatch().String(),
"Level": releaseLevel,
"NetworkUpgrade": networkUpgrade,
"NetworkUpgradeDiscussionLink": discussionLink,
"NetworkUpgradeChangelogEntryLink": changelogLink,
"RC1DateString": rc1Date,
"StableDateString": stableDate,
}

// Render the issue template
issueTemplate, err := os.ReadFile("documentation/misc/RELEASE_ISSUE_TEMPLATE.md")
if err != nil {
return fmt.Errorf("failed to read issue template: %w", err)
}
// Sprig used for String contains and Lists
tmpl, err := template.New("issue").Funcs(sprig.FuncMap()).Parse(string(issueTemplate))
if err != nil {
return fmt.Errorf("failed to parse issue template: %w", err)
}
var issueBodyBuffer bytes.Buffer
err = tmpl.Execute(&issueBodyBuffer, data)
if err != nil {
return fmt.Errorf("failed to execute issue template: %w", err)
}

// Prepare issue creation options
issueTitle := fmt.Sprintf("Lotus %s v%s Release", releaseType, releaseTag)
issueBody := issueBodyBuffer.String()

// Remove duplicate newlines before headers and list items since the templating leaves a lot extra newlines around.
// Extra newlines are present because go formatting control statements done within HTML comments rather than using {{- -}}.
// HTML comments are used instead so that the template file parses as clean markdown on its own.
// In addition, HTML comments were also required within "ranges" in the template.
// Using HTML comments everywhere keeps things consistent.
re := regexp.MustCompile(`\n\n+([^#*\[\|])`)
issueBody = re.ReplaceAllString(issueBody, "\n$1")

if !createOnGitHub {
// Create the URL-encoded parameters
params := url.Values{}
params.Add("title", issueTitle)
params.Add("body", issueBody)
params.Add("labels", "tpm")

// Construct the URL
issueURL := fmt.Sprintf("https://github.com/%s/issues/new?%s", repoFullName, params.Encode())

debugFormat := `
Issue Details:
=============
Title: %s
Body:
-----
%s
URL to create issue:
-------------------
%s
`
_, _ = fmt.Fprintf(c.App.Writer, debugFormat, issueTitle, issueBody, issueURL)
} else {
// Set up the GitHub client
client := github.NewClient(nil).WithAuthToken(os.Getenv("GITHUB_TOKEN"))

// Check if the issue already exists
issues, _, err := client.Search.Issues(context.Background(), fmt.Sprintf("%s in:title state:open repo:%s is:issue", issueTitle, repoFullName), &github.SearchOptions{})
if err != nil {
return fmt.Errorf("failed to list issues: %w", err)
}
if issues.GetTotal() > 0 {
return fmt.Errorf("issue already exists: %s", issues.Issues[0].GetHTMLURL())
}

// Create the issue
issue, _, err := client.Issues.Create(context.Background(), repoOwner, repoName, &github.IssueRequest{
Title: &issueTitle,
Body: &issueBody,
Labels: &[]string{
"tpm",
},
})
if err != nil {
return fmt.Errorf("failed to create issue: %w", err)
}
_, _ = fmt.Fprintf(c.App.Writer, "Issue created: %s", issue.GetHTMLURL())
}

return nil
},
},
},
}

Expand Down
Loading

0 comments on commit dff8bfd

Please sign in to comment.