From f2ed02dc3ebf89db89e7ae70db03425dd0185ce6 Mon Sep 17 00:00:00 2001 From: Brice TEXIER Date: Tue, 15 Sep 2026 14:08:33 +0200 Subject: [PATCH 1/2] Install asdf from its pre-compiled binary and default to 0.20 Since 0.16 asdf ships as a Go binary instead of a tree of shell scripts, so there is no repository to clone, nothing to source, and `asdf update` refuses to upgrade the binary it ships as. asdf:setup now reads the installed version from `asdf version` and, when it differs from :asdf_version, downloads the release archive built for the architecture `uname -m` reports and untars its single `asdf` executable into #{asdf_path}/bin. Linux is the only supported target. asdf:map_bins exports ASDF_DATA_DIR, which the binary no longer derives from its own location: without it, a custom :asdf_path would hold the binary while the plugins, the installs and the shims went to ~/.asdf, where the PATH does not point. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 18 +++++++++-- lib/capistrano/asdf/release.rb | 34 ++++++++++++++++++++ lib/capistrano/tasks/asdf.rake | 55 ++++++++++++++++++-------------- test/capistrano/test_map_bins.rb | 18 ++++++++--- test/capistrano/test_release.rb | 35 ++++++++++++++++++++ 5 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 lib/capistrano/asdf/release.rb create mode 100644 test/capistrano/test_release.rb diff --git a/README.md b/README.md index 486f3ac..c9fcd22 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,23 @@ However we strongly encourage you to use the `.tool-versions` for proper tool ve If you need some special settings, set those in the stage file for your server: # deploy.rb or stage file (staging.rb, production.rb or else) + set :asdf_version, '0.20.0' # defaults to '0.20.0' + set :asdf_setup, false # defaults to true set :asdf_path, '~/.my_asdf_installation_path' # only needed if not '~/.asdf' set :asdf_tools, %w{ ruby } # defaults to %{ ruby nodejs } set :asdf_map_ruby_bins, %w{ bundle gem } # defaults to %w{ rake gem bundle ruby rails } set :asdf_map_nodejs_bins, %w{ node npm } # defaults to %w{ node npm yarn } +### ASDF version: `:asdf_version` + +The deploy installs ASDF itself, and keeps it at the requested version. On every +deploy, `asdf:setup` compares `asdf version` on the target host with +`:asdf_version` and, when they differ, downloads the pre-compiled binary of that +release into `#{fetch(:asdf_path)}/bin`. + +Set `:asdf_setup` to `false` if you would rather install and update ASDF +yourself. + ### Custom ASDF path: `:asdf_path` If you have a custom ASDF setup with a different path then expected, you have @@ -74,9 +86,9 @@ For example; if you just want to map `node` and `npm` nodejs binaries, you may s ## Restrictions -Capistrano can't use ASDF to install rubies, nodes or other tools yet. -So on the servers you are deploying to, you will have to manually use ASDF to install the -proper rubies, nodes or other tools. +ASDF is installed from the pre-compiled Linux binary of its GitHub release, so +the target hosts have to run Linux. Ubuntu is the only distribution this is used +on, and `uname -m` has to answer `x86_64` or `aarch64`. ## How it works diff --git a/lib/capistrano/asdf/release.rb b/lib/capistrano/asdf/release.rb new file mode 100644 index 0000000..9369776 --- /dev/null +++ b/lib/capistrano/asdf/release.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Capistrano + module Asdf + # Since 0.16 asdf is a pre-compiled binary rather than a set of shell + # scripts: every install and every update downloads a release archive. + module Release + DOWNLOAD_HOST = "https://github.com/asdf-vm/asdf/releases/download" + + # Ubuntu is the only supported target, so only the Linux builds that the + # asdf releases carry are mapped, keyed by what `uname -m` answers. + ARCHITECTURES = { + "x86_64" => "amd64", + "aarch64" => "arm64" + }.freeze + + # The archive holds the single `asdf` executable, with no enclosing + # directory, so it untars straight into a directory of the PATH. + def self.download_url(version, machine) + architecture = ARCHITECTURES.fetch(machine) do + raise ArgumentError, "asdf publishes no Linux build for #{machine} hardware" + end + + "#{DOWNLOAD_HOST}/v#{version}/asdf-v#{version}-linux-#{architecture}.tar.gz" + end + + # `asdf version` answers `v0.20.0 (revision 150aaf0)`, so only the + # version number is read out of it. + def self.installed_version(output) + output.to_s[/\d+\.\d+\.\d+/] + end + end + end +end diff --git a/lib/capistrano/tasks/asdf.rake b/lib/capistrano/tasks/asdf.rake index 7665814..78613a6 100644 --- a/lib/capistrano/tasks/asdf.rake +++ b/lib/capistrano/tasks/asdf.rake @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "../asdf/release" + namespace :asdf do desc "Install ASDF tools on deploy" task :deploy do @@ -19,29 +21,32 @@ namespace :asdf do end end - desc "Setup ASDF on the target host" + desc "Install ASDF, or update it to :asdf_version, on the target host" task :setup do - if fetch(:asdf_setup) - on roles(fetch(:asdf_roles)) do - if test("[ -d #{fetch(:asdf_path)} ]") - within(fetch(:asdf_path)) do - version = capture(:cat, "version.txt").strip - if fetch(:asdf_version) == version - info "ASDF #{fetch(:asdf_version)} is already installed on #{fetch(:asdf_path)}" - else - execute :git, "fetch", "origin" - execute :git, "checkout", "v#{fetch(:asdf_version)}" - info "ASDF is updated from #{version} to #{fetch(:asdf_version)} (on #{fetch(:asdf_path)})" - end - end - else - execute :git, "clone", fetch(:asdf_repository), fetch(:asdf_path) - within(fetch(:asdf_path)) do - execute :git, "checkout", "v#{fetch(:asdf_version)}" - execute :echo, fetch(:asdf_version), ">", "version.txt" - end - info "ASDF #{fetch(:asdf_version)} is installed on #{fetch(:asdf_path)}" - end + next unless fetch(:asdf_setup) + + version = fetch(:asdf_version) + on roles(fetch(:asdf_roles)) do + bin_path = "#{fetch(:asdf_path)}/bin" + installed = if test("[ -x #{bin_path}/asdf ]") + Capistrano::Asdf::Release.installed_version(capture(:asdf, "version", raise_on_non_zero_exit: false)) + end + + if installed == version + info "ASDF #{version} is already installed on #{fetch(:asdf_path)}" + next + end + + url = Capistrano::Asdf::Release.download_url(version, capture(:uname, "-m")) + execute :mkdir, "-p", bin_path + # `asdf update` refuses to upgrade the binary it ships as, so the release + # archive is downloaded over the installed one. + execute "curl -fsSL #{url} | tar -xzf - -C #{bin_path} asdf" + + if installed + info "ASDF is updated from #{installed} to #{version} (on #{fetch(:asdf_path)})" + else + info "ASDF #{version} is installed on #{fetch(:asdf_path)}" end end end @@ -93,6 +98,9 @@ namespace :asdf do asdf_home = fetch(:asdf_path).sub(%r{\A~(?=/|\z)}, "$HOME") path = "#{asdf_home}/shims:#{asdf_home}/bin:" + (SSHKit.config.default_env[:path] || "$PATH") SSHKit.config.default_env[:path] = path + # Since 0.16 the asdf binary no longer derives its data directory from its + # own location, so a custom :asdf_path has to be handed over explicitly. + SSHKit.config.default_env[:asdf_data_dir] = asdf_home asdf_prefix = fetch(:asdf_prefix, -> { "#{fetch(:asdf_path)}/bin/asdf exec" }) SSHKit.config.command_map[:asdf] = "#{fetch(:asdf_path)}/bin/asdf" @@ -122,8 +130,7 @@ end namespace :load do task :defaults do set :asdf_path, fetch(:asdf_path, "~/.asdf") - set :asdf_repository, fetch(:asdf_repository, "https://github.com/asdf-vm/asdf.git") - set :asdf_version, fetch(:asdf_version, "0.15.0") + set :asdf_version, fetch(:asdf_version, "0.20.0") set :asdf_setup, fetch(:asdf_setup, true) set :asdf_roles, fetch(:asdf_roles, :all) set :asdf_ruby_use_jemalloc, fetch(:asdf_ruby_use_jemalloc, true) diff --git a/test/capistrano/test_map_bins.rb b/test/capistrano/test_map_bins.rb index ec5dfec..a741e59 100644 --- a/test/capistrano/test_map_bins.rb +++ b/test/capistrano/test_map_bins.rb @@ -40,7 +40,7 @@ def teardown def test_the_exported_path_holds_no_tilde Rake::Task["asdf:map_bins"].invoke - assert_equal "$HOME/.asdf/shims:$HOME/.asdf/bin:$PATH", exported_path + assert_equal "$HOME/.asdf/shims:$HOME/.asdf/bin:$PATH", exported("PATH") end def test_it_keeps_a_path_already_set_by_the_application @@ -48,7 +48,15 @@ def test_it_keeps_a_path_already_set_by_the_application Rake::Task["asdf:map_bins"].invoke - assert_equal "$HOME/.asdf/shims:$HOME/.asdf/bin:/opt/custom/bin:$PATH", exported_path + assert_equal "$HOME/.asdf/shims:$HOME/.asdf/bin:/opt/custom/bin:$PATH", exported("PATH") + end + + # Since 0.16 the asdf binary reads its data directory from the environment + # instead of deriving it from its own location. + def test_it_exports_the_asdf_data_dir + Rake::Task["asdf:map_bins"].invoke + + assert_equal "$HOME/.asdf", exported("ASDF_DATA_DIR") end def test_an_absolute_asdf_path_is_left_alone @@ -56,14 +64,14 @@ def test_an_absolute_asdf_path_is_left_alone Rake::Task["asdf:map_bins"].invoke - assert_equal "/opt/asdf/shims:/opt/asdf/bin:$PATH", exported_path + assert_equal "/opt/asdf/shims:/opt/asdf/bin:$PATH", exported("PATH") end # What the shell actually receives, tilde expansion included. - def exported_path + def exported(variable) command = SSHKit::Command.new(:node, "--version").to_command - command[/PATH="([^"]*)"/, 1] + command[/#{variable}="([^"]*)"/, 1] end end end diff --git a/test/capistrano/test_release.rb b/test/capistrano/test_release.rb new file mode 100644 index 0000000..9d45247 --- /dev/null +++ b/test/capistrano/test_release.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "test_helper" + +require "capistrano/asdf/release" + +module Capistrano + module Asdf + class TestRelease < Minitest::Test + def test_it_points_at_the_linux_build_of_the_host_hardware + assert_equal "https://github.com/asdf-vm/asdf/releases/download/v0.20.0/asdf-v0.20.0-linux-amd64.tar.gz", + Release.download_url("0.20.0", "x86_64") + assert_equal "https://github.com/asdf-vm/asdf/releases/download/v0.20.0/asdf-v0.20.0-linux-arm64.tar.gz", + Release.download_url("0.20.0", "aarch64") + end + + def test_it_refuses_hardware_asdf_publishes_no_build_for + error = assert_raises(ArgumentError) { Release.download_url("0.20.0", "riscv64") } + + assert_includes error.message, "riscv64" + end + + def test_it_reads_the_version_number_out_of_the_asdf_output + assert_equal "0.20.0", Release.installed_version("v0.20.0 (revision 150aaf0)\n") + # What the 0.15 shell script the servers still run answers, so that an + # update is detected whatever installed ASDF in the first place. + assert_equal "0.15.0", Release.installed_version("v0.15.0-31e8c93\n") + end + + def test_it_reads_no_version_out_of_a_failed_call + assert_nil Release.installed_version("") + end + end + end +end From 7a974d915cb287c64c5eee07d18db168224dc0a7 Mon Sep 17 00:00:00 2001 From: Brice TEXIER Date: Tue, 15 Sep 2026 14:08:38 +0200 Subject: [PATCH 2/2] Bump to 1.6.0 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 21 +++++++++++++++++++++ lib/capistrano/asdf/version.rb | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5999b1e..3d4b034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.0] - 2026-09-15 + +### Changed + +- `asdf_version` now defaults to `0.20.0`, and `asdf:setup` installs it the way + asdf is distributed since 0.16: the pre-compiled Linux binary of the release + is downloaded into `#{asdf_path}/bin`, instead of the repository being cloned + and a tag checked out. The architecture comes from `uname -m` on the target + host, so only Linux (Ubuntu) targets are supported. +- `asdf:setup` reads the installed version from `asdf version` rather than from + the `version.txt` it used to write, so an update is detected whatever + installed asdf in the first place. The stale `version.txt` and the leftover + 0.15 git clone in `#{asdf_path}` can be deleted by hand. +- `asdf:map_bins` exports `ASDF_DATA_DIR`. The asdf binary no longer derives + its data directory from its own location, so a custom `asdf_path` needs it to + keep holding the plugins, the installs and the shims. + +### Removed + +- `asdf_repository` variable, which nothing downloads from any more. + ## [1.5.5] - 2026-09-15 ### Fixed diff --git a/lib/capistrano/asdf/version.rb b/lib/capistrano/asdf/version.rb index 4d71e3d..ff9f119 100644 --- a/lib/capistrano/asdf/version.rb +++ b/lib/capistrano/asdf/version.rb @@ -2,6 +2,6 @@ module Capistrano module Asdf - VERSION = "1.5.5" + VERSION = "1.6.0" end end