Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
permissions:
contents: write
id-token: write
attestations: write

jobs:
release:
Expand Down Expand Up @@ -115,6 +116,23 @@ jobs:
echo "--- Binary checksums ---"
cat "$SUMFILE"

# Shell-scoped VER is not visible to YAML expressions in later steps.
echo "VER=${VER}" >> "$GITHUB_ENV"

# Provenance covers the same two sets the cosign steps sign, driven off the
# checksum files rather than a dist/ glob so the subject list cannot drift
# from the signed one. Verification needs no .sig/.pem handling:
# gh attestation verify ./verda --repo verda-cloud/verda-cli
- name: Attest archives and packages
uses: actions/attest-build-provenance@v4
with:
subject-checksums: dist/verda_${{ env.VER }}_SHA256SUMS

- name: Attest binaries
uses: actions/attest-build-provenance@v4
with:
subject-checksums: dist/verda_${{ env.VER }}_binary_SHA256SUMS

- name: Sign checksum files with cosign
# Keyless signing via GitHub OIDC. The certificate identity will be:
# https://github.com/verda-cloud/verda-cli/.github/workflows/release.yml@refs/heads/main
Expand Down
13 changes: 12 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,19 @@ builds:
- id: verda
binary: verda
main: ./cmd/verda/
# buildDate carries .CommitDate, not .Date: the stamp is a property of the
# tag, so re-running a release does not change it, and it matches the
# vcs.time fallback in version.GetFromDebugInfo.
ldflags:
- -s -w -X github.com/verda-cloud/verda-cli/pkg/version.gitVersion={{ .Version }}
- -s -w
- -X github.com/verda-cloud/verda-cli/pkg/version.gitVersion={{ .Version }}
- -X github.com/verda-cloud/verda-cli/pkg/version.gitCommit={{ .FullCommit }}
- -X github.com/verda-cloud/verda-cli/pkg/version.gitTreeState={{ .GitTreeState }}
- -X github.com/verda-cloud/verda-cli/pkg/version.buildDate={{ .CommitDate }}
# -trimpath strips absolute build paths, so the output depends only on the
# tag and not on where CI happened to check out.
flags:
- -trimpath
env:
- CGO_ENABLED=0
goos:
Expand Down
46 changes: 36 additions & 10 deletions internal/verda-cli/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func NewRootCommand(ioStreams cmdutil.IOStreams) (*cobra.Command, *clioptions.Op
var showVersion bool

cmd := &cobra.Command{
Use: "verda",
Use: rootCmdName,
Short: "Command-line interface for Verda Cloud",
Long: cmdutil.LongDesc(`
Command-line interface for Verda Cloud.`),
Expand Down Expand Up @@ -193,6 +193,7 @@ func NewRootCommand(ioStreams cmdutil.IOStreams) (*cobra.Command, *clioptions.Op
doctor.NewCmdDoctor(f, ioStreams),
settings.NewCmdSettings(f, ioStreams),
update.NewCmdUpdate(f, ioStreams),
newVersionCommand(ioStreams),
},
},
)
Expand Down Expand Up @@ -234,7 +235,9 @@ func skipCredentialResolution(cmd *cobra.Command) bool {
return true
case pName == "registry":
return true
case cmd.Name() == "doctor" && pName == "verda":
case cmd.Name() == "doctor" && pName == rootCmdName:
return true
case cmd.Name() == "version" && pName == rootCmdName:
return true
}
return false
Expand All @@ -246,7 +249,7 @@ func shouldCheckVersion(cmd *cobra.Command) bool {
switch cmd.Name() {
case "help":
return true
case "verda":
case rootCmdName:
// Root with no subcommand (prints help then PostRun).
return true
}
Expand All @@ -257,18 +260,41 @@ func shouldCheckVersion(cmd *cobra.Command) bool {
// Callers should check for this error and exit 0 instead of printing it.
var ErrVersionRequested = errors.New("version requested")

// versionOutput returns the formatted version string.
const (
rootCmdName = "verda"
cliModulePath = "github.com/verda-cloud/verda-cli"
sdkModulePath = "github.com/verda-cloud/verdacloud-sdk-go"
)

// versionOutput returns the formatted version string. GetFromDebugInfo (not
// Get) so commit/date survive builds without ldflags — `go install`, `make
// build` — by falling back to the vcs.* stamps Go embeds.
func versionOutput() string {
info := version.Get()
sdkVer := depVersion("github.com/verda-cloud/verdacloud-sdk-go")
return fmt.Sprintf(" Version: %s\n Platform: %s\n SDK: %s\n",
info.GitVersion, info.Platform, sdkVer)
info := version.GetFromDebugInfo(cliModulePath)

out := " Version: " + info.GitVersion + "\n"
if info.GitCommit != unknownVersionValue {
out += " Commit: " + info.GitCommit
if info.GitTreeState == "dirty" {
out += " (dirty)"
}
out += "\n"
}
if info.BuildDate != unknownVersionValue {
out += " Built: " + info.BuildDate + "\n"
}
out += " Platform: " + info.Platform + "\n"
out += " SDK: " + depVersion(sdkModulePath) + "\n"
return out
}

// Mirrors the sentinel pkg/version uses for unstamped build vars.
const unknownVersionValue = "unknown"

func depVersion(modulePath string) string {
bi, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
return unknownVersionValue
}
for _, dep := range bi.Deps {
if dep.Path == modulePath {
Expand All @@ -278,5 +304,5 @@ func depVersion(modulePath string) string {
return dep.Version
}
}
return "unknown"
return unknownVersionValue
}
38 changes: 38 additions & 0 deletions internal/verda-cli/cmd/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2026 Verda Cloud Oy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"fmt"

"github.com/spf13/cobra"

cmdutil "github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/util"
)

// newVersionCommand: `verda version` and `verda --version` share versionOutput
// verbatim. The flag predates the subcommand and stays supported.
// skipCredentialResolution covers it — version must answer unconfigured.
func newVersionCommand(ioStreams cmdutil.IOStreams) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print version information",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
_, _ = fmt.Fprint(ioStreams.Out, versionOutput())
return nil
},
}
}
21 changes: 18 additions & 3 deletions pkg/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,26 @@ package version
import (
"encoding/json"
"fmt"
"regexp"
"runtime"
"runtime/debug"
)

const (
unknownValue = "unknown"
trueValue = "true"
devVersion = "v0.0.0-dev"
)

// Go synthesizes a pseudo-version as Main.Version for a build from local
// source. The separator before the timestamp is '.' when the pseudo-version
// has a base tag (v1.8.2-0.<ts>-<sha>) and '-' when it does not
// (v0.0.0-<ts>-<sha>). A real tag never ends this way.
var pseudoVersionRe = regexp.MustCompile(`[-.]\d{14}-[0-9a-f]{12}(\+[\w.]+)?$`)

// Build-time variables set via -ldflags.
var (
gitVersion = "v0.0.0-dev"
gitVersion = devVersion
gitCommit = unknownValue
gitTreeState = unknownValue
buildDate = unknownValue
Expand Down Expand Up @@ -77,14 +85,17 @@ func GetFromDebugInfo(modulePath string) Info {
return info
}

if info.GitVersion == "v0.0.0-dev" {
if info.GitVersion == devVersion {
for _, dep := range bi.Deps {
if dep.Path == modulePath {
info.GitVersion = dep.Version
break
}
}
if info.GitVersion == "v0.0.0-dev" && bi.Main.Version != "" && bi.Main.Version != "(devel)" {
// Main.Version is a real tag for `go install pkg@v1.2.3` but a
// pseudo-version for a local `go build`. Keep the sentinel for the
// latter: GitCommit/GitTreeState already say what a dev build is.
if info.GitVersion == devVersion && isTaggedVersion(bi.Main.Version) {
info.GitVersion = bi.Main.Version
}
}
Expand Down Expand Up @@ -113,6 +124,10 @@ func GetFromDebugInfo(modulePath string) Info {
return info
}

func isTaggedVersion(v string) bool {
return v != "" && v != "(devel)" && !pseudoVersionRe.MatchString(v)
}

// String returns the git version string.
func (i Info) String() string {
return i.GitVersion
Expand Down
25 changes: 25 additions & 0 deletions pkg/version/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,28 @@ func TestVersionValue_Type(t *testing.T) {
t.Errorf("Type() = %q, want 'version'", v.Type())
}
}

func TestIsTaggedVersion(t *testing.T) {
tests := []struct {
name string
in string
want bool
}{
{"real tag", "v1.8.2", true},
{"real prerelease tag", "v1.8.2-rc.1", true},
{"incompatible tag", "v2.0.0+incompatible", true},
{"empty", "", false},
{"devel", "(devel)", false},
{"pseudo-version", "v1.8.2-0.20260818162257-0abc56bfdc8e", false},
{"dirty pseudo-version", "v1.8.2-0.20260818162257-0abc56bfdc8e+dirty", false},
{"pseudo-version off a prerelease", "v1.8.2-rc.1.0.20260818162257-0abc56bfdc8e", false},
{"pseudo-version with no base tag", "v0.0.0-20260818162257-0abc56bfdc8e", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isTaggedVersion(tt.in); got != tt.want {
t.Errorf("isTaggedVersion(%q) = %v, want %v", tt.in, got, tt.want)
}
})
}
}