Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions envd/api/v1/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ def nodejs(version: Optional[str] = "25.1.0"):
"""


def codex(version: Optional[str] = "0.55.0"):
def codex(version: Optional[str] = "rust-v0.98.0"):
Comment thread
kemingy marked this conversation as resolved.
Outdated
"""Install Codex agent.

Args:
version (Optional[str]): Codex version, such as '0.55.0'.
version (Optional[str]): Codex GitHub release tag, such as 'rust-v0.98.0'.
Comment thread
kemingy marked this conversation as resolved.
"""


Expand Down
15 changes: 13 additions & 2 deletions pkg/lang/ir/v1/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,34 @@ package v1

import (
"github.com/moby/buildkit/client/llb"
"github.com/sirupsen/logrus"

"github.com/tensorchord/envd/pkg/lang/ir"
)

// https://github.com/openai/codex
const (
codexDefaultVersion = "0.92.0"
codexDefaultVersion = "rust-v0.98.0"
codexReleaseUser = "openai"
codexReleaseRepo = "codex"
)

func (g generalGraph) installAgentCodex(root llb.State, agent ir.CodeAgent) llb.State {
base := llb.Image(curlImage)
version := codexDefaultVersion
if agent.Version != nil {
version = *agent.Version
} else {
Comment thread
kemingy marked this conversation as resolved.
latestVersion, err := getLatestReleaseVersion(codexReleaseUser, codexReleaseRepo)
if err != nil {
logrus.WithError(err).WithField("default", codexDefaultVersion).Debug("failed to resolve latest codex version")
} else {
version = latestVersion
}
}
logrus.WithField("codex_version", version).Debug("parse the agent version")
Comment thread
kemingy marked this conversation as resolved.
Outdated
builder := base.Run(
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),
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),
Comment thread
kemingy marked this conversation as resolved.
Comment thread
kemingy marked this conversation as resolved.
llb.WithCustomNamef("[internal] download codex %s", version),
).Run(
llb.Shlex(`sh -c "mv /tmp/codex-$(uname -m)-unknown-linux-musl /tmp/codex"`),
Expand Down
166 changes: 166 additions & 0 deletions pkg/lang/ir/v1/gh_release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2025 The envd 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 v1

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

"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"

"github.com/tensorchord/envd/pkg/util/fileutil"
)

var (
githubAPIBaseURL = "https://api.github.com"
latestVersionCacheTTL = time.Hour
maxReleaseNum = 10
)

type cacheEntry struct {
Version string `json:"version"`
ExpiresAt time.Time `json:"expires_at"`
}

func getLatestReleaseVersion(user, repo string) (string, error) {
now := time.Now()
if version, ok, err := readLatestVersionCache(user, repo, now); err != nil {
logrus.WithError(err).Debug("failed to read latest release cache")
} else if ok {
return version, nil
}
Comment thread
kemingy marked this conversation as resolved.

latestURL := fmt.Sprintf("%s/repos/%s/%s/releases", githubAPIBaseURL, user, repo)
req, err := http.NewRequest(http.MethodGet, latestURL, nil)
if err != nil {
return "", errors.Wrap(err, "failed to create request")
}
q := req.URL.Query()
q.Set("per_page", strconv.Itoa(maxReleaseNum))
req.URL.RawQuery = q.Encode()
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "envd")
if token := strings.TrimSpace(os.Getenv("GITHUB_TOKEN")); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", errors.Wrap(err, "failed to get latest release")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
Comment thread
kemingy marked this conversation as resolved.
return "", errors.Errorf("failed to get latest release: %s", resp.Status)
}

var releases []struct {
TagName string `json:"tag_name"`
PreRelease bool `json:"prerelease"`
Draft bool `json:"draft"`
}
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
return "", errors.Wrap(err, "failed to decode response")
}
if len(releases) == 0 {
return "", errors.New("failed to get latest release: empty response")
}

version := ""
for _, release := range releases {
if release.Draft || release.PreRelease || release.TagName == "" {
continue
}
version = release.TagName
break
}
if version == "" {
return "", errors.Newf("failed to get latest release: no stable release found in the %d releases", maxReleaseNum)
}
if err := writeLatestVersionCache(user, repo, cacheEntry{
Version: version,
ExpiresAt: now.Add(latestVersionCacheTTL),
}); err != nil {
logrus.WithError(err).Debug("failed to write latest release cache")
}
return version, nil
}

func readLatestVersionCache(user, repo string, now time.Time) (string, bool, error) {
cachePath, err := cacheFilePath(user, repo)
if err != nil {
return "", false, err
}
data, err := os.ReadFile(cachePath)
if err != nil {
if os.IsNotExist(err) {
return "", false, nil
}
return "", false, errors.Wrap(err, "failed to read cache file")
}

var entry cacheEntry
if err := json.Unmarshal(data, &entry); err != nil {
return "", false, errors.Wrap(err, "failed to decode cache file")
}
if entry.Version == "" || now.After(entry.ExpiresAt) {
return "", false, nil
}
return entry.Version, true, nil
}

func writeLatestVersionCache(user, repo string, entry cacheEntry) error {
cachePath, err := cacheFilePath(user, repo)
if err != nil {
return err
}
dir := filepath.Dir(cachePath)
tmp, err := os.CreateTemp(dir, "github-release-*.tmp")
if err != nil {
return errors.Wrap(err, "failed to create temp cache file")
}
defer func() {
_ = os.Remove(tmp.Name())
}()
if err := json.NewEncoder(tmp).Encode(entry); err != nil {
_ = tmp.Close()
return errors.Wrap(err, "failed to encode cache file")
}
if err := tmp.Close(); err != nil {
return errors.Wrap(err, "failed to close temp cache file")
}
if err := os.Rename(tmp.Name(), cachePath); err != nil {
return errors.Wrap(err, "failed to move cache file")
}
return nil
}

func cacheFilePath(user, repo string) (string, error) {
name := fmt.Sprintf("github-release-%s-%s.json", sanitizeCacheComponent(user), sanitizeCacheComponent(repo))
return fileutil.CacheFile(name)
}

func sanitizeCacheComponent(component string) string {
replacer := strings.NewReplacer("/", "_", "\\", "_")
return replacer.Replace(component)
}
Comment thread
kemingy marked this conversation as resolved.
Outdated
Comment thread
kemingy marked this conversation as resolved.
Outdated
Loading