Skip to content

Commit ecc1faa

Browse files
committed
move the getLatestReleaseVersion to gh_release.go
- refact the getLatestReleaseVersion - cache to the ~/.cache/envd/ dir Signed-off-by: Keming <kemingyang@tensorchord.ai>
1 parent 8001e8b commit ecc1faa

File tree

3 files changed

+171
-45
lines changed

3 files changed

+171
-45
lines changed

envd/api/v1/install.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,11 @@ def nodejs(version: Optional[str] = "25.1.0"):
107107
"""
108108

109109

110-
def codex(version: Optional[str] = "0.55.0"):
110+
def codex(version: Optional[str] = "rust-v0.98.0"):
111111
"""Install Codex agent.
112112
113113
Args:
114-
version (Optional[str]): Codex version, such as '0.55.0'.
114+
version (Optional[str]): Codex GitHub release tag, such as 'rust-v0.98.0'.
115115
"""
116116

117117

pkg/lang/ir/v1/agent.go

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@
1515
package v1
1616

1717
import (
18-
"encoding/json"
19-
"fmt"
20-
"net/http"
21-
"strings"
22-
"time"
23-
24-
"github.com/cockroachdb/errors"
2518
"github.com/moby/buildkit/client/llb"
2619
"github.com/sirupsen/logrus"
2720

@@ -30,51 +23,18 @@ import (
3023

3124
// https://github.com/openai/codex
3225
const (
33-
codexDefaultVersion = "0.98.0"
26+
codexDefaultVersion = "rust-v0.98.0"
3427
codexReleaseUser = "openai"
3528
codexReleaseRepo = "codex"
3629
)
3730

38-
func getLatestVersion(user, repo string) (string, error) {
39-
latestURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", user, repo)
40-
req, err := http.NewRequest(http.MethodGet, latestURL, nil)
41-
if err != nil {
42-
return "", errors.Wrap(err, "failed to create request")
43-
}
44-
45-
client := &http.Client{Timeout: 10 * time.Second}
46-
resp, err := client.Do(req)
47-
if err != nil {
48-
return "", errors.Wrap(err, "failed to get latest release")
49-
}
50-
defer resp.Body.Close()
51-
52-
if resp.StatusCode != http.StatusOK {
53-
return "", errors.Errorf("failed to get latest release: %s", resp.Status)
54-
}
55-
56-
var payload struct {
57-
TagName string `json:"tag_name"`
58-
}
59-
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
60-
return "", errors.Wrap(err, "failed to decode response")
61-
}
62-
if payload.TagName == "" {
63-
return "", errors.New("failed to get latest release: empty tag name")
64-
}
65-
66-
version := strings.TrimPrefix(payload.TagName, "rust-v")
67-
version = strings.TrimPrefix(version, "v")
68-
return version, nil
69-
}
70-
7131
func (g generalGraph) installAgentCodex(root llb.State, agent ir.CodeAgent) llb.State {
7232
base := llb.Image(curlImage)
7333
version := codexDefaultVersion
7434
if agent.Version != nil {
7535
version = *agent.Version
7636
} else {
77-
latestVersion, err := getLatestVersion(codexReleaseUser, codexReleaseRepo)
37+
latestVersion, err := getLatestReleaseVersion(codexReleaseUser, codexReleaseRepo)
7838
if err != nil {
7939
logrus.WithError(err).WithField("default", codexDefaultVersion).Debug("failed to resolve latest codex version")
8040
} else {
@@ -83,7 +43,7 @@ func (g generalGraph) installAgentCodex(root llb.State, agent ir.CodeAgent) llb.
8343
}
8444
logrus.WithField("codex_version", version).Debug("parse the agent version")
8545
builder := base.Run(
86-
llb.Shlexf(`sh -c "wget -qO- https://github.com/openai/codex/releases/download/rust-v%s/codex-$(uname -m)-unknown-linux-musl.tar.gz | tar -xz -C /tmp || exit 1"`, version),
46+
llb.Shlexf(`sh -c "wget -qO- https://github.com/openai/codex/releases/download/%s/codex-$(uname -m)-unknown-linux-musl.tar.gz | tar -xz -C /tmp || exit 1"`, version),
8747
llb.WithCustomNamef("[internal] download codex %s", version),
8848
).Run(
8949
llb.Shlex(`sh -c "mv /tmp/codex-$(uname -m)-unknown-linux-musl /tmp/codex"`),

pkg/lang/ir/v1/gh_release.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2025 The envd Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package v1
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"net/http"
21+
"os"
22+
"path/filepath"
23+
"strconv"
24+
"strings"
25+
"time"
26+
27+
"github.com/cockroachdb/errors"
28+
"github.com/sirupsen/logrus"
29+
30+
"github.com/tensorchord/envd/pkg/util/fileutil"
31+
)
32+
33+
var (
34+
githubAPIBaseURL = "https://api.github.com"
35+
latestVersionCacheTTL = time.Hour
36+
maxReleaseNum = 10
37+
)
38+
39+
type cacheEntry struct {
40+
Version string `json:"version"`
41+
ExpiresAt time.Time `json:"expires_at"`
42+
}
43+
44+
func getLatestReleaseVersion(user, repo string) (string, error) {
45+
now := time.Now()
46+
if version, ok, err := readLatestVersionCache(user, repo, now); err != nil {
47+
logrus.WithError(err).Debug("failed to read latest release cache")
48+
} else if ok {
49+
return version, nil
50+
}
51+
52+
latestURL := fmt.Sprintf("%s/repos/%s/%s/releases", githubAPIBaseURL, user, repo)
53+
req, err := http.NewRequest(http.MethodGet, latestURL, nil)
54+
if err != nil {
55+
return "", errors.Wrap(err, "failed to create request")
56+
}
57+
q := req.URL.Query()
58+
q.Set("per_page", strconv.Itoa(maxReleaseNum))
59+
req.URL.RawQuery = q.Encode()
60+
req.Header.Set("Accept", "application/vnd.github+json")
61+
req.Header.Set("User-Agent", "envd")
62+
if token := strings.TrimSpace(os.Getenv("GITHUB_TOKEN")); token != "" {
63+
req.Header.Set("Authorization", "Bearer "+token)
64+
}
65+
66+
client := &http.Client{Timeout: 10 * time.Second}
67+
resp, err := client.Do(req)
68+
if err != nil {
69+
return "", errors.Wrap(err, "failed to get latest release")
70+
}
71+
defer resp.Body.Close()
72+
73+
if resp.StatusCode != http.StatusOK {
74+
return "", errors.Errorf("failed to get latest release: %s", resp.Status)
75+
}
76+
77+
var releases []struct {
78+
TagName string `json:"tag_name"`
79+
PreRelease bool `json:"prerelease"`
80+
Draft bool `json:"draft"`
81+
}
82+
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
83+
return "", errors.Wrap(err, "failed to decode response")
84+
}
85+
if len(releases) == 0 {
86+
return "", errors.New("failed to get latest release: empty response")
87+
}
88+
89+
version := ""
90+
for _, release := range releases {
91+
if release.Draft || release.PreRelease || release.TagName == "" {
92+
continue
93+
}
94+
version = release.TagName
95+
break
96+
}
97+
if version == "" {
98+
return "", errors.Newf("failed to get latest release: no stable release found in the %d releases", maxReleaseNum)
99+
}
100+
if err := writeLatestVersionCache(user, repo, cacheEntry{
101+
Version: version,
102+
ExpiresAt: now.Add(latestVersionCacheTTL),
103+
}); err != nil {
104+
logrus.WithError(err).Debug("failed to write latest release cache")
105+
}
106+
return version, nil
107+
}
108+
109+
func readLatestVersionCache(user, repo string, now time.Time) (string, bool, error) {
110+
cachePath, err := cacheFilePath(user, repo)
111+
if err != nil {
112+
return "", false, err
113+
}
114+
data, err := os.ReadFile(cachePath)
115+
if err != nil {
116+
if os.IsNotExist(err) {
117+
return "", false, nil
118+
}
119+
return "", false, errors.Wrap(err, "failed to read cache file")
120+
}
121+
122+
var entry cacheEntry
123+
if err := json.Unmarshal(data, &entry); err != nil {
124+
return "", false, errors.Wrap(err, "failed to decode cache file")
125+
}
126+
if entry.Version == "" || now.After(entry.ExpiresAt) {
127+
return "", false, nil
128+
}
129+
return entry.Version, true, nil
130+
}
131+
132+
func writeLatestVersionCache(user, repo string, entry cacheEntry) error {
133+
cachePath, err := cacheFilePath(user, repo)
134+
if err != nil {
135+
return err
136+
}
137+
dir := filepath.Dir(cachePath)
138+
tmp, err := os.CreateTemp(dir, "github-release-*.tmp")
139+
if err != nil {
140+
return errors.Wrap(err, "failed to create temp cache file")
141+
}
142+
defer func() {
143+
_ = os.Remove(tmp.Name())
144+
}()
145+
if err := json.NewEncoder(tmp).Encode(entry); err != nil {
146+
_ = tmp.Close()
147+
return errors.Wrap(err, "failed to encode cache file")
148+
}
149+
if err := tmp.Close(); err != nil {
150+
return errors.Wrap(err, "failed to close temp cache file")
151+
}
152+
if err := os.Rename(tmp.Name(), cachePath); err != nil {
153+
return errors.Wrap(err, "failed to move cache file")
154+
}
155+
return nil
156+
}
157+
158+
func cacheFilePath(user, repo string) (string, error) {
159+
name := fmt.Sprintf("github-release-%s-%s.json", sanitizeCacheComponent(user), sanitizeCacheComponent(repo))
160+
return fileutil.CacheFile(name)
161+
}
162+
163+
func sanitizeCacheComponent(component string) string {
164+
replacer := strings.NewReplacer("/", "_", "\\", "_")
165+
return replacer.Replace(component)
166+
}

0 commit comments

Comments
 (0)