Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
feat(dist): download the binary matching the installed package versio…
…n + lint jobs

Two follow-ups to the RubyGems/Composer CLI launchers.

1. Version match. The launchers no longer trust a baked-in version constant for
   the download URL β€” they derive it from the package the user actually
   installed, so the downloaded binary always matches the installed package:
   - Ruby: `Gem.loaded_specs["socket-patch"].version` (then
     `Gem::Specification.find_by_name`), so `gem install socket-patch -v X`
     fetches the vX binary.
   - Composer: loads Composer's autoloader via `$_composer_autoload_path` and
     reads `Composer\InstalledVersions::getPrettyVersion('socketsecurity/socket-patch')`
     (only when it's a concrete X.Y.Z release).
   The `VERSION` / `SP_VERSION` constants remain solely as a fallback for
   not-installed contexts (raw checkout); version-sync keeps them current.

2. Lint jobs. New `lint-ecosystems` job in ci.yml covering the out-of-workspace
   packaging for the ecosystems we added:
   - Ruby: `ruby -c` the CLI launcher gem + Bundler plugin + the generated-plugin
     templates, and `gem build` both gemspecs.
   - PHP: `php -l` the Composer launcher + `composer validate`.
   - Go: gofmt-clean + `go vet` the setup-guard templates (rendered to a temp
     package).

Verified: ci.yml valid YAML; both gems build; `ruby -c`/`gofmt`/`go vet` clean
on the templates locally (php/composer lint runs in CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • Loading branch information
mikolalysenko and claude committed Jun 5, 2026
commit 1068be3990e293aea14df5499e852a70685f7073
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,45 @@ jobs:
- name: Run clippy
run: cargo clippy --workspace --all-features -- -D warnings

# Lint the out-of-workspace packaging artifacts for the ecosystems whose setup
# / CLI-distribution we added: the RubyGems CLI launcher gem + the Bundler
# plugin gem (Ruby), the Composer CLI launcher (PHP), and the generated Go
# setup-guard templates. Ruby, PHP, Composer, and Go are all pre-installed on
# the ubuntu-latest runner.
lint-ecosystems:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Ruby β€” syntax-check + build the launcher gem and Bundler plugin
run: |
( cd gem/socket-patch && ruby -c lib/socket_patch/launcher.rb && ruby -c exe/socket-patch && gem build socket-patch.gemspec )
( cd gem/socket-patch-bundler && ruby -c plugins.rb && gem build socket-patch-bundler.gemspec )
# The generated-plugin templates are pure Ruby β€” keep them parseable.
ruby -c crates/socket-patch-core/src/gem_setup/templates/plugins.rb.tmpl
ruby -c crates/socket-patch-core/src/gem_setup/templates/gemspec.tmpl

- name: PHP β€” lint the Composer launcher + validate composer.json
run: |
php -l composer/socket-patch/bin/socket-patch
( cd composer/socket-patch && composer validate --no-check-publish )

- name: Go β€” gofmt + vet the setup-guard templates
run: |
tmp="$(mktemp -d)"
cp crates/socket-patch-core/src/go_setup/templates/guard.go.tmpl "$tmp/guard.go"
cp crates/socket-patch-core/src/go_setup/templates/guard_test.go.tmpl "$tmp/guard_test.go"
unformatted="$(gofmt -l "$tmp")"
if [ -n "$unformatted" ]; then
echo "::error::Go setup-guard templates are not gofmt-clean:"
gofmt -d "$tmp"
exit 1
fi
( cd "$tmp" && go mod init socketpatchguardlint >/dev/null && go vet ./... )

test:
strategy:
fail-fast: false
Expand Down
16 changes: 9 additions & 7 deletions composer/socket-patch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ vendor/bin/socket-patch --help
```

This is a thin **launcher** package. On first run `vendor/bin/socket-patch`
downloads the prebuilt binary for your platform from the matching GitHub release
(`v<version>`), verifies it against the release's `SHA256SUMS`, caches it under
your user cache (`~/.cache/socket-patch/bin/` or `%LOCALAPPDATA%\socket-patch\bin\`
on Windows), and execs it. Subsequent runs use the cached binary.

The package version maps to the release it fetches: installing the package at
tag `vX.Y.Z` downloads the `vX.Y.Z` binary.
downloads the prebuilt binary for your platform from the GitHub release
**matching the installed package's own version** (read from Composer's
`InstalledVersions`), verifies it against the release's `SHA256SUMS`, caches it
under your user cache (`~/.cache/socket-patch/bin/` or
`%LOCALAPPDATA%\socket-patch\bin\` on Windows), and execs it. Subsequent runs use
the cached binary.

So `composer require socketsecurity/socket-patch:3.2.0` downloads the `v3.2.0`
binary β€” the binary version always tracks the installed package version.

## Airgapped / offline use

Expand Down
41 changes: 38 additions & 3 deletions composer/socket-patch/bin/socket-patch
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,52 @@
// verifies it against the release's SHA256SUMS, caches it, and execs it. Set
// SOCKET_PATCH_BIN to an existing executable to bypass the download (airgap).
//
// SP_VERSION is kept in sync with the workspace by scripts/version-sync.sh.
// SP_VERSION is a fallback used ONLY when Composer's recorded version for this
// package can't be read (see sp_version); a normal install downloads the binary
// matching the installed package version. Kept in sync by version-sync.sh.

const SP_VERSION = '3.3.0';
const SP_REPO = 'SocketDev/socket-patch';
const SP_BINARY = 'socket-patch';

// Load Composer's autoloader (the bin proxy exposes its path) so we can read the
// version Composer recorded for THIS package β€” the binary we download must match
// the package the user actually installed, not a constant that could drift.
$sp_autoload = $GLOBALS['_composer_autoload_path'] ?? null;
if (is_string($sp_autoload) && is_file($sp_autoload)) {
require_once $sp_autoload;
}

function sp_fail($msg)
{
fwrite(STDERR, "socket-patch: $msg\n");
exit(1);
}

/**
* The version to fetch. Prefer the version Composer recorded for this package
* (matches what the user installed); fall back to the baked SP_VERSION constant
* (`version-sync.sh` keeps it current) when InstalledVersions is unavailable or
* reports a non-release (dev/branch) version with no matching release binary.
*/
function sp_version()
{
if (class_exists('\\Composer\\InstalledVersions')) {
try {
$v = \Composer\InstalledVersions::getPrettyVersion('socketsecurity/socket-patch');
if ($v !== null) {
$v = ltrim($v, 'v');
if (preg_match('/^\d+\.\d+\.\d+/', $v)) {
return $v;
}
}
} catch (\Throwable $e) {
// fall through to the constant
}
}
return SP_VERSION;
}

/** @return array{0:string,1:string} [target-triple, archive-extension] */
function sp_detect_target()
{
Expand Down Expand Up @@ -180,16 +214,17 @@ function sp_resolve_binary()
return $env;
}

$ver = sp_version();
list($target, $ext) = sp_detect_target();
$exe = SP_BINARY . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '');
$cached = sp_cache_dir() . DIRECTORY_SEPARATOR . SP_VERSION
$cached = sp_cache_dir() . DIRECTORY_SEPARATOR . $ver
. DIRECTORY_SEPARATOR . $target . DIRECTORY_SEPARATOR . $exe;
if (is_executable($cached)) {
return $cached;
}

$archive = SP_BINARY . "-$target.$ext";
$base = 'https://github.com/' . SP_REPO . '/releases/download/v' . SP_VERSION;
$base = 'https://github.com/' . SP_REPO . '/releases/download/v' . $ver;
$tmp = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'socket-patch-' . getmypid();
@mkdir($tmp, 0777, true);

Expand Down
5 changes: 3 additions & 2 deletions gem/socket-patch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ socket-patch --help
```

This is a thin **launcher** gem. On first run it downloads the prebuilt binary
for your platform from the matching GitHub release (`v<version>`), verifies it
against the release's `SHA256SUMS`, caches it under your user cache
for your platform from the GitHub release **matching the installed gem's own
version** (so `gem install socket-patch -v 3.2.0` fetches the `v3.2.0` binary),
verifies it against the release's `SHA256SUMS`, caches it under your user cache
(`~/.cache/socket-patch/bin/` or `%LOCALAPPDATA%\socket-patch\bin\` on Windows),
and execs it. Subsequent runs use the cached binary.

Expand Down
26 changes: 22 additions & 4 deletions gem/socket-patch/lib/socket_patch/launcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ module SocketPatch
# GitHub release, verify its SHA-256 against the release's SHA256SUMS,
# extract the binary, cache it, and run it.
module Launcher
# Fallback version, used ONLY when the installed gem's version can't be read
# (e.g. running this file from a checkout). In a real `gem install` the
# download uses the installed gem's own version β€” see `version`.
VERSION = "3.3.0"
REPO = "SocketDev/socket-patch"
BINARY = "socket-patch"
Expand Down Expand Up @@ -46,15 +49,30 @@ def resolve_binary
env = ENV["SOCKET_PATCH_BIN"]
return env if env && !env.empty? && File.executable?(env)

ver = version
target, ext = detect_target
exe = BINARY + (Gem.win_platform? ? ".exe" : "")
cached = File.join(cache_dir, VERSION, target, exe)
cached = File.join(cache_dir, ver, target, exe)
return cached if File.executable?(cached)

download_binary(target, ext, cached)
download_binary(ver, target, ext, cached)
cached
end

# The version to fetch β€” the binary MUST match the CLI package the user
# actually installed, so derive it from the installed gem's own spec rather
# than trusting the `VERSION` constant (which `version-sync.sh` keeps current
# but which could drift). Falls back to the constant when the gem isn't
# activated (e.g. running this file directly from a checkout).
def version
if (spec = Gem.loaded_specs["socket-patch"])
return spec.version.to_s
end
Gem::Specification.find_by_name("socket-patch").version.to_s
rescue StandardError
VERSION
end

# Map the host to a release target triple + archive extension. Mirrors
# scripts/install.sh.
def detect_target
Expand Down Expand Up @@ -111,9 +129,9 @@ def cache_dir

# ── download + verify + extract ───────────────────────────────────────────

def download_binary(target, ext, dest)
def download_binary(ver, target, ext, dest)
archive = "#{BINARY}-#{target}.#{ext}"
base = "https://github.com/#{REPO}/releases/download/v#{VERSION}"
base = "https://github.com/#{REPO}/releases/download/v#{ver}"

Dir.mktmpdir("socket-patch") do |tmp|
archive_path = File.join(tmp, archive)
Expand Down
Loading