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
2 changes: 2 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
set-meta-annotations: true
build-args: |
"VERSION=${{ github.ref_name }}"
"GIT_COMMIT=${{ github.sha }}"
"BUILD_DATE=${{ github.event.head_commit.timestamp }}"
meta-images: |
ghcr.io/score-spec/score-k8s
scorespec/score-k8s
Expand Down
2 changes: 2 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ builds:
main: ./cmd/score-k8s
ldflags:
- -X github.com/score-spec/score-k8s/internal/version.Version={{ .Version }}
- -X github.com/score-spec/score-k8s/internal/version.GitCommit={{ .Commit }}
- -X github.com/score-spec/score-k8s/internal/version.BuildDate={{ .Date }}
env:
- CGO_ENABLED=0
targets:
Expand Down
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
FROM --platform=$BUILDPLATFORM dhi.io/golang:1.26.2-alpine3.23-dev@sha256:0d916bcd9ca2060863389d96c8ea686d72e03ccc48ebf498be2150f21f720999 AS builder

ARG VERSION=0.0.0
ARG GIT_COMMIT=unknown
ARG BUILD_DATE=unknown

# Set the current working directory inside the container.
WORKDIR /go/src/github.com/score-spec/score-k8s
Expand All @@ -11,7 +13,12 @@ RUN go mod download

# Copy the entire project and build it.
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X github.com/score-spec/score-k8s/internal/version.Version=${VERSION}" -o /usr/local/bin/score-k8s ./cmd/score-k8s
RUN CGO_ENABLED=0 GOOS=linux \
go build -ldflags="-s -w \
-X github.com/score-spec/score-k8s/internal/version.Version=${VERSION} \
-X github.com/score-spec/score-k8s/internal/version.GitCommit=${GIT_COMMIT} \
-X github.com/score-spec/score-k8s/internal/version.BuildDate=${BUILD_DATE}" \
-o /usr/local/bin/score-k8s ./cmd/score-k8s

# We can use static since we don't rely on any linux libs or state, but we need ca-certificates to connect to https/oci with the init command.
FROM dhi.io/static:20251003-alpine3.23@sha256:a08d9a53a4758b4006d56341aa88b1edf583ddebd93e620a32acd5135535573c
Expand Down
7 changes: 4 additions & 3 deletions internal/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (
"github.com/score-spec/score-k8s/internal/version"
)

var ScoreImplementationName = "score-k8s"

var rootCmd = &cobra.Command{
Use: "score-k8s",
Use: ScoreImplementationName,
Short: "Score to Kubernetes manifest translator",
Long: `Score is a specification for defining environment agnostic configuration for cloud based workloads.
This tool produces a file of Kubernetes manifests from the Score specification.`,
Expand All @@ -50,8 +52,7 @@ This tool produces a file of Kubernetes manifests from the Score specification.`

func init() {
rootCmd.Version = version.BuildVersionString()
rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s" .Version}}
`)
rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s\n" .Version}}`)
rootCmd.PersistentFlags().Bool("quiet", false, "Mute any logging output")
rootCmd.PersistentFlags().CountP("verbose", "v", "Increase log verbosity and detail by specifying this flag one or more times")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/command/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func executeAndResetCommand(ctx context.Context, cmd *cobra.Command, args []stri
func TestRootVersion(t *testing.T) {
stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"--version"})
assert.NoError(t, err)
pattern := regexp.MustCompile(`^score-k8s 0.0.0 \(build: \S+, sha: \S+\)\n$`)
pattern := regexp.MustCompile(`^score-k8s 0\.0\.0 \(go\S+ - \S+/\S+\)\ngit commit: \S+\nbuild date: \S+\n$`)
assert.Truef(t, pattern.MatchString(stdout), "%s does not match: '%s'", pattern.String(), stdout)
assert.Equal(t, "", stderr)
}
Expand Down
139 changes: 139 additions & 0 deletions internal/command/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright 2024 The Score Authors
//
// 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 command

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/score-spec/score-k8s/internal/version"
"github.com/spf13/cobra"
)

const (
versionCmdFileNoLogo = "no-logo"
versionCmdFileNoUpdatesCheck = "no-updates-check"
logo = `
... .............
....... .............
......... ............
......... .....
....... .... ..
.......... ..... ......
........ ...... ..........
..... ..... ..........
.... ........
........... .........
.............. .........
................ ......
............... ..
............
`
)

var versionCmd = &cobra.Command{
Use: "version",
Short: "Show the version for " + ScoreImplementationName + " and new version to update if available.",
Args: cobra.NoArgs,
CompletionOptions: cobra.CompletionOptions{
HiddenDefaultCmd: true,
},
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

if noLogo, _ := cmd.Flags().GetBool(versionCmdFileNoLogo); !noLogo {
fmt.Println(logo)
}

fmt.Println(ScoreImplementationName, version.BuildVersionString())

if noUpdateCheck, _ := cmd.Flags().GetBool(versionCmdFileNoUpdatesCheck); !noUpdateCheck {
if newer, err := checkForNewerVersion(version.Version); err == nil && newer != "" {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nA newer version is available: %s\nUpdate at: https://github.com/score-spec/%s/releases/tag/%s\n", newer, ScoreImplementationName, newer)
}
}

return nil
},
}

// checkForNewerVersion queries the GitHub releases API and returns the tag name of the latest
// release if it is newer than currentVersion. Returns an empty string if no newer version is found
// or if the current version cannot be parsed.
func checkForNewerVersion(currentVersion string) (string, error) {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://api.github.com/repos/score-spec/" + ScoreImplementationName + "/releases/latest")
if err != nil {
return "", err
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status from releases API: %s", resp.Status)
}

var release struct {
TagName string `json:"tag_name"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", err
}

if isNewerVersion(currentVersion, release.TagName) {
return release.TagName, nil
}
return "", nil
}

// isNewerVersion reports whether latestVersion is strictly greater than currentVersion.
// Both versions may optionally start with a "v" prefix and are expected to follow semver
// (MAJOR.MINOR.PATCH). Non-numeric or unparseable segments are treated as 0.
func isNewerVersion(currentVersion, latestVersion string) bool {
current := parseSemver(currentVersion)
latest := parseSemver(latestVersion)
for i := range current {
if latest[i] > current[i] {
return true
}
if latest[i] < current[i] {
return false
}
}
return false
}

// parseSemver splits a version string (with an optional leading "v") into its three numeric
// components [major, minor, patch]. Components that cannot be parsed are treated as 0.
func parseSemver(v string) [3]int {
v = strings.TrimPrefix(v, "v")
parts := strings.SplitN(v, ".", 3)
var out [3]int
for i := 0; i < 3 && i < len(parts); i++ {
n, _ := strconv.Atoi(parts[i])
out[i] = n
}
return out
}

func init() {
versionCmd.Flags().Bool(versionCmdFileNoLogo, false, "Do not show the Score logo")
versionCmd.Flags().Bool(versionCmdFileNoUpdatesCheck, false, "Do not check for a new version")
rootCmd.AddCommand(versionCmd)
}
26 changes: 4 additions & 22 deletions internal/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,21 @@ package version
import (
"fmt"
"regexp"
"runtime/debug"
"runtime"
"strconv"
)

var (
Version string = "0.0.0"
GitCommit string = "unknown"
BuildDate string = "unknown"
semverPattern = regexp.MustCompile(`^(?:v?)(\d+)(?:\.(\d+))?(?:\.(\d+))?$`)
constraintAndSemver = regexp.MustCompile("^(>|>=|=)?" + semverPattern.String()[1:])
)

// BuildVersionString constructs a version string by looking at the build metadata injected at build time.
// This is particularly useful when score-k8s is installed from the go module using go install.
func BuildVersionString() string {
versionNumber, buildTime, gitSha, isDirtySuffix := Version, "local", "unknown", ""
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.time":
if setting.Value != "" {
buildTime = setting.Value
}
case "vcs.revision":
if setting.Value != "" {
gitSha = setting.Value
}
case "vcs.modified":
if setting.Value == "true" {
isDirtySuffix = "-dirty"
}
}
}
}
return fmt.Sprintf("%s (build: %s, sha: %s%s)", versionNumber, buildTime, gitSha, isDirtySuffix)
return fmt.Sprintf("%s (%s - %s/%s)\ngit commit: %s\nbuild date: %s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, GitCommit, BuildDate)
}

func semverToI(x string) (int, error) {
Expand Down
Loading