diff --git a/.agents/skills/release-devbox/SKILL.md b/.agents/skills/release-devbox/SKILL.md index 2bd7fc5a763..56d72bb088d 100644 --- a/.agents/skills/release-devbox/SKILL.md +++ b/.agents/skills/release-devbox/SKILL.md @@ -195,7 +195,8 @@ result and the current `flake.nix` version in one shot. | `cli-release` never started | Tag push didn't land | Re-run the same command | | `cli-release` failed in `tests` | Red `main` (see below) | Fix the test, re-run | | Draft has 0 assets | The upload step didn't run or failed | Check `cli-release`'s "Attach artifacts" step, re-run | -| Published but installer serves the old version | `cli-post-release` failed or is still running | `gh run list --workflow=cli-post-release.yml` | +| Published but installer serves the old version | Release not marked latest on GitHub (installer reads `releases/latest/download/checksums.txt`) | `gh release list --limit 3`; fix with `gh release edit --latest` | +| devbox 0.20.0 or older doesn't see the release | Legacy `cli-post-release` (S3 `stable/version`) failed | `gh run list --workflow=cli-post-release.yml` | | Docker build failed | Published before assets uploaded | Re-run `docker-image-release` via `workflow_dispatch` with the tag | ## Known breakage diff --git a/.agents/skills/use-devbox/SKILL.md b/.agents/skills/use-devbox/SKILL.md index 7a294d73d58..8a4f66b36f6 100644 --- a/.agents/skills/use-devbox/SKILL.md +++ b/.agents/skills/use-devbox/SKILL.md @@ -26,7 +26,7 @@ command -v devbox If that prints nothing, install it (non-interactive): ```bash -curl -fsSL https://get.jetify.com/devbox | bash +curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash ``` devbox installs to `~/.local/bin` (or `/usr/local/bin`). If `devbox` still isn't found after installing, ensure that directory is on `PATH`. devbox itself requires the Nix package manager; the install script sets it up if missing. @@ -110,7 +110,7 @@ devbox services stop # stop them ## Key facts -- devbox is not always installed — check with `command -v devbox` and install via `curl -fsSL https://get.jetify.com/devbox | bash` if missing. +- devbox is not always installed — check with `command -v devbox` and install via `curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash` if missing. - Only directories with a `devbox.json` use it. - `devbox.json` may live in the repo root **or** a subdir root — check both. - Packages/scripts defined in `devbox.json` are only on PATH inside `devbox run`/`devbox shell`, not the plain shell. diff --git a/.github/workflows/cli-post-release.yml b/.github/workflows/cli-post-release.yml index 7376159ffc6..95417447707 100644 --- a/.github/workflows/cli-post-release.yml +++ b/.github/workflows/cli-post-release.yml @@ -1,5 +1,13 @@ name: cli-post-release -# Finalize and announce the release once its been published on Github. +# Legacy: promote the release on releases.jetify.com once it's published. +# +# The installer (install.sh) and the launcher in devbox releases after 0.20.0 find +# the latest stable version from GitHub itself +# (releases/latest/download/checksums.txt), so publishing a release as +# "latest" is all a release needs. devbox 0.20.0 and older, and the old bash +# launcher, still read s3://releases.jetpack.io/devbox/stable/version, so this +# keeps it in sync for them. Failures don't affect new installs. Delete this +# workflow once those versions are gone. on: release: @@ -38,11 +46,13 @@ jobs: needs: check-release steps: - name: Configure AWS Credentials + continue-on-error: true uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ secrets.AWS_ROLE }} aws-region: us-west-2 - name: Update latest version in s3 + continue-on-error: true run: | tmp_file=$(mktemp) echo "${{ github.ref_name }}" > $tmp_file diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 753541c59fd..047a3f19df1 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -98,12 +98,18 @@ jobs: files: | dist/checksums.txt dist/*.tar.gz - - name: Configure AWS Credentials + # Legacy: devbox 0.20.0 and older (and the old bash launcher) read the + # edge channel from releases.jetify.com, which is served from this S3 + # bucket. Newer versions don't need it, so failures here don't fail the + # release. Remove these steps once those versions are gone. + - name: Configure AWS Credentials (legacy) + continue-on-error: true uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ secrets.AWS_ROLE }} aws-region: us-west-2 - - name: Update edge version in s3 + - name: Update edge version in s3 (legacy) + continue-on-error: true run: | tmp_file=$(mktemp) echo "${{ env.EDGE_TAG }}" > $tmp_file @@ -181,7 +187,12 @@ jobs: fi # --clobber so a re-run of this workflow replaces the assets instead of # failing on the ones already uploaded. - gh release upload "$TAG" dist/*.tar.gz dist/checksums.txt \ + gh release upload "$TAG" dist/*.tar.gz \ + --repo "$GITHUB_REPOSITORY" --clobber + # checksums.txt goes last: the installer and launcher find a release's + # version and archives through it, and release.ts won't publish a + # release without it. + gh release upload "$TAG" dist/checksums.txt \ --repo "$GITHUB_REPOSITORY" --clobber - name: Notify jetpack.io slack of release status id: slack diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a6b926aa186..5f53ef09e2d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -76,7 +76,7 @@ announce: Updating: If you installed devbox via our recommended installer - (`curl -fsSL https://get.jetpack.io/devbox | bash`) you will get the new version + (`curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash`) you will get the new version _automatically_, the next time you run `devbox` Thanks, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bee544a437..3cdd50d14e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ The easiest way to develop Devbox is with Devbox! 1. Install Devbox: - curl -fsSL https://get.jetify.com/devbox | bash + curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash 2. Clone this repository: diff --git a/README.md b/README.md index 442fb2ab7dd..8f9bdc0f8d6 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,15 @@ interactive shell in it with `devbox shell`: Use the following install script to get the latest version of Devbox: ```sh -curl -fsSL https://get.jetify.com/devbox | bash +curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash ``` +The script ([install.sh](install.sh)) downloads the latest stable +[GitHub release](https://github.com/jetify-com/devbox/releases/latest), checks +its SHA-256 checksum, and installs it to `/usr/local/bin/devbox`. Pass `-f` to +skip the confirmation prompt. After that, devbox keeps itself up to date from +GitHub releases (`devbox version update`). + Read more on the [Devbox docs](https://www.jetify.com/docs/devbox/installing-devbox/). diff --git a/devbox.md b/devbox.md index 3ba3485e6d2..47dd0b43010 100644 --- a/devbox.md +++ b/devbox.md @@ -12,7 +12,7 @@ development environment. Install devbox: ```sh -curl -fsSL https://get.jetify.com/devbox | bash +curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash ``` Start the devbox shell: diff --git a/examples/cloud_development/maelstrom/README.md b/examples/cloud_development/maelstrom/README.md index 48d285e2e4e..5223250fdc3 100644 --- a/examples/cloud_development/maelstrom/README.md +++ b/examples/cloud_development/maelstrom/README.md @@ -10,7 +10,7 @@ You should also check out the [Fly.io Distributed Systems Challenge](https://fly If you don't already have [Devbox](https://www.jetify.com/docs/devbox/installing-devbox/index), you can install it by running the following command: ```bash -curl -s https://get.jetify.com/install.sh | bash +curl -s https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash ``` You can skip this step if you're running on devbox.sh diff --git a/scripts/install.sh b/install.sh similarity index 85% rename from scripts/install.sh rename to install.sh index d17130cefa6..c682fc30820 100755 --- a/scripts/install.sh +++ b/install.sh @@ -12,13 +12,16 @@ set -euo pipefail # Customize install script # ======================== -# This script is published at get.jetify.com/devbox so users can install via: -# curl -fsSL https://get.jetify.com/devbox | bash +# Users install devbox with: +# curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash +# +# Everything is downloaded from GitHub releases. The latest stable version is +# whichever release GitHub marks as "latest" (never a draft or prerelease). readonly INSTALL_DIR="/usr/local/bin" readonly BIN="devbox" -readonly RELEASES_URL="https://releases.jetify.com/devbox" -readonly DOWNLOAD_URL="https://github.com/jetify-com/devbox/releases/download" +# Must match releasesURL in internal/launcher/install.go. +readonly RELEASES_URL="https://github.com/jetify-com/devbox/releases" # Must match the version files that devbox reads (internal/launcher/files.go). readonly CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/${BIN}" @@ -201,7 +204,7 @@ intro_msg() { printf "\n" header "Confirm Installation Details" plain " Location: ${GREEN}${INSTALL_DIR}/${BIN}${NO_COLOR}" - plain " Download URL: ${UNDERLINE}${BLUE}${DOWNLOAD_URL}${NO_COLOR}" + plain " Download URL: ${UNDERLINE}${BLUE}${RELEASES_URL}${NO_COLOR}" printf "\n" } @@ -214,22 +217,26 @@ install_flow() { trap 'rm -rf "${TMP_DIR}"' EXIT local -r tmp_dir="${TMP_DIR}" + # The latest release's checksums.txt names the latest version (in its + # archive names) and the checksum to verify, in one request. start_task "Finding the latest version" - download "${RELEASES_URL}/stable/version" "${tmp_dir}/version" + download "${RELEASES_URL}/latest/download/checksums.txt" "${tmp_dir}/checksums.txt" + local -r platform="$(platform)" local version - version="$(tr -d '[:space:]' <"${tmp_dir}/version")" - if [[ ! ${version} =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then + version="$(awk -v suffix="_${platform}.tar.gz" ' + index($2, "devbox_") == 1 && substr($2, length($2) - length(suffix) + 1) == suffix { + print substr($2, 8, length($2) - 7 - length(suffix)); exit + }' "${tmp_dir}/checksums.txt")" + if [[ ! ${version} =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then fail_task "Finding the latest version" - error "Unexpected version: ${version}" + error "The latest release has no ${BIN} for ${platform}" exit 1 fi - version="${version#v}" end_task "Finding the latest version (${version})" - local -r archive="${BIN}_${version}_$(platform).tar.gz" + local -r archive="${BIN}_${version}_${platform}.tar.gz" start_task "Downloading ${BIN} ${version}" - download "${DOWNLOAD_URL}/${version}/checksums.txt" "${tmp_dir}/checksums.txt" - download "${DOWNLOAD_URL}/${version}/${archive}" "${tmp_dir}/${archive}" + download "${RELEASES_URL}/download/${version}/${archive}" "${tmp_dir}/${archive}" end_task "Downloading ${BIN} ${version}" start_task "Verifying checksum" diff --git a/internal/devbox/docgen/readme.tmpl b/internal/devbox/docgen/readme.tmpl index 72e60d3a734..ecbf096a3a4 100644 --- a/internal/devbox/docgen/readme.tmpl +++ b/internal/devbox/docgen/readme.tmpl @@ -12,7 +12,7 @@ This project uses [devbox](https://github.com/jetify-com/devbox) to manage its d Install devbox: ```sh -curl -fsSL https://get.jetpack.io/devbox | bash +curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash ``` Start the devbox shell: diff --git a/internal/devbox/generate/tmpl/DevboxImageDockerfile b/internal/devbox/generate/tmpl/DevboxImageDockerfile index 8664d6585e4..74b52b31bcd 100644 --- a/internal/devbox/generate/tmpl/DevboxImageDockerfile +++ b/internal/devbox/generate/tmpl/DevboxImageDockerfile @@ -29,7 +29,7 @@ ENV PATH="/home/${DEVBOX_USER}/.nix-profile/bin:$PATH" # Step 5: Installing devbox ENV DEVBOX_USE_VERSION=$DEVBOX_USE_VERSION -RUN wget --quiet --output-document=/dev/stdout https://get.jetify.com/devbox | bash -s -- -f +RUN wget --quiet --output-document=/dev/stdout https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash -s -- -f RUN chown -R "${DEVBOX_USER}:${DEVBOX_USER}" /usr/local/bin/devbox # run a devbox command to make launcher download devbox binary diff --git a/internal/devbox/generate/tmpl/DevboxImageDockerfileRootUser b/internal/devbox/generate/tmpl/DevboxImageDockerfileRootUser index 108d23be5de..daac2fc0d33 100644 --- a/internal/devbox/generate/tmpl/DevboxImageDockerfileRootUser +++ b/internal/devbox/generate/tmpl/DevboxImageDockerfileRootUser @@ -20,7 +20,7 @@ ENV PATH="/root/.nix-profile/bin:$PATH" # Step 3: Installing devbox ENV DEVBOX_USE_VERSION=$DEVBOX_USE_VERSION -RUN wget --quiet --output-document=/dev/stdout https://get.jetify.com/devbox | bash -s -- -f +RUN wget --quiet --output-document=/dev/stdout https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash -s -- -f # run a devbox command to make launcher download devbox binary RUN devbox version diff --git a/internal/launcher/install.go b/internal/launcher/install.go index cc4304ab4fd..bda36cf1126 100644 --- a/internal/launcher/install.go +++ b/internal/launcher/install.go @@ -20,41 +20,69 @@ import ( "go.jetify.com/devbox/internal/fileutil" ) -// These URLs are variables so that tests can serve them locally. -var ( - // releasesURL serves the latest version of each channel. - releasesURL = "https://releases.jetify.com/devbox" - // downloadURL serves the release archives and checksums. - downloadURL = "https://github.com/jetify-com/devbox/releases/download" -) +// releasesURL is the GitHub releases page. It's a variable so that tests can +// serve releases locally. Everything the launcher downloads comes from here: +// +// - /download// is a release's archive or +// checksums.txt. +// - /latest/download/checksums.txt is the checksums.txt of the +// latest stable release, which GitHub resolves to the release that's marked +// "latest" (never a draft or prerelease). Its archive names give the +// latest stable version. +var releasesURL = "https://github.com/jetify-com/devbox/releases" // httpClient's timeout is a backstop: callers set their own deadlines. var httpClient = &http.Client{Timeout: 10 * time.Minute} // FetchLatestVersion returns the latest devbox version from the release -// channel, which is the stable channel unless the channel config file names -// another URL. +// channel, which is the latest stable GitHub release unless the channel config +// file names another URL. func FetchLatestVersion(ctx context.Context) (string, error) { url := readConfig(channelFile()) if url == "" { - url = releasesURL + "/stable/version" + url = releasesURL + "/latest/download/checksums.txt" } resp, err := get(ctx, url) if err != nil { return "", err } defer resp.Body.Close() - data, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) + data, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) if err != nil { return "", fmt.Errorf("read %s: %w", url, err) } - version := strings.TrimSpace(string(data)) - if !validVersion(version) { - return "", fmt.Errorf("%s returned an invalid version %q", url, version) + version, err := parseLatestVersion(data) + if err != nil { + return "", fmt.Errorf("%s: %w", url, err) } return normalizeVersion(version), nil } +// parseLatestVersion returns the version in a channel response, which is +// either a bare version (as served by older channel URLs) or a release's +// checksums.txt, whose archive for this platform names the version. +func parseLatestVersion(data []byte) (string, error) { + if version := strings.TrimSpace(string(data)); validVersion(version) { + return version, nil + } + suffix := "_" + platform() + ".tar.gz" + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) != 2 { + continue + } + name, ok := strings.CutPrefix(fields[1], "devbox_") + if !ok { + continue + } + version, ok := strings.CutSuffix(name, suffix) + if ok && validVersion(version) { + return version, nil + } + } + return "", fmt.Errorf("found no devbox version for %s", platform()) +} + // Install downloads devbox version unless it's already installed, and returns // the path to its binary. It prints progress to w. func Install(ctx context.Context, version string, w io.Writer) (string, error) { @@ -98,7 +126,7 @@ func download(ctx context.Context, version, dir string) error { } archivePath := filepath.Join(tmp, archiveName) - gotSum, err := fetchFile(ctx, fmt.Sprintf("%s/%s/%s", downloadURL, version, archiveName), archivePath) + gotSum, err := fetchFile(ctx, fmt.Sprintf("%s/download/%s/%s", releasesURL, version, archiveName), archivePath) if err != nil { return err } @@ -140,7 +168,7 @@ func download(ctx context.Context, version, dir string) error { // fetchChecksum returns the SHA-256 checksum of archiveName from the // release's checksums.txt. func fetchChecksum(ctx context.Context, version, archiveName string) (string, error) { - url := fmt.Sprintf("%s/%s/checksums.txt", downloadURL, version) + url := fmt.Sprintf("%s/download/%s/checksums.txt", releasesURL, version) resp, err := get(ctx, url) if err != nil { return "", err diff --git a/internal/launcher/launcher.go b/internal/launcher/launcher.go index 4fef7a740ae..d543f50dfb6 100644 --- a/internal/launcher/launcher.go +++ b/internal/launcher/launcher.go @@ -31,7 +31,12 @@ import ( // as LAUNCHER_VERSION. Bump it only when installed launchers need to change, // because a newer version makes devbox ask users to replace their launcher, // which can need sudo. The bash launcher's versions are all below 1.0.0. -const Version = "1.0.0" +// +// 1.1.0: the launcher's background check reads the latest version from GitHub +// releases instead of releases.jetify.com. The check runs in the front +// binary, so 1.0.0 launchers (devbox 0.19.0 and 0.20.0) keep reading +// releases.jetify.com until they're replaced. +const Version = "1.1.0" // refreshArg is a hidden command that checks for a new devbox version. The // launcher runs it in a detached child process so that commands don't wait diff --git a/internal/launcher/launcher_test.go b/internal/launcher/launcher_test.go index cd9396dc72b..9d789296809 100644 --- a/internal/launcher/launcher_test.go +++ b/internal/launcher/launcher_test.go @@ -29,7 +29,7 @@ const ( latestVersion = "0.19.0" ) -// fakeReleases serves the stable channel and release archives that hold a +// fakeReleases serves GitHub-style release checksums and archives that hold a // devbox binary whose contents name the version. type fakeReleases struct { t *testing.T @@ -41,16 +41,10 @@ type fakeReleases struct { func (f *fakeReleases) ServeHTTP(w http.ResponseWriter, r *http.Request) { f.requests = append(f.requests, r.URL.Path) - if r.URL.Path == "/releases/stable/version" { - if f.latest == "" { - http.NotFound(w, r) - return - } - fmt.Fprintln(w, f.latest) - return - } var version, file string - if _, err := fmt.Sscanf(strings.ReplaceAll(strings.TrimPrefix(r.URL.Path, "/download/"), "/", " "), "%s %s", &version, &file); err != nil { + if r.URL.Path == "/latest/download/checksums.txt" { + version, file = f.latest, "checksums.txt" + } else if _, err := fmt.Sscanf(strings.ReplaceAll(strings.TrimPrefix(r.URL.Path, "/download/"), "/", " "), "%s %s", &version, &file); err != nil { http.NotFound(w, r) return } @@ -127,8 +121,7 @@ func setup(t *testing.T) *testEnv { env.releases.addVersion(latestVersion) server := httptest.NewServer(env.releases) t.Cleanup(server.Close) - setVar(t, &releasesURL, server.URL+"/releases") - setVar(t, &downloadURL, server.URL+"/download") + setVar(t, &releasesURL, server.URL) setVar(t, &execFunc, func(argv0 string, argv, envv []string) error { call := execCall{argv0: argv0, argv: argv, env: map[string]string{}} @@ -499,3 +492,34 @@ func TestValidVersion(t *testing.T) { } } } + +func TestParseLatestVersion(t *testing.T) { + sum := strings.Repeat("0", 64) + tests := []struct { + name, data, want string + }{ + {"bare version", "0.0.0-edge.2026-10-25\n", "0.0.0-edge.2026-10-25"}, + {"bare version with v", "v0.20.0", "v0.20.0"}, + { + "checksums", + fmt.Sprintf("%s devbox_0.20.0_other_os.tar.gz\n%s devbox_0.20.0_%s.tar.gz\n", sum, sum, platform()), + "0.20.0", + }, + {"no archive for this platform", fmt.Sprintf("%s devbox_0.20.0_other_os.tar.gz\n", sum), ""}, + {"garbage", "not found", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseLatestVersion([]byte(tt.data)) + if tt.want == "" { + if err == nil { + t.Errorf("got %q, want an error", got) + } + return + } + if err != nil || got != tt.want { + t.Errorf("got %q, %v, want %q", got, err, tt.want) + } + }) + } +} diff --git a/internal/vercheck/project.go b/internal/vercheck/project.go index 81e1f9327aa..a1ba54c7c62 100644 --- a/internal/vercheck/project.go +++ b/internal/vercheck/project.go @@ -164,7 +164,7 @@ func switchVersion(w io.Writer, configPath string, required *configfile.DevboxVe if launcherPath == "" { return usererr.New( "%s\nDevbox can't switch versions automatically because it wasn't started by the devbox launcher. "+ - "Install devbox with `curl -fsSL https://get.jetify.com/devbox | bash` to enable automatic switching.", + "Install devbox with `curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash` to enable automatic switching.", msg, ) } diff --git a/internal/vercheck/vercheck.go b/internal/vercheck/vercheck.go index db74bf4e776..bab0405b3ff 100644 --- a/internal/vercheck/vercheck.go +++ b/internal/vercheck/vercheck.go @@ -110,7 +110,7 @@ func SelfUpdate(stdErr io.Writer, updateLauncher bool) error { return usererr.New( "Devbox can't update itself because it wasn't installed with the devbox installer. " + "Update it with the package manager you installed it with, or reinstall it with " + - "`curl -fsSL https://get.jetify.com/devbox | bash`.", + "`curl -fsSL https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh | bash`.", ) } diff --git a/scripts/release.ts b/scripts/release.ts index f1e786e88d8..a7489927c13 100755 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -358,6 +358,9 @@ type ReleaseInfo = { isDraft: boolean; isPrerelease: boolean; assetCount: number; + // checksums.txt is uploaded last, and it's what the installer and launcher + // read to find the latest version, so a release without it isn't installable. + hasChecksums: boolean; url: string; }; @@ -368,13 +371,14 @@ function getRelease(version: string): ReleaseInfo | null { ]); if (res.status !== 0) return null; const parsed = JSON.parse(res.stdout) as { - tagName: string; isDraft: boolean; isPrerelease: boolean; assets: unknown[]; url: string; + tagName: string; isDraft: boolean; isPrerelease: boolean; assets: { name: string }[]; url: string; }; return { tagName: parsed.tagName, isDraft: parsed.isDraft, isPrerelease: parsed.isPrerelease, assetCount: parsed.assets.length, + hasChecksums: parsed.assets.some((a) => a.name === "checksums.txt"), url: parsed.url, }; } @@ -880,6 +884,10 @@ async function stepPublish(version: string, autoYes: boolean): Promise { ok(`${version} is already published`); } else { if (release.assetCount === 0) fail(`${version} has no assets — do not publish; the Docker build would fail`); + // Publishing a non-prerelease as "latest" is what makes it the stable + // version: the installer and launcher download + // releases/latest/download/checksums.txt from GitHub. + if (!release.hasChecksums) fail(`${version} has no checksums.txt — do not publish; installs would fail`); if (!autoYes && !(await confirm(`Publish ${version} live now?`, false))) fail("aborted before publishing"); const latestFlag = isPrerelease(version) ? ["--latest=false"] : ["--latest"]; @@ -895,9 +903,10 @@ async function stepPublish(version: string, autoYes: boolean): Promise { console.log(` ${dim("gh run list --repo " + REPO + " --workflow=cli-post-release.yml --limit 3")}`); console.log(` ${dim("gh run list --repo " + REPO + " --workflow=docker-image-release.yml --limit 3")}`); console.log(); - console.log(` cli-post-release promotes s3://releases.jetpack.io/devbox/stable/version,`); - console.log(` which is what the installer reads. Until it finishes, get.jetify.com still`); - console.log(` serves the previous version.`); + if (!isPrerelease(version)) { + console.log(` ${version} is now GitHub's latest release, so the installer and the launcher's`); + console.log(` update check pick it up right away.`); + } } // --------------------------------------------------------------------------- diff --git a/specs/native-launcher.md b/specs/native-launcher.md index d453cfa8c87..e6fe9bd2b8c 100644 --- a/specs/native-launcher.md +++ b/specs/native-launcher.md @@ -6,9 +6,9 @@ installed in `/usr/local/bin` (the "front binary") picks the devbox version to run, downloads it if needed, and either runs the command itself or execs that version. All of this then lives in this repo, in `internal/launcher`. -**Status:** implemented, except publishing the new installer -(`scripts/install.sh`) to `get.jetify.com/devbox` and retiring the launcher in -the axiom repo. See [Rollout](#rollout). +**Status:** implemented. Since 0.21.0, the installer and launcher only use +GitHub: see [Releases on GitHub](#releases-on-github). Retiring the old bash +launcher and the S3 version files is left. See [Rollout](#rollout). ## Why @@ -127,7 +127,7 @@ Most updates never touch the front binary, so they never need sudo. the cache. It isn't printed (and doesn't use up the day's notice) when `--quiet` discards stderr. It's still skipped for `shellenv` and the other commands in `commandSkipList`, and it never prompts. -- **The new installer**, `scripts/install.sh`, gets the latest version, +- **The new installer**, `install.sh` at the repo root, gets the latest version, downloads the archive and `checksums.txt` from GitHub, checks the archive, and installs the binary to `/usr/local/bin/devbox`. It uses sudo only when that directory isn't writable. It writes `current-version` and @@ -137,16 +137,43 @@ Most updates never touch the front binary, so they never need sudo. `DEVBOX_USE_VERSION` (which the generated Dockerfiles set) by downloading that version on the first run. +## Releases on GitHub + +Since 0.21.0, nothing in the install or update path uses a Jetify domain. + +- **Latest stable version:** the release GitHub marks as "latest", which is + never a draft or prerelease. The installer and launcher download + `https://github.com/jetify-com/devbox/releases/latest/download/checksums.txt` + and take the version from the archive name for their platform. This needs + no API token or rate limit, no extra release asset, and works for every + existing release. `release.ts` publishes non-prereleases with `--latest`, + and refuses to publish a release without `checksums.txt`, which + `cli-release` uploads after the archives. +- **Archives and checksums:** + `https://github.com/jetify-com/devbox/releases/download//`. +- **Installer:** `https://raw.githubusercontent.com/jetify-com/devbox/main/install.sh`. +- **Channel file:** still a URL, which can return either a bare version (like + the old S3 files) or a release's `checksums.txt`. +- **Launcher 1.1.0:** the background version check runs in the front binary, + so 1.0.0 front binaries (devbox 0.19.0 and 0.20.0) keep reading + `releases.jetify.com` even after `devbox version update` installs 0.21.0. + 0.21.0 bumps `launcher.Version` to 1.1.0, so those users see "New launcher + available" and `devbox version update` replaces the front binary. +- **Legacy:** 0.20.0 and older, and the bash launcher, read + `releases.jetify.com/devbox/{stable,edge}/version` from S3. `cli-release` + (edge) and `cli-post-release` (stable) keep updating those files, but + failures there don't fail a release. Remove them once those versions are + gone. + ## Rollout 1. Ship this in a release. New and existing installs keep working as before until users run `devbox version update`. -2. After that release is the stable version, publish `scripts/install.sh` to - `get.jetify.com/devbox` (it's currently published from - `axiom/installers/devbox/install.sh`). It needs a release with the native - launcher, because it installs the latest release as the front binary. - Older devbox versions' `devbox version update` runs this installer too, - through `get.jetpack.io/devbox`, which redirects to the same script. +2. Done in 0.21.0: the installer is `install.sh` at the repo root, served + from GitHub. `get.jetify.com/devbox` still serves the old installer + (`axiom/installers/devbox/install.sh`) for anyone using that URL, and older + devbox versions' `devbox version update` runs it through + `get.jetpack.io/devbox`. 3. Keep `releases.jetify.com/devbox` (the bash launcher) published for anything that downloads it directly. Later, retire it and `axiom/installers/devbox`. diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md index 58812611bf2..a6113daff77 100644 --- a/vscode-extension/CHANGELOG.md +++ b/vscode-extension/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the "devbox" extension will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [Unreleased] + +- Removed the Devbox Cloud "open in VS Code" URI handler, since Devbox Cloud and api.devbox.sh are shut down. + ## [0.1.9] - Added `devbox.runInitHookOnReopen` setting (off by default) to run the project's `init_hook` and include its environment variables when reopening in the Devbox environment. diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 4a8dd005428..7960f629eaf 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -152,7 +152,6 @@ "@types/mocha": "^10.0.10", "@types/node": "20.x", "@types/vscode": "~1.90.0", - "@types/which": "^3.0.4", "@vscode/test-electron": "^3.1.0", "eslint": "^10.13.0", "glob": "^13.0.6", @@ -160,8 +159,5 @@ "typescript": "~6.0.3", "typescript-eslint": "^8.72.0" }, - "dependencies": { - "which": "^7.0.0" - }, "packageManager": "yarn@4.18.1+sha512.b2e1e7524f654f2749d32b4ebcb4622473cb5bcbc485df2007e12a154e50162a4d795526768bc5f5b8f81717bfd79deb2472813d86fb5ae2eb551fa9c872b08f" } diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 4da9033c89f..108262483b1 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -2,7 +2,6 @@ import { workspace, window, commands, Uri, ExtensionContext } from 'vscode'; import { posix } from 'path'; -import { handleOpenInVSCode } from './openinvscode'; import { devboxReopen } from './devbox'; // This method is called when your extension is activated @@ -36,9 +35,6 @@ export function activate(context: ExtensionContext) { } }); - // open in vscode URI handler - const handleVSCodeUri = window.registerUriHandler({ handleUri: handleOpenInVSCode }); - const devboxAdd = commands.registerCommand('devbox.add', async () => { const result = await window.showInputBox({ value: '', @@ -113,7 +109,6 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(devboxShell); context.subscriptions.push(setupDevcontainer); context.subscriptions.push(generateDockerfile); - context.subscriptions.push(handleVSCodeUri); } async function initialCheckDevboxJSON(context: ExtensionContext) { diff --git a/vscode-extension/src/openinvscode.ts b/vscode-extension/src/openinvscode.ts deleted file mode 100644 index 573b5696de8..00000000000 --- a/vscode-extension/src/openinvscode.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as os from 'os'; -import which from 'which'; -import { exec } from 'child_process'; -import { Uri, commands, window } from 'vscode'; -import { chmod, mkdir, open, writeFile } from 'fs/promises'; - -type VmInfo = { - /* eslint-disable @typescript-eslint/naming-convention */ - vm_id: string; - private_key: string; - username: string; - working_directory: string; - /* eslint-enable @typescript-eslint/naming-convention */ -}; - -export async function handleOpenInVSCode(uri: Uri) { - const queryParams = new URLSearchParams(uri.query); - - if (queryParams.has('vm_id') && queryParams.has('token')) { - //Not yet supported for windows + WSL - will be added in future - if (os.platform() !== 'win32') { - window.showInformationMessage('Setting up devbox'); - // getting ssh keys - try { - console.debug('Calling getVMInfo...'); - const response = await getVMInfo(queryParams.get('token'), queryParams.get('vm_id')); - const res = await response.json() as VmInfo; - console.debug('getVMInfo response: ' + res); - // set ssh config - console.debug('Calling setupSSHConfig...'); - await setupSSHConfig(res.vm_id, res.private_key); - - // connect to remote vm - console.debug('Calling connectToRemote...'); - connectToRemote(res.username, res.vm_id, res.working_directory); - } catch (err: any) { - console.error(err); - window.showInformationMessage('Failed to setup devbox remote connection.'); - } - } else { - window.showErrorMessage('This function is not yet supported in Windows operating system.'); - } - } else { - window.showErrorMessage('Error parsing information for remote environment.'); - console.debug(queryParams.toString()); - }; -} - -async function getVMInfo(token: string | null, vmId: string | null): Promise { - // send post request to gateway to get vm info and ssh keys - const gatewayHost = 'https://api.devbox.sh/g/vm_info'; - const data = new FormData(); - data.append("vm_id", vmId ?? ''); - console.debug("calling devbox to get vm_info..."); - const response = await fetch(gatewayHost, { - method: 'post', - body: data, - headers: { - authorization: `Bearer ${token}` - } - }); - console.debug("API Call to api.devbox.sh response: " + response); - return response; -} - -async function setupDevboxLauncher(): Promise { - // download devbox launcher script - const gatewayHost = 'https://releases.jetify.com/devbox'; - const response = await fetch(gatewayHost, { - method: 'get', - }); - const launcherPath = `${process.env['HOME']}/.config/devbox/launcher.sh`; - - try { - const launcherScript = await response.text(); - const launcherData = new Uint8Array(Buffer.from(launcherScript)); - const fileHandler = await open(launcherPath, 'w'); - await writeFile(fileHandler, launcherData, { flag: 'w' }); - await chmod(launcherPath, 0o711); - await fileHandler.close(); - } catch (err: any) { - console.error("error setting up launcher script" + err); - throw (err); - } - return launcherPath; -} - -async function setupSSHConfig(vmId: string, prKey: string) { - const devboxBinary = await which('devbox', { nothrow: true }); - let devboxPath = 'devbox'; - if (devboxBinary === null) { - devboxPath = await setupDevboxLauncher(); - } - // For testing change devbox to path to a compiled devbox binary or add --config - exec(`${devboxPath} generate ssh-config`, (error, stdout, stderr) => { - if (error) { - window.showErrorMessage('Failed to setup ssh config. Run VSCode in debug mode to see logs.'); - console.error(`Failed to setup ssh config: ${error}`); - return; - } - console.debug(`stdout: ${stdout}`); - console.debug(`stderr: ${stderr}`); - }); - - // save private key to file - const prkeyDir = `${process.env['HOME']}/.config/devbox/ssh/keys`; - await ensureDir(prkeyDir); - const prkeyPath = `${prkeyDir}/${vmId}.vm.devbox-vms.internal`; - try { - const prKeydata = new Uint8Array(Buffer.from(prKey)); - const fileHandler = await open(prkeyPath, 'w'); - await writeFile(fileHandler, prKeydata, { flag: 'w' }); - await chmod(prkeyPath, 0o600); - await fileHandler.close(); - } catch (err: any) { - // When a request is aborted - err is an AbortError - console.error('Failed to setup ssh config: ' + err); - throw (err); - } -} - -function connectToRemote(username: string, vmId: string, workDir: string) { - try { - const host = `${username}@${vmId}.vm.devbox-vms.internal`; - const workspaceURI = `vscode-remote://ssh-remote+${host}${workDir}`; - const uriToOpen = Uri.parse(workspaceURI); - console.debug("uriToOpen: ", uriToOpen.toString()); - commands.executeCommand("vscode.openFolder", uriToOpen, false); - } catch (err: any) { - console.error('failed to connect to remote: ' + err); - } -} - -async function ensureDir(dir: string) { - try { - await mkdir(dir, {recursive: true, mode: 0o700}); - } catch (err: any) { - if (err.code !== 'EEXIST') { - console.error('Failed to setup ssh keys directory: ' + err); - throw (err); - } - } -} diff --git a/vscode-extension/yarn.lock b/vscode-extension/yarn.lock index f214b8dba7f..41b4b6319eb 100644 --- a/vscode-extension/yarn.lock +++ b/vscode-extension/yarn.lock @@ -211,13 +211,6 @@ __metadata: languageName: node linkType: hard -"@types/which@npm:^3.0.4": - version: 3.0.4 - resolution: "@types/which@npm:3.0.4" - checksum: 10c0/036e4cb243ebfd5cf4893be2ab3b9a60a22368811c1f1c78fb8fc70cadc274024282d04b8d7c0948268372600003252d84e2d3a5e064014a543a5da235c5989d - languageName: node - linkType: hard - "@typescript-eslint/eslint-plugin@npm:8.72.0": version: 8.72.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.72.0" @@ -530,14 +523,12 @@ __metadata: "@types/mocha": "npm:^10.0.10" "@types/node": "npm:20.x" "@types/vscode": "npm:~1.90.0" - "@types/which": "npm:^3.0.4" "@vscode/test-electron": "npm:^3.1.0" eslint: "npm:^10.13.0" glob: "npm:^13.0.6" mocha: "npm:^12.0.3" typescript: "npm:~6.0.3" typescript-eslint: "npm:^8.72.0" - which: "npm:^7.0.0" languageName: unknown linkType: soft @@ -948,13 +939,6 @@ __metadata: languageName: node linkType: hard -"isexe@npm:^4.0.0": - version: 4.0.0 - resolution: "isexe@npm:4.0.0" - checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce - languageName: node - linkType: hard - "js-yaml@npm:^5.0.0": version: 5.5.0 resolution: "js-yaml@npm:5.5.0" @@ -1476,17 +1460,6 @@ __metadata: languageName: node linkType: hard -"which@npm:^7.0.0": - version: 7.0.0 - resolution: "which@npm:7.0.0" - dependencies: - isexe: "npm:^4.0.0" - bin: - node-which: bin/which.js - checksum: 10c0/ca0b54f198f78bbc4b7c02e34bda8d335cb352e0adb4cbca1c37b1a957af3a879a82c4c27ca6525bc942f548d8b64f816ef6528360af9f3de55ffb9b979b620d - languageName: node - linkType: hard - "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5"