Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: automate release issue creation #12741

Merged
merged 11 commits into from
Dec 6, 2024
71 changes: 71 additions & 0 deletions .github/workflows/create-release-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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
- run: go build -o release ./cmd/release
- env:
GITHUB_TOKEN: ${{ github.token }}
run: |
./release create-issue \
BigLep marked this conversation as resolved.
Show resolved Hide resolved
--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"
sprig "github.com/Masterminds/sprig/v3"
BigLep marked this conversation as resolved.
Show resolved Hide resolved
"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.Printf(debugFormat, issueTitle, issueBody, issueURL)
BigLep marked this conversation as resolved.
Show resolved Hide resolved
} 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.Println("Issue created: ", issue.GetHTMLURL())
}

return nil
},
},
},
}

Expand Down
Loading
Loading