Skip to content

Commit 4177d63

Browse files
authored
Merge branch 'main' into marc/ci-release-artifact-checks
2 parents 45603f1 + 61d7456 commit 4177d63

10 files changed

Lines changed: 491 additions & 14 deletions

File tree

‎.github/workflows/docker.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ on:
1313

1414
jobs:
1515
publish:
16-
runs-on: ubuntu-24.04
16+
runs-on: ubuntu-26.04
1717
steps:
1818
- name: Checkout
1919
uses: actions/checkout@v7

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ All notable changes to `src-cli` are documented in this file.
1313

1414
### Added
1515

16+
- `src` now prints a warning to stderr when it is older than the version recommended by your Sourcegraph instance. The recommended version is looked up at most once every 24 hours per instance. To disable the check, set the `SRC_SKIP_VERSION_CHECK` environment variable to any non-blank string. [#930](https://github.com/sourcegraph/src-cli/issues/930)
17+
1618
### Changed
1719

1820
### Fixed

‎Dockerfile‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
#
55
# Nothing fancy here: we copy in the source code and build on the Alpine Go
66
# image. Refer to .dockerignore to get a sense of what we're not going to copy.
7-
FROM golang:1.27.3-alpine3.24@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS builder
7+
FROM golang:1.27.3-alpine3.24@sha256:fa3d596898d9f126af7c01d14ee7597bab3f39cccf392ef36b69fe77cd681f18 AS builder
88

99
COPY . /src
1010
WORKDIR /src
1111
RUN go build ./cmd/src
1212

1313
# This stage should be kept in sync with Dockerfile.release.
14-
FROM alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
14+
FROM alpine:3.24@sha256:f59abe656b692d901e184be3445e084786602108599d2a51d229c0f5a48c71bf
1515

1616
# needed for `src code-intel upload` and `src actions exec`
1717
# ca-certificates-bundle >= 20261020-r0 drops the eMudhra roots (CVE-2026-111943).

‎Dockerfile.release‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# suitable for testing, since it depends on a src binary being at the project
33
# root _and_ that src binary being runnable on Alpine. To test this, refer to
44
# the main Dockerfile, which should have an identical second stage.
5-
FROM alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
5+
FROM alpine:3.24@sha256:f59abe656b692d901e184be3445e084786602108599d2a51d229c0f5a48c71bf
66

77
# needed for `src code-intel upload` and `src actions exec`
88
# ca-certificates-bundle >= 20261020-r0 drops the eMudhra roots (CVE-2026-111943).

‎cmd/src/main.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@ func (c *config) apiClient(flags *api.Flags, out io.Writer) api.Client {
156156
}
157157
}
158158

159-
return api.NewClient(opts)
159+
client := api.NewClient(opts)
160+
// Checked here rather than before the command is dispatched, so that the
161+
// lookup uses the command's own flags, such as -insecure-skip-verify.
162+
warnOutdatedVersionOnce.Do(func() {
163+
maybeWarnOutdatedVersion(client, c.endpointURL, flag.CommandLine.Arg(0))
164+
})
165+
return client
160166
}
161167

162168
// readConfig reads the config file from the given path.

‎cmd/src/version.go‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"net/url"
910
"os"
1011

1112
"github.com/sourcegraph/sourcegraph/lib/errors"
@@ -42,17 +43,19 @@ var versionCommand = clicompat.Wrap(&cli.Command{
4243
HideVersion: true,
4344
Action: func(ctx context.Context, c *cli.Command) error {
4445
args := VersionArgs{
45-
Client: cfg.apiClient(clicompat.APIFlagsFromCmd(c), os.Stdout),
46-
ClientOnly: c.Bool("client-only"),
46+
Client: cfg.apiClient(clicompat.APIFlagsFromCmd(c), os.Stdout),
47+
EndpointURL: cfg.endpointURL,
48+
ClientOnly: c.Bool("client-only"),
4749
}
4850
return versionHandler(args)
4951
},
5052
})
5153

5254
type VersionArgs struct {
53-
ClientOnly bool
54-
Client api.Client
55-
Output io.Writer
55+
ClientOnly bool
56+
Client api.Client
57+
EndpointURL *url.URL
58+
Output io.Writer
5659
}
5760

5861
func versionHandler(args VersionArgs) error {
@@ -68,9 +71,10 @@ func versionHandler(args VersionArgs) error {
6871
if recommendedVersion == "" {
6972
fmt.Println("Recommended version: <unknown>")
7073
fmt.Println("This Sourcegraph instance does not support this feature.")
71-
return nil
74+
} else {
75+
fmt.Printf("Recommended version: %s or later\n", recommendedVersion)
7276
}
73-
fmt.Printf("Recommended version: %s or later\n", recommendedVersion)
77+
warnOutdatedVersionFromLookup(recommendedVersion, args.EndpointURL)
7478
return nil
7579
}
7680

‎cmd/src/version_check.go‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
"os"
8+
"path/filepath"
9+
"sync"
10+
"time"
11+
12+
"github.com/Masterminds/semver"
13+
14+
"github.com/sourcegraph/src-cli/internal/api"
15+
"github.com/sourcegraph/src-cli/internal/version"
16+
)
17+
18+
const (
19+
// versionCheckTimeout bounds the recommended-version lookup, which runs
20+
// before the command the user asked for.
21+
versionCheckTimeout = 3 * time.Second
22+
23+
// versionCheckInterval is how long the result of a lookup is reused before
24+
// the instance is asked again.
25+
versionCheckInterval = 24 * time.Hour
26+
)
27+
28+
// warnOutdatedVersionOnce limits the check to once per run of src-cli, however
29+
// many API clients the command creates.
30+
var warnOutdatedVersionOnce sync.Once
31+
32+
// maybeWarnOutdatedVersion prints a warning to stderr when src-cli is older
33+
// than the version recommended by the Sourcegraph instance at endpointURL,
34+
// which it asks using client unless a recent answer is cached.
35+
//
36+
// It never fails the command: an unreachable instance, an unparseable version,
37+
// or an unwritable cache all result in no warning.
38+
func maybeWarnOutdatedVersion(client api.Client, endpointURL *url.URL, subcommand string) {
39+
// 'src version' asks the instance itself, and then calls
40+
// warnOutdatedVersionFromLookup with the answer.
41+
if subcommand == "version" || skipVersionCheck() {
42+
return
43+
}
44+
cachePath, err := recommendedVersionCachePath(endpointURL)
45+
if err != nil {
46+
return
47+
}
48+
printOutdatedVersionWarning(recommendedVersion(client, cachePath), endpointURL)
49+
}
50+
51+
// warnOutdatedVersionFromLookup is the check for 'src version', which has just
52+
// been told the recommended version by the instance at endpointURL. The answer
53+
// is cached, so that the commands that follow warn about the same version.
54+
func warnOutdatedVersionFromLookup(recommended string, endpointURL *url.URL) {
55+
if skipVersionCheck() {
56+
return
57+
}
58+
if cachePath, err := recommendedVersionCachePath(endpointURL); err == nil {
59+
_ = cacheRecommendedVersion(cachePath, recommended)
60+
}
61+
printOutdatedVersionWarning(recommended, endpointURL)
62+
}
63+
64+
// skipVersionCheck reports whether the version check is disabled, or has no
65+
// version to compare.
66+
func skipVersionCheck() bool {
67+
return os.Getenv("SRC_SKIP_VERSION_CHECK") != "" || version.BuildTag == version.DefaultBuildTag
68+
}
69+
70+
func printOutdatedVersionWarning(recommended string, endpointURL *url.URL) {
71+
if warning := outdatedVersionWarning(recommended, endpointURL); warning != "" {
72+
fmt.Fprintln(os.Stderr, warning)
73+
}
74+
}
75+
76+
// outdatedVersionWarning returns the warning to print, or "" when src-cli is
77+
// not known to be older than the recommended version.
78+
func outdatedVersionWarning(recommendedVersion string, endpointURL *url.URL) string {
79+
current, err := semver.NewVersion(version.BuildTag)
80+
if err != nil {
81+
return ""
82+
}
83+
recommended, err := semver.NewVersion(recommendedVersion)
84+
if err != nil {
85+
return ""
86+
}
87+
if !current.LessThan(recommended) {
88+
return ""
89+
}
90+
return fmt.Sprintf("⚠️ Warning: src-cli %s is older than %s, the version recommended by %s. "+
91+
"To upgrade, see https://github.com/sourcegraph/src-cli#installation. "+
92+
"To silence this warning, set SRC_SKIP_VERSION_CHECK=1.",
93+
current, recommended, endpointURL)
94+
}
95+
96+
// recommendedVersionCachePath returns the file that holds the version
97+
// recommended by the instance at endpointURL.
98+
func recommendedVersionCachePath(endpointURL *url.URL) (string, error) {
99+
cacheDir, err := os.UserCacheDir()
100+
if err != nil {
101+
return "", err
102+
}
103+
return filepath.Join(cacheDir, "sourcegraph", "src-cli", "recommended-version-"+url.QueryEscape(endpointURL.String())), nil
104+
}
105+
106+
// cacheRecommendedVersion records recommended, and the time it was learned, in
107+
// the file at cachePath.
108+
func cacheRecommendedVersion(cachePath, recommended string) error {
109+
if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil {
110+
return err
111+
}
112+
return os.WriteFile(cachePath, []byte(recommended), 0o600)
113+
}
114+
115+
// recommendedVersion returns the src-cli version recommended by the instance
116+
// that client talks to, or "" when it is unknown. The answer is cached in the
117+
// file at cachePath for versionCheckInterval, so most commands make no extra
118+
// request.
119+
func recommendedVersion(client api.Client, cachePath string) string {
120+
if info, err := os.Stat(cachePath); err == nil && time.Since(info.ModTime()) < versionCheckInterval {
121+
cached, _ := os.ReadFile(cachePath)
122+
return string(cached)
123+
}
124+
125+
// Record the attempt before the lookup, so that an instance that does not
126+
// answer delays one command per versionCheckInterval instead of every
127+
// command. Without a writable cache there is no such bound, so do not look.
128+
if cacheRecommendedVersion(cachePath, "") != nil {
129+
return ""
130+
}
131+
132+
ctx, cancel := context.WithTimeout(context.Background(), versionCheckTimeout)
133+
defer cancel()
134+
recommended, err := getRecommendedVersion(ctx, client)
135+
if err != nil {
136+
return ""
137+
}
138+
_ = cacheRecommendedVersion(cachePath, recommended)
139+
return recommended
140+
}

0 commit comments

Comments
 (0)