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

Create release command #145

Merged
merged 13 commits into from
Aug 5, 2024
66 changes: 66 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const (
defaultBranchVersionEnvironmentVariable = "BRANCH_VERSION"
// defaultForkEnvironmentVariable is the default environment variable that indicates the fork URL
defaultForkEnvironmentVariable = "FORK"
// defaultChartVersionEnvironmentVariable is the default environment variable that indicates the version to release
defaultChartVersionEnvironmentVariable = ""
)

var (
Expand Down Expand Up @@ -76,6 +78,8 @@ var (
DebugMode = false
// ForkURL represents the fork URL configured as a remote in your local git repository
ForkURL = ""
// ChartVersion of the chart to release
ChartVersion = ""
)

func main() {
Expand Down Expand Up @@ -171,6 +175,18 @@ func main() {
Destination: &ForkURL,
EnvVar: defaultForkEnvironmentVariable,
}
chartVersionFlag := cli.StringFlag{
Name: "version",
Usage: `Usage:
./bin/charts-build-scripts <command> --version="<chart_version>"
VERSION="<chart_version>" make <command>

Target version of chart to release.
`,
Required: true,
Destination: &ChartVersion,
EnvVar: defaultChartVersionEnvironmentVariable,
}
app.Commands = []cli.Command{
{
Name: "list",
Expand Down Expand Up @@ -319,6 +335,13 @@ func main() {
Action: autoForwardPort,
Flags: []cli.Flag{branchVersionFlag, chartFlag, forkFlag},
},
{
Name: "release",
Usage: `Execute the release script to release a chart to the production branch.
`,
Action: release,
Flags: []cli.Flag{branchVersionFlag, chartFlag, chartVersionFlag, forkFlag},
},
}

if err := app.Run(os.Args); err != nil {
Expand Down Expand Up @@ -688,3 +711,46 @@ func autoForwardPort(c *cli.Context) {
}

}

func release(c *cli.Context) {
if ForkURL == "" {
logrus.Fatal("FORK environment variable must be set to run release cmd")
}

if CurrentChart == "" {
logrus.Fatal("CHART environment variable must be set to run release cmd")
}

rootFs := filesystem.GetFilesystem(getRepoRoot())

dependencies, err := lifecycle.InitDependencies(rootFs, c.String("branch-version"), CurrentChart, false)
if err != nil {
logrus.Fatalf("encountered error while initializing dependencies: %v", err)
}

status, err := lifecycle.LoadState(rootFs)
if err != nil {
logrus.Fatalf("could not load state; please run lifecycle-status before this command: %v", err)
}

release, err := auto.InitRelease(dependencies, status, ChartVersion, CurrentChart, ForkURL)
if err != nil {
logrus.Fatalf("failed to initialize release: %v", err)
}

if err := release.PullAsset(); err != nil {
logrus.Fatalf("failed to execute release: %v", err)
}

// Unzip Assets: ASSET=<chart>/<chart>-<version.tgz make unzip
CurrentAsset = release.Chart + "/" + release.AssetTgz
unzipAssets(c)

// update release.yaml
if err := release.UpdateReleaseYaml(); err != nil {
logrus.Fatalf("failed to update release.yaml: %v", err)
}

// make index
createOrUpdateIndex(c)
}
171 changes: 171 additions & 0 deletions pkg/auto/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package auto

import (
"errors"
"fmt"
"io"
"os"

"github.com/rancher/charts-build-scripts/pkg/filesystem"
"github.com/rancher/charts-build-scripts/pkg/git"
"github.com/rancher/charts-build-scripts/pkg/lifecycle"
"github.com/rancher/charts-build-scripts/pkg/path"
"gopkg.in/yaml.v3"
)

// Release holds necessary metadata to release a chart version
type Release struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used outside of this package? If not, please unexport it.

Copy link
Collaborator Author

@nicholasSUSE nicholasSUSE Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not used outside yet, except from main.go file.

But this will be used by auto-forward-port in a future PR.

git *git.Git
VR *lifecycle.VersionRules
AssetTgz string
AssetPath string
ChartVersion string
Chart string
ReleaseYamlPath string
ForkRemoteURL string
}

// InitRelease will create the Release struct with access to the necessary dependencies.
func InitRelease(d *lifecycle.Dependencies, s *lifecycle.Status, v, c, f string) (*Release, error) {
r := &Release{
git: d.Git,
VR: d.VR,
ChartVersion: v,
Chart: c,
ForkRemoteURL: f,
}

var ok bool
var assetVersions []lifecycle.Asset

assetVersions, ok = s.AssetsToBeReleased[r.Chart]
if !ok {
assetVersions, ok = s.AssetsToBeForwardPorted[r.Chart]
if !ok {
return nil, errors.New("no asset version to release for chart:" + r.Chart)
}
}

var assetVersion string
for _, version := range assetVersions {
if version.Version == r.ChartVersion {
assetVersion = version.Version
break
}
}
if assetVersion == "" {
return nil, errors.New("no asset version to release for chart:" + r.Chart + " version:" + r.ChartVersion)
}

r.AssetPath, r.AssetTgz = mountAssetVersionPath(r.Chart, assetVersion)

// Check again if the asset was already released in the local repository
if err := checkAssetReleased(r.AssetPath); err != nil {
return nil, fmt.Errorf("failed to check for chart:%s ; err: %w", r.Chart, err)
}

// Check if we have a release.yaml file in the expected path
if exist, err := filesystem.PathExists(d.RootFs, path.RepositoryReleaseYaml); err != nil || !exist {
return nil, errors.New("release.yaml not found")
}

r.ReleaseYamlPath = filesystem.GetAbsPath(d.RootFs, path.RepositoryReleaseYaml)

return r, nil
}

// PullAsset will execute the release porting for a chart in the repository
func (r *Release) PullAsset() error {
if err := r.git.FetchBranch(r.VR.DevBranch); err != nil {
return err
}

if err := r.git.CheckFileExists(r.AssetPath, r.VR.DevBranch); err != nil {
return fmt.Errorf("asset version not found in dev branch: %w", err)
}

if err := r.git.CheckoutFile(r.VR.DevBranch, r.AssetPath); err != nil {
return err
}

return r.git.ResetHEAD()
}

func checkAssetReleased(chartVersion string) error {
if _, err := os.Stat(chartVersion); err != nil {
return err
}

return nil
}

// mountAssetVersionPath returns the asset path and asset tgz name for a given chart and version.
// example: assets/longhorn/longhorn-100.0.0+up0.0.0.tgz
func mountAssetVersionPath(chart, version string) (string, string) {
assetTgz := chart + "-" + version + ".tgz"
assetPath := "assets/" + chart + "/" + assetTgz
return assetPath, assetTgz
}

func (r *Release) readReleaseYaml() (map[string][]string, error) {
var releaseVersions = make(map[string][]string, 0)

file, err := os.Open(r.ReleaseYamlPath)
if err != nil {
return nil, err
}
defer file.Close()

decoder := yaml.NewDecoder(file)
if err := decoder.Decode(&releaseVersions); err != nil {
if err == io.EOF {
// Handle EOF error gracefully
return releaseVersions, nil
}
return nil, err
}

return releaseVersions, nil
}

// UpdateReleaseYaml reads and parse the release.yaml file to a struct, appends the new version and writes it back to the file.
func (r *Release) UpdateReleaseYaml() error {
releaseVersions, err := r.readReleaseYaml()
if err != nil {
return err
}

// Append new version and remove duplicates if any
releaseVersions[r.Chart] = append(releaseVersions[r.Chart], r.ChartVersion)
releaseVersions[r.Chart] = removeDuplicates(releaseVersions[r.Chart])

// Since we opened and read the file before we can truncate it.
outputFile, err := os.Create(r.ReleaseYamlPath)
if err != nil {
return err
}
defer outputFile.Close()

encoder := yaml.NewEncoder(outputFile)
encoder.SetIndent(2)
if err := encoder.Encode(releaseVersions); err != nil {
return err
}

return nil
}

// removeDuplicates takes a slice of strings and returns a new slice with duplicates removed.
func removeDuplicates(slice []string) []string {
seen := make(map[string]struct{}) // map to keep track of seen strings
var result []string // slice to hold the results

for _, val := range slice {
if _, ok := seen[val]; !ok {
seen[val] = struct{}{} // mark string as seen
result = append(result, val) // append to result if not seen before
}
}

return result
}
Loading
Loading