diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5389db40..bec90a80 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '^1.20' + go-version: '^1.22' - uses: replicatedhq/action-install-pact@v1 - run: make test - if: github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository ) @@ -31,6 +31,6 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '^1.20' + go-version: '^1.22' - name: make build run: make build diff --git a/.github/workflows/working.yml b/.github/workflows/working.yml index e72b16ec..fd5aa24a 100644 --- a/.github/workflows/working.yml +++ b/.github/workflows/working.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '^1.20' + go-version: '^1.22' - uses: replicatedhq/action-install-pact@v1 - name: make test run: make test @@ -27,6 +27,6 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '^1.20' + go-version: '^1.22' - name: make build run: make build diff --git a/.goreleaser-nodocker.yaml b/.goreleaser-nodocker.yaml deleted file mode 100644 index fd790dbb..00000000 --- a/.goreleaser-nodocker.yaml +++ /dev/null @@ -1,53 +0,0 @@ -version: 2 - -project_name: cli -release: - github: - name: replicated - owner: replicatedhq -brews: -- homepage: https://docs.replicated.com/reference/replicated-cli-installing - description: "Package Replicated applications and manage releases, channels, customers and entitlements using a command-line interface." - repository: - name: replicatedhq/replicated - owner: replicatedhq - branch: main - install: bin.install "replicated" - directory: HomebrewFormula -universal_binaries: -- ids: - - cli - replace: true - name_template: replicated -builds: -- goos: - - linux - - darwin - goarch: - - amd64 - - "386" - env: - - CGO_ENABLED=0 - main: cli/main.go - ldflags: -s -w - -X github.com/replicatedhq/replicated/pkg/version.version={{.Version}} - -X github.com/replicatedhq/replicated/pkg/version.gitSHA={{.FullCommit}} - -X github.com/replicatedhq/replicated/pkg/version.buildTime={{.Date}} - -extldflags "-static" - flags: -tags netgo -installsuffix netgo - binary: replicated - hooks: {} -archives: -- format: tar.gz - name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - files: - - licence* - - LICENCE* - - license* - - LICENSE* - - readme* - - README* - - changelog* - - CHANGELOG* -snapshot: - version_template: SNAPSHOT-{{ .Commit }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ae837f1c..63232c26 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -51,10 +51,4 @@ archives: - CHANGELOG* snapshot: version_template: SNAPSHOT-{{ .Commit }} -dockers: - - dockerfile: deploy/Dockerfile - image_templates: - - "replicated/vendor-cli:latest" - - "replicated/vendor-cli:{{ .Major }}" - - "replicated/vendor-cli:{{ .Major }}.{{ .Minor }}" - - "replicated/vendor-cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}" + diff --git a/Makefile b/Makefile index 0f585116..7823aa6d 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,10 @@ API_PKGS=apps channels releases -VERSION=$(shell git describe) -ABBREV_VERSION=$(shell git describe --abbrev=0) -VERSION_PACKAGE = github.com/replicatedhq/replicated/pkg/version -DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` BUILDTAGS = containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp export GO111MODULE=on -GIT_TREE = $(shell git rev-parse --is-inside-work-tree 2>/dev/null) -ifneq "$(GIT_TREE)" "" -define GIT_UPDATE_INDEX_CMD -git update-index --assume-unchanged -endef -define GIT_SHA -`git rev-parse HEAD` -endef -else -define GIT_UPDATE_INDEX_CMD -echo "Not a git repo, skipping git update-index" -endef -define GIT_SHA -"" -endef -endif - -define LDFLAGS --ldflags "\ - -X ${VERSION_PACKAGE}.version=${VERSION} \ - -X ${VERSION_PACKAGE}.gitSHA=${GIT_SHA} \ - -X ${VERSION_PACKAGE}.buildTime=${DATE} \ -" -endef +export CGO_ENABLED=0 .PHONY: test-unit test-unit: @@ -138,4 +111,8 @@ docs: .PHONE: release release: - dagger call release --one-password-service-account env:OP_SERVICE_ACCOUNT_PRODUCTION --version $(version) + dagger call release \ + --one-password-service-account-production env:OP_SERVICE_ACCOUNT_PRODUCTION \ + --version $(version) \ + --github-token env:GITHUB_TOKEN \ + --progress plain diff --git a/dagger/release.go b/dagger/release.go index 4fa6d7f8..a3213486 100644 --- a/dagger/release.go +++ b/dagger/release.go @@ -4,8 +4,10 @@ import ( "context" "dagger/replicated/internal/dagger" "encoding/json" + "errors" "fmt" "net/http" + "strings" "github.com/Masterminds/semver" ) @@ -27,32 +29,86 @@ func (r *Replicated) Release( clean bool, onePasswordServiceAccountProduction *dagger.Secret, + + githubToken *dagger.Secret, ) error { - gitTreeOK, err := checkGitTree(ctx, source) + gitTreeOK, err := checkGitTree(ctx, source, githubToken) if err != nil { return err } if !gitTreeOK { - return fmt.Errorf("git tree is not clean") + return fmt.Errorf("Your git tree is not clean. You cannot release what's not commited.") + } + + latestVersion, err := getLatestVersion(ctx) + if err != nil { + return err } - major, minor, patch, err := parseVersion(ctx, version) + major, minor, patch, err := parseVersion(ctx, latestVersion, version) if err != nil { return err } - _ = dag.Container(). + fmt.Printf("Releasing as version %d.%d.%d\n", major, minor, patch) + + // replace the version in the Makefile + buildFileContent, err := source.File("./pkg/version/build.go").Contents(ctx) + if err != nil { + return err + } + buildFileContent = strings.ReplaceAll(buildFileContent, "const version = \"unknown\"", fmt.Sprintf("const version = \"%d.%d.%d\"", major, minor, patch)) + updatedSource := source.WithNewFile("./pkg/version/build.go", buildFileContent) + + // mount that and commit the updated build.go to git (don't push) + // so that goreleaser won't have a dirty git tree error + gitCommitContainer := dag.Container(). From("alpine/git:latest"). - WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source). + WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource). + WithWorkdir("/go/src/github.com/replicatedhq/replicated"). + WithExec([]string{"git", "config", "user.email", "release@replicated.com"}). + WithExec([]string{"git", "config", "user.name", "Replicated Release Pipeline"}). + WithExec([]string{"git", "add", "pkg/version/build.go"}). + WithExec([]string{"git", "commit", "-m", fmt.Sprintf("Set version to %d.%d.%d", major, minor, patch)}) + _, err = gitCommitContainer.Stdout(ctx) + if err != nil { + return err + } + updatedSource = gitCommitContainer.Directory("/go/src/github.com/replicatedhq/replicated") + + githubTokenPlaintext, err := githubToken.Plaintext(ctx) + if err != nil { + return err + } + tagContainer := dag.Container(). + From("alpine/git:latest"). + WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource). WithWorkdir("/go/src/github.com/replicatedhq/replicated"). - WithExec([]string{"git", "tag", fmt.Sprintf("v%d.%d.%d", major, minor, patch)}). - WithExec([]string{"git", "push", "origin", fmt.Sprintf("v%d.%d.%d", major, minor, patch)}) + WithExec([]string{"git", "remote", "add", "tag", fmt.Sprintf("https://%s@github.com/replicatedhq/replicated.git", githubTokenPlaintext)}). + With(CacheBustingExec([]string{"git", "tag", fmt.Sprintf("v%d.%d.%d", major, minor, patch)})). + With(CacheBustingExec([]string{"git", "push", "tag", fmt.Sprintf("v%d.%d.%d", major, minor, patch)})) + _, err = tagContainer.Stdout(ctx) + if err != nil { + return err + } - replicatedBinary := dag.Container(). + // copy the source that has the tag included in it + updatedSource = tagContainer.Directory("/go/src/github.com/replicatedhq/replicated") + + goModCache := dag.CacheVolume("replicated-go-mod-122") + goBuildCache := dag.CacheVolume("replicated-go-build-121") + + replicatedBinary := dag.Container(dagger.ContainerOpts{ + Platform: "linux/amd64", + }). From("golang:1.22"). - WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source). + WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource). WithWorkdir("/go/src/github.com/replicatedhq/replicated"). - WithExec([]string{"make", "build"}). + WithMountedCache("/go/pkg/mod", goModCache). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", goBuildCache). + WithEnvVariable("GOCACHE", "/go/build-cache"). + With(CacheBustingExec([]string{"make", "build"})). File("/go/src/github.com/replicatedhq/replicated/bin/replicated") dockerContainer := dag.Container(). @@ -65,6 +121,10 @@ func (r *Replicated) Release( WithWorkdir("/out"). WithEntrypoint([]string{"/replicated"}). WithFile("/replicated", replicatedBinary) + _, err = dockerContainer.Stdout(ctx) + if err != nil { + return err + } username, err := dag.Onepassword().FindSecret( onePasswordServiceAccountProduction, @@ -96,12 +156,17 @@ func (r *Replicated) Release( panic(err) } + goreleaserContainer := dag.Goreleaser(dagger.GoreleaserOpts{ + Version: goreleaserVersion, + }).Ctr().WithSecretVariable("GITHUB_TOKEN", githubToken) + if snapshot { _, err := dag. Goreleaser(dagger.GoreleaserOpts{ Version: goreleaserVersion, + Ctr: goreleaserContainer, }). - WithSource(source). + WithSource(updatedSource). Snapshot(ctx, dagger.GoreleaserSnapshotOpts{ Clean: clean, }) @@ -112,8 +177,9 @@ func (r *Replicated) Release( _, err := dag. Goreleaser(dagger.GoreleaserOpts{ Version: goreleaserVersion, + Ctr: goreleaserContainer, }). - WithSource(source). + WithSource(updatedSource). Release(ctx, dagger.GoreleaserReleaseOpts{ Clean: clean, }) @@ -125,11 +191,7 @@ func (r *Replicated) Release( return nil } -func parseVersion(ctx context.Context, version string) (int64, int64, int64, error) { - latestVersion, err := getLatestVersion(ctx) - if err != nil { - return 0, 0, 0, err - } +func parseVersion(ctx context.Context, latestVersion string, version string) (int64, int64, int64, error) { parsedLatestVersion, err := semver.NewVersion(latestVersion) if err != nil { return 0, 0, 0, err @@ -168,8 +230,14 @@ func getLatestVersion(ctx context.Context) (string, error) { return release.TagName, nil } +var ( + ErrGitTreeNotClean = errors.New("Your git tree is not clean. You cannot release what's not commited.") + ErrMainBranch = errors.New("You must be on the main branch to release") + ErrCommitNotInGitHub = errors.New("You must merge your changes into the main branch before releasing") +) + // checkGitTree will return true if the local git tree is clean -func checkGitTree(ctx context.Context, source *dagger.Directory) (bool, error) { +func checkGitTree(ctx context.Context, source *dagger.Directory, githubToken *dagger.Secret) (bool, error) { container := dag.Container(). From("alpine/git:latest"). WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source). @@ -181,8 +249,67 @@ func checkGitTree(ctx context.Context, source *dagger.Directory) (bool, error) { return false, err } - if len(output) == 0 { - return true, nil + if len(output) > 0 { + return false, ErrGitTreeNotClean + } + + container = dag.Container(). + From("alpine/git:latest"). + WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source). + WithWorkdir("/go/src/github.com/replicatedhq/replicated"). + WithExec([]string{"git", "branch"}) + + output, err = container.Stdout(ctx) + if err != nil { + return false, err + } + + if !strings.Contains(output, "* main") { + return false, ErrMainBranch + } + + container = dag.Container(). + From("alpine/git:latest"). + WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source). + WithWorkdir("/go/src/github.com/replicatedhq/replicated"). + WithExec([]string{"git", "rev-parse", "HEAD"}) + + commit, err := container.Stdout(ctx) + if err != nil { + return false, err + } + + req, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/replicatedhq/replicated/commits/%s", commit), nil) + if err != nil { + return false, err + } + + githubTokenPlaintext, err := githubToken.Plaintext(ctx) + if err != nil { + return false, err + } + + req.Header.Set("Authorization", fmt.Sprintf("token %s", githubTokenPlaintext)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + type GitHubResponse struct { + SHA string `json:"sha"` + NodeID string `json:"node_id"` + Status string `json:"status"` + } + + var ghResp GitHubResponse + if err := json.NewDecoder(resp.Body).Decode(&ghResp); err != nil { + return false, err + } + + if ghResp.Status == "422" { + return false, ErrCommitNotInGitHub } return false, nil diff --git a/pkg/version/build.go b/pkg/version/build.go index a79e72b1..9865d720 100644 --- a/pkg/version/build.go +++ b/pkg/version/build.go @@ -1,7 +1,3 @@ package version -// NOTE: these variables are injected at build time - -var ( - version, gitSHA, buildTime string -) +const version = "unknown" diff --git a/pkg/version/version.go b/pkg/version/version.go index 58651bc8..fa2e1015 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,8 +3,6 @@ package version import ( "fmt" "os" - "runtime" - "time" "github.com/pkg/errors" ) @@ -15,35 +13,15 @@ var ( // Build holds details about this build of the replicated cli binary type Build struct { - Version string `json:"version,omitempty"` - GitSHA string `json:"git,omitempty"` - BuildTime time.Time `json:"buildTime,omitempty"` - TimeFallback string `json:"buildTimeFallback,omitempty"` - GoInfo GoInfo `json:"go,omitempty"` - UpdateInfo *UpdateInfo `json:"updateInfo,omitempty"` -} - -type GoInfo struct { - Version string `json:"version,omitempty"` - Compiler string `json:"compiler,omitempty"` - OS string `json:"os,omitempty"` - Arch string `json:"arch,omitempty"` + Version string `json:"version,omitempty"` + UpdateInfo *UpdateInfo `json:"updateInfo,omitempty"` } // initBuild sets up the version info from build args func initBuild() { build.Version = version - if len(gitSHA) >= 7 { - build.GitSHA = gitSHA[:7] - } - var err error - build.BuildTime, err = time.Parse(time.RFC3339, buildTime) - if err != nil { - build.TimeFallback = buildTime - } - - build.GoInfo = getGoInfo() + var err error updateChecker, err := NewUpdateChecker(build.Version, "replicatedhq/replicated/cli") if err != nil { return @@ -61,35 +39,14 @@ func initBuild() { } } -// GetBuild gets the build func GetBuild() Build { return build } -// Version gets the version func Version() string { return build.Version } -// GitSHA gets the gitsha -func GitSHA() string { - return build.GitSHA -} - -// BuildTime gets the build time -func BuildTime() time.Time { - return build.BuildTime -} - -func getGoInfo() GoInfo { - return GoInfo{ - Version: runtime.Version(), - Compiler: runtime.Compiler, - OS: runtime.GOOS, - Arch: runtime.GOARCH, - } -} - func Print() { fmt.Printf("replicated version %s\n", build.Version) diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go deleted file mode 100644 index 23a204cd..00000000 --- a/pkg/version/version_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package version - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestVersion(t *testing.T) { - tests := []struct { - name string - want string - }{ - { - name: "empty", - want: "", - }, - { - name: "version string", - want: "v0.1.2", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := require.New(t) - - version = tt.want - initBuild() - - got := Version() - req.Equal(tt.want, got) - }) - } -} - -func TestGitSHA(t *testing.T) { - tests := []struct { - name string - sha string - want string - }{ - { - name: "empty", - sha: "", - want: "", - }, - { - name: "too short", - sha: "123456", - want: "", - }, - { - name: "7 chars", - sha: "1234567", - want: "1234567", - }, - { - name: "full sha", - sha: "e21cf800acca2aa972e7f5f65f7134b5da92f05f", - want: "e21cf80", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := require.New(t) - - gitSHA = tt.sha - initBuild() - - got := GitSHA() - req.Equal(tt.want, got) - }) - } -} - -func TestBuildTime(t *testing.T) { - req := require.New(t) - aTime, err := time.Parse(time.RFC3339, "2019-06-26T18:53:19Z") - req.NoError(err, "parse constant time") - - tests := []struct { - name string - timestring string - want time.Time - }{ - { - name: "empty", - timestring: "", - want: time.Time{}, - }, - { - name: "proper format", - timestring: "2019-06-26T18:53:19Z", - want: aTime, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := require.New(t) - - buildTime = tt.timestring - initBuild() - - got := BuildTime() - req.Equal(tt.want, got) - }) - } -} - -func TestGetBuild(t *testing.T) { - tests := []struct { - name string - gitSHA string - version string - buildTime string - want Build - }{ - { - name: "goInfo", - gitSHA: "12345678", - want: Build{ - GitSHA: "1234567", - GoInfo: getGoInfo(), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := require.New(t) - - version = tt.version - gitSHA = tt.gitSHA - buildTime = tt.buildTime - - initBuild() - - got := GetBuild() - req.Equal(tt.want, got) - }) - } -}