From d4944eb9596b93d5666f6084b59884f13fef017c Mon Sep 17 00:00:00 2001 From: "dylan.wang" Date: Fri, 25 Sep 2026 21:00:42 +0800 Subject: [PATCH 1/2] installer: download only from the release on GitHub for v1.0.2 install.sh no longer reads ENVRELAY_DOWNLOAD_URL. Every download comes from https://github.com/FutrixDev/envrelay-skill/releases, with curl held to HTTPS and TLS 1.2, and no http, file:// or local path. Only the release workflow's verify jobs and tests/installer.sh ever set the variable, and ClawHub's audit rated v1.0.0 and v1.0.1 Review because of it (T03: it accepted http, and SHA256SUMS came from the same place as the files). The tests put tests/stubs/curl first on PATH instead. It serves a release packaged on the spot and refuses any call that is not held to HTTPS and TLS 1.2 or that asks for anything but this repository's release, so the tests run the installer users get. A new scenario covers a missing asset. Anyone who set ENVRELAY_DOWNLOAD_URL now downloads from GitHub; it shipped only in v1.0.0 and v1.0.1. ADR-027 records the decision and amends ADR-024 and ADR-026. docs/publishing.md now says how to read ClawHub's audit verdict, which is separate from moderation. Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/plugin.json | 2 +- .github/scripts/check-versions.sh | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 8 ++- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- docs/decisions/024-one-command-install.md | 31 ++++---- ...atter-and-manifests-for-awesome-copilot.md | 6 +- ...27-installer-downloads-only-from-github.md | 71 +++++++++++++++++++ docs/publishing.md | 27 +++++-- plugin.json | 2 +- skills/envrelay/SKILL.md | 5 +- skills/envrelay/install.sh | 37 ++++------ tests/installer.sh | 30 +++++--- tests/stubs/curl | 53 ++++++++++++++ 17 files changed, 213 insertions(+), 71 deletions(-) create mode 100644 docs/decisions/027-installer-downloads-only-from-github.md create mode 100755 tests/stubs/curl diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 35d14a1..f86c174 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "envrelay", - "version": "1.0.1", + "version": "1.0.2", "description": "Move a development environment to a new machine: back up dotfiles, credentials, git repositories, AI coding agent state and installed software into one passphrase-encrypted file, then restore it step by step.", "author": { "name": "FutrixDev", diff --git a/.github/scripts/check-versions.sh b/.github/scripts/check-versions.sh index 462be76..36d29c2 100755 --- a/.github/scripts/check-versions.sh +++ b/.github/scripts/check-versions.sh @@ -33,7 +33,7 @@ version=$(sed -n 's/^version = "\([^"]*\)"$/\1/p' Cargo.toml | head -n 1) # second way: this is the path a user's agent takes. home=$(mktemp -d) trap 'rm -rf "$home"' EXIT -plan=$(HOME="$home" ENVRELAY_DOWNLOAD_URL='' ENVRELAY_BIN_DIR='' \ +plan=$(HOME="$home" ENVRELAY_BIN_DIR='' \ sh skills/envrelay/install.sh --dry-run --bin-only --no-modify-path 2>&1) || { printf '%s\n' "$plan" >&2 exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f9fe59..301c323 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,6 @@ jobs: - name: shellcheck run: | shellcheck --version - shellcheck skills/envrelay/install.sh .github/scripts/*.sh tests/installer.sh + shellcheck skills/envrelay/install.sh .github/scripts/*.sh tests/installer.sh tests/stubs/curl - name: The versions agree run: sh .github/scripts/check-versions.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19a56de..7105e97 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,7 +119,9 @@ jobs: if-no-files-found: error # Install from the packaged release, as a user would, before anything is - # published. + # published. install.sh downloads only from the release on GitHub, which + # does not exist yet: tests/stubs/curl, first on PATH, serves dist in its + # place. verify: name: verify ${{ matrix.os }} needs: package @@ -130,6 +132,7 @@ jobs: matrix: os: [macos-latest, ubuntu-latest, ubuntu-24.04-arm] steps: + - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 with: name: dist @@ -138,7 +141,8 @@ jobs: run: | home="$RUNNER_TEMP/home" mkdir "$home" - HOME="$home" SHELL=/bin/bash ENVRELAY_DOWNLOAD_URL="$PWD/dist" sh dist/install.sh + HOME="$home" SHELL=/bin/bash PATH="$PWD/tests/stubs:$PATH" RELEASE_DIR="$PWD/dist" \ + sh dist/install.sh "$home/.local/bin/envrelay" --version test -f "$home/.agents/skills/envrelay/SKILL.md" test -L "$home/.claude/skills/envrelay" diff --git a/Cargo.lock b/Cargo.lock index 7d52169..07f967a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,7 +424,7 @@ dependencies = [ [[package]] name = "envrelay" -version = "1.0.1" +version = "1.0.2" dependencies = [ "age", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 42a1223..399d353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "envrelay" -version = "1.0.1" +version = "1.0.2" edition = "2024" rust-version = "1.97.0" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 9c9dc19..7f9c17c 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ sh tests/installer.sh sh .github/scripts/check-versions.sh ``` -`tests/installer.sh` runs the installer against a release packaged from this checkout, with a throwaway `HOME` per scenario, so it needs the release build first. `SH=dash sh tests/installer.sh` runs the installer under another shell; CI runs it under sh, dash, bash and zsh. `check-versions.sh` checks that the version is the same everywhere it is written. +`tests/installer.sh` runs the installer against a release packaged from this checkout, with a throwaway `HOME` per scenario, so it needs the release build first. The installer downloads only from GitHub, so a stand-in for curl, [`tests/stubs/curl`](tests/stubs/curl), serves that release in GitHub's place. `SH=dash sh tests/installer.sh` runs the installer under another shell; CI runs it under sh, dash, bash and zsh. `check-versions.sh` checks that the version is the same everywhere it is written. ## Releasing diff --git a/README.zh-CN.md b/README.zh-CN.md index 19d6e72..98180b0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -225,7 +225,7 @@ sh tests/installer.sh sh .github/scripts/check-versions.sh ``` -`tests/installer.sh` 用当前 checkout 打出来的 release 跑安装器,每个场景一个一次性的 `HOME`,所以要先 build release。`SH=dash sh tests/installer.sh` 换一个 shell 跑安装器;CI 在 sh、dash、bash、zsh 下各跑一遍。`check-versions.sh` 检查所有写了版本号的地方是否一致。 +`tests/installer.sh` 用当前 checkout 打出来的 release 跑安装器,每个场景一个一次性的 `HOME`,所以要先 build release。安装器只从 GitHub 下载,所以由一个替身 curl([`tests/stubs/curl`](tests/stubs/curl))代替 GitHub 提供这个 release。`SH=dash sh tests/installer.sh` 换一个 shell 跑安装器;CI 在 sh、dash、bash、zsh 下各跑一遍。`check-versions.sh` 检查所有写了版本号的地方是否一致。 ## 发版 diff --git a/docs/decisions/024-one-command-install.md b/docs/decisions/024-one-command-install.md index 9035b1d..2d8ceb3 100644 --- a/docs/decisions/024-one-command-install.md +++ b/docs/decisions/024-one-command-install.md @@ -4,7 +4,8 @@ Date: 2026-09-24 Status: accepted Extends: ADR-022 (deterministic mechanics scripts) Amended by: ADR-025 (the site moves to a repository of its own), ADR-026 -(frontmatter and manifests that awesome-copilot accepts) +(frontmatter and manifests that awesome-copilot accepts), ADR-027 (the +installer downloads only from EnvRelay's release on GitHub) ## Context @@ -67,9 +68,11 @@ and the arm64 Linux runners and the attestations need a public repository too. - **No sudo, ever.** It refuses to run under sudo and installs into the home directory: the binary into `~/.local/bin` (or `--bin-dir`, `ENVRELAY_BIN_DIR`), the skill into `~/.agents/skills/envrelay`. -- **Everything is downloaded and checked before anything changes.** Each file - must match `SHA256SUMS`; curl is held to HTTPS and TLS 1.2. The new binary is - run once from where it will live, since `/tmp` is noexec on some systems. +- **Everything is downloaded and checked before anything changes**, and only + from this repository's release on GitHub, which nothing can point elsewhere + (ADR-027). Each file must match `SHA256SUMS`; curl is held to HTTPS and TLS + 1.2. The new binary is run once from where it will live, since `/tmp` is + noexec on some systems. - **One real copy of the skill, links for the rest.** `~/.agents/skills` is read by Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode, OpenClaw and most other agents. Claude Code gets a symlink in @@ -91,9 +94,9 @@ and the arm64 Linux runners and the attestations need a public repository too. user clicks through; on Linux it prints the distribution's install command for the user to run. - `--dry-run` downloads and changes nothing, and says what would happen. - `ENVRELAY_DOWNLOAD_URL` swaps the GitHub release for a mirror or a local - directory, which is how CI and `tests/installer.sh` install a release that - has not been published. +- CI and `tests/installer.sh` install a release that has not been published + through `tests/stubs/curl`, a stand-in for curl that serves it in GitHub's + place (ADR-027). **The installer is the one exception to ADR-022's "a script never installs".** It lives at the skill's root, not in `scripts/`, and SKILL.md says it is not @@ -128,13 +131,13 @@ the latest release. scripts and nothing else. The installer and `envrelay` stay behind a prompt. - `compatibility` names what the skill needs to run. - A top-level `clawdis` block declares the required binaries (python3, git), - the operating systems (darwin, linux), the two optional environment - variables the installer reads, and the homepage, and ClawHub's review - compares them with what the code does. v1.0.0 had them in - `metadata.openclaw`, where OpenClaw itself also read them to decide whether - the skill can load. They moved because awesome-copilot's lint refuses a - `metadata` value that is not a string. OpenClaw does not read the new block - (ADR-026). + the operating systems (darwin, linux), the one optional environment variable + the installer reads (`ENVRELAY_BIN_DIR`; two before ADR-027), and the + homepage, and ClawHub's review compares them with what the code does. v1.0.0 + had them in `metadata.openclaw`, where OpenClaw itself also read them to + decide whether the skill can load. They moved because awesome-copilot's lint + refuses a `metadata` value that is not a string. OpenClaw does not read the + new block (ADR-026). - There is no `license` field. ClawHub releases every skill it publishes under MIT-0 and asks for no conflicting license terms in SKILL.md, so the field would be wrong there. The repository's MIT OR Apache-2.0 covers the source, diff --git a/docs/decisions/026-frontmatter-and-manifests-for-awesome-copilot.md b/docs/decisions/026-frontmatter-and-manifests-for-awesome-copilot.md index fb616a8..5517269 100644 --- a/docs/decisions/026-frontmatter-and-manifests-for-awesome-copilot.md +++ b/docs/decisions/026-frontmatter-and-manifests-for-awesome-copilot.md @@ -4,6 +4,8 @@ Date: 2026-09-25 Status: accepted Amends: ADR-024 (one command installs the binary and the skill), ADR-025 (the site moves to a repository of its own) +Amended by: ADR-027 (the installer downloads only from EnvRelay's release on +GitHub) ## Context @@ -49,8 +51,8 @@ No one frontmatter satisfies all of these. ## Decision - **The declarations move to a top-level `clawdis` block**, unchanged: the - required binaries, the operating systems, the two optional environment - variables and the homepage. `metadata` keeps only `version`. + required binaries, the operating systems, the optional environment variables + (one since ADR-027) and the homepage. `metadata` keeps only `version`. - **`plugin.json` at the root is the plugin's Agent Plugins manifest**: the Claude Code one plus the `$schema` that opts into Agent Plugins 1.0. Copilot CLI and VS Code read it with that format's semantics, and it is where diff --git a/docs/decisions/027-installer-downloads-only-from-github.md b/docs/decisions/027-installer-downloads-only-from-github.md new file mode 100644 index 0000000..d1a9ae5 --- /dev/null +++ b/docs/decisions/027-installer-downloads-only-from-github.md @@ -0,0 +1,71 @@ +# ADR-027: The installer downloads only from EnvRelay's release on GitHub + +Date: 2026-09-25 +Status: accepted +Amends: ADR-024 (one command installs the binary and the skill), ADR-026 +(frontmatter and manifests that awesome-copilot accepts) + +## Context + +ADR-024 gave `install.sh` an environment variable that changed where it +downloads from: `ENVRELAY_DOWNLOAD_URL` replaced the GitHub release with a +mirror, or with a local directory holding the release's files. Only the +installer's `--help` and the skill's frontmatter mentioned it, and only two +things ever set it: the release workflow's *verify* jobs, which install a +release before it is published, and `tests/installer.sh`. No mirror exists. + +ClawHub's security audit rated v1.0.0 and v1.0.1 "Review" because of it. Its +one finding (T03, high) is that the variable accepted plain http, while +`SHA256SUMS` came from the same place as the files it checks. Whoever could set +the variable, or answer that http request, chose the binary, and the installer +runs a new binary once (`--version`) before it puts it in place. + +The owner's rule: a variable that only tests use does not belong in what is +released. + +## Decision + +- **`install.sh` downloads only from + `https://github.com/FutrixDev/envrelay-skill/releases`**: the release that + matches the skill beside it, the one `--version` names, or the latest. + Nothing changes that: no variable or option, no http, no `file://` and no + local path. curl stays held to HTTPS and TLS 1.2. +- **The tests replace curl instead.** `tests/stubs/curl`, first on `PATH`, + answers the installer's downloads from a release packaged on the spot. It + refuses a call that is not held to HTTPS and TLS 1.2, or that asks for + anything but this repository's release. `tests/installer.sh` and the + *verify* jobs both use it, so they run the installer users get, calling curl + the way it does for them. +- **The frontmatter's `clawdis` block declares one optional environment + variable**, `ENVRELAY_BIN_DIR`. +- This ships as v1.0.2. + +## Consequences + +- **A mirror or a local copy of a release no longer works.** Anyone who set + `ENVRELAY_DOWNLOAD_URL` now downloads from GitHub. It shipped only in v1.0.0 + and v1.0.1, both released on 2026-09-25. A machine that cannot reach GitHub + cannot use the installer. +- **No test takes a path that users do not.** Every install the tests make + asks for the same URLs a user's does. +- **The stub has to follow the installer.** A change to how `install.sh` calls + curl fails the tests until the stub accepts it, which is the point: the + flags that hold curl to HTTPS are checked on every run. +- **wget stays untested**, as before: the stub stands in for curl, and every + runner has curl. +- **The checksums still come from the release they check.** They catch a + corrupted or mixed-up download, not a forged release. What the installer + trusts is HTTPS to GitHub; build provenance (`gh attestation verify`) is how + anyone checks that a file came from this repository's release workflow. + +## Alternatives rejected + +- **Keep the variable, https only.** The finding would shrink, but a variable + that only tests use would still ship. +- **Sign `SHA256SUMS` and pin the key in `install.sh`.** It would make a mirror + safe, but no one needs a mirror, and a signing key is one more secret to + keep. +- **Take `--passphrase-file` out of the binary too**, the other thing only + tests use. The owner declined: the skill is the core of the product, and it + is the agent's own judgment, guided by SKILL.md's first rule, that keeps the + passphrase out of its hands. diff --git a/docs/publishing.md b/docs/publishing.md index 83b172e..d03655d 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -141,8 +141,8 @@ works as soon as the repository is public, and the README lists it. `chat.plugins.marketplaces` setting. Factory Droid falls back to `.claude-plugin/marketplace.json` as well (its commands were not checked). -Validate before every release; it passes with no errors or warnings as of -v1.0.1: +Validate before every release, and release only when it reports no errors or +warnings: ```bash claude plugin validate --strict . @@ -281,17 +281,30 @@ To publish: clawhub skill publish ./skills/envrelay --owner futrixdev --name EnvRelay --version 1.0.0 --changelog "First release." --categories operations,development --topics backup,restore,migration,dotfiles,developer-environment --source-repo FutrixDev/envrelay-skill --source-commit "$(git rev-parse HEAD)" --source-ref v1.0.0 --source-path skills/envrelay ``` - For a later release, change the tag, `--version` and `--changelog`. The - upload stays hidden while ClawHub reviews it (`clawhub inspect` shows - `pending.publication`). v1.0.0's scan came back clean within a minute; how - long publication takes after that was not verified. + For a later release, change the tag (in `git checkout` and `--source-ref`), + `--version` and `--changelog`. The upload stays hidden while ClawHub + reviews it (`clawhub inspect` shows `pending.publication`). v1.0.0's scan + came back clean within a minute; how long publication takes after that was + not verified. -4. Check the listing: +4. Check the listing, then the version's security audit. They are separate + verdicts: moderation decides whether the listing is public, and it can be + public (`clean`) while the audit on its page says Review, which asks users + to read the findings before they install. ```bash clawhub inspect @futrixdev/envrelay ``` + ```bash + clawhub inspect @futrixdev/envrelay --version 1.0.0 --json + ``` + + The audit is `version.security`. v1.0.0 and v1.0.1 read `suspicious`, + shown as Review, for the download override that ADR-027 removed. The + findings are on + . + ```bash openclaw skills verify @futrixdev/envrelay ``` diff --git a/plugin.json b/plugin.json index 75a1b8d..74dda70 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "envrelay", - "version": "1.0.1", + "version": "1.0.2", "description": "Move a development environment to a new machine: back up dotfiles, credentials, git repositories, AI coding agent state and installed software into one passphrase-encrypted file, then restore it step by step.", "author": { "name": "FutrixDev", diff --git a/skills/envrelay/SKILL.md b/skills/envrelay/SKILL.md index 618edba..7388184 100644 --- a/skills/envrelay/SKILL.md +++ b/skills/envrelay/SKILL.md @@ -4,7 +4,7 @@ description: Use when backing up, restoring, or migrating a development environm compatibility: Needs macOS or Linux with a terminal the user can type into, python3 3.9 or newer, git, and the envrelay binary, which the skill's own installer adds once the user agrees. allowed-tools: Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/*) metadata: - version: "1.0.1" + version: "1.0.2" clawdis: requires: bins: @@ -14,9 +14,6 @@ clawdis: - name: ENVRELAY_BIN_DIR required: false description: Where install.sh puts the envrelay binary instead of ~/.local/bin. - - name: ENVRELAY_DOWNLOAD_URL - required: false - description: A mirror, or a local directory of release assets, that install.sh downloads from instead of GitHub releases. os: - darwin - linux diff --git a/skills/envrelay/install.sh b/skills/envrelay/install.sh index 67180f6..c7d2416 100755 --- a/skills/envrelay/install.sh +++ b/skills/envrelay/install.sh @@ -9,7 +9,8 @@ # In order, it: # # 1. downloads SHA256SUMS, the envrelay binary for this machine and the skill -# from one release, and refuses any file whose checksum does not match; +# from one release on GitHub, over HTTPS, and refuses any file whose +# checksum does not match; # 2. puts envrelay in ~/.local/bin (or --bin-dir), replacing it atomically; # 3. puts the skill in ~/.agents/skills/envrelay, which Codex, Cursor, Gemini # CLI, OpenCode, Copilot and most other agents read, and symlinks it into @@ -54,9 +55,8 @@ Options: -h, --help show this help Run from inside an installed skill (sh /install.sh), it installs the -release that matches that skill's version rather than the latest one. -ENVRELAY_DOWNLOAD_URL replaces the GitHub release URL with a mirror, or with a -local directory holding the release assets. +release that matches that skill's version rather than the latest one. Every +download comes from https://github.com/FutrixDev/envrelay-skill/releases. EOF } @@ -220,22 +220,13 @@ unlink_one() { } fetch() { - case $1 in - https://* | http://*) - if have curl; then - case $1 in - https://*) curl --proto '=https' --tlsv1.2 -fsSL --retry 3 -o "$2" "$1" ;; - *) curl -fsSL --retry 3 -o "$2" "$1" ;; - esac - elif have wget; then - wget -q -O "$2" "$1" - else - die "downloading EnvRelay needs curl or wget" - fi - ;; - file://*) cp "${1#file://}" "$2" ;; - *) cp "$1" "$2" ;; - esac || die "could not download $1 (is there a published release? --version picks one)" + if have curl; then + curl --proto '=https' --tlsv1.2 -fsSL --retry 3 -o "$2" "$1" + elif have wget; then + wget -q -O "$2" "$1" + else + die "downloading EnvRelay needs curl or wget" + fi || die "could not download $1 (is there a published release? --version picks one)" } verify() { @@ -526,14 +517,12 @@ main() { fi detect_platform - [ -n "$version" ] || [ -n "${ENVRELAY_DOWNLOAD_URL:-}" ] || version=$(skill_version) + [ -n "$version" ] || version=$(skill_version) version=${version#v} case $version in *[!0-9A-Za-z.+-]*) die "not a version: $version" ;; esac - if [ -n "${ENVRELAY_DOWNLOAD_URL:-}" ]; then - base=${ENVRELAY_DOWNLOAD_URL%/} - elif [ -n "$version" ]; then + if [ -n "$version" ]; then base=https://github.com/$REPO/releases/download/v$version else base=https://github.com/$REPO/releases/latest/download diff --git a/tests/installer.sh b/tests/installer.sh index 4791bf1..482fafc 100755 --- a/tests/installer.sh +++ b/tests/installer.sh @@ -1,7 +1,9 @@ #!/bin/sh # End-to-end tests for skills/envrelay/install.sh. The release it installs is # packaged on the spot by .github/scripts/package-release.sh, the same script -# the release workflow runs, and every scenario gets a throwaway HOME. +# the release workflow runs, and every scenario gets a throwaway HOME. The +# installer downloads only from EnvRelay's release on GitHub: tests/stubs/curl, +# first on its PATH, serves the packaged release in GitHub's place. # # tests/installer.sh [BINARY] BINARY: target/release/envrelay # SH=dash tests/installer.sh run the installer under another shell @@ -51,7 +53,7 @@ scenario() { link=$h/.claude/skills/envrelay out=$work/$1.out : >"$out" - url=$work/release + release=$work/release test_path=/usr/bin:/bin test_shell=/bin/zsh } @@ -60,8 +62,8 @@ scenario() { # lacks ~/.local/bin. Sets $status; the output is in $out. run_installer() { set +e - env HOME="$h" SHELL="$test_shell" PATH="$test_path" ZDOTDIR= XDG_CONFIG_HOME= \ - CLAUDE_CONFIG_DIR= ENVRELAY_BIN_DIR= SUDO_USER= ENVRELAY_DOWNLOAD_URL="$url" \ + env HOME="$h" SHELL="$test_shell" PATH="$root/tests/stubs:$test_path" ZDOTDIR= XDG_CONFIG_HOME= \ + CLAUDE_CONFIG_DIR= ENVRELAY_BIN_DIR= SUDO_USER= RELEASE_DIR="$release" \ "$SH" "${installer:-$work/release/install.sh}" "$@" >"$out" 2>&1 status=$? set -e @@ -72,8 +74,8 @@ run_piped() { script=$1 shift set +e - env HOME="$h" SHELL="$test_shell" PATH="$test_path" ZDOTDIR= XDG_CONFIG_HOME= \ - CLAUDE_CONFIG_DIR= ENVRELAY_BIN_DIR= SUDO_USER= ENVRELAY_DOWNLOAD_URL="$url" \ + env HOME="$h" SHELL="$test_shell" PATH="$root/tests/stubs:$test_path" ZDOTDIR= XDG_CONFIG_HOME= \ + CLAUDE_CONFIG_DIR= ENVRELAY_BIN_DIR= SUDO_USER= RELEASE_DIR="$release" \ "$SH" -s -- "$@" <"$script" >"$out" 2>&1 status=$? set -e @@ -116,7 +118,6 @@ check "replaces the whole skill" absent "$skill/scripts/gone_in_this_release.py" check "leaves no staging behind after an upgrade" no_leftovers # Inside an installed skill it asks for the release that matches the skill. -url= installer=$skill/install.sh run_installer --dry-run --bin-only check "asks for the skill's own release" said "/releases/download/v${version#envrelay }" @@ -192,7 +193,6 @@ metadata: name: envrelay --- EOF -url= installer=$link/install.sh run_installer --dry-run --bin-only check "exits 0" status_is 0 @@ -249,6 +249,7 @@ run_installer --dry-run check "exits 0" status_is 0 check "changes nothing" home_empty check "says it is a dry run" said "Dry run" +check "downloads from the latest release on GitHub" said "from https://github.com/FutrixDev/envrelay-skill/releases/latest/download" check "shows where the skill would go" said "would install the skill in $skill" check "shows the link it would make" said "would link $link" check "shows the PATH change" said "would add $h/.local/bin to PATH in $h/.zshrc" @@ -280,7 +281,7 @@ check "keeps a skill another tool installed" [ "$(cat "$skill/SKILL.md")" = thei scenario tampered-binary cp -R "$work/release" "$work/tampered-binary.release" for f in "$work/tampered-binary.release"/envrelay-*-*.tar.gz; do printf x >>"$f"; done -url=$work/tampered-binary.release +release=$work/tampered-binary.release run_installer check "fails" failed_run check "names the mismatch" said "does not match SHA256SUMS" @@ -289,11 +290,20 @@ check "installs nothing" home_empty scenario tampered-skill cp -R "$work/release" "$work/tampered-skill.release" printf x >>"$work/tampered-skill.release/envrelay-skill.tar.gz" -url=$work/tampered-skill.release +release=$work/tampered-skill.release run_installer check "fails" failed_run check "installs nothing, not even the binary" home_empty +scenario missing-asset +cp -R "$work/release" "$work/missing-asset.release" +rm "$work/missing-asset.release/envrelay-skill.tar.gz" +release=$work/missing-asset.release +run_installer +check "fails" failed_run +check "names the download" said "could not download https://github.com/FutrixDev/envrelay-skill/releases/latest/download/envrelay-skill.tar.gz" +check "installs nothing, not even the binary" home_empty + scenario truncated size=$(wc -c <"$work/release/install.sh") for pct in 10 25 50 75 90 99; do diff --git a/tests/stubs/curl b/tests/stubs/curl new file mode 100755 index 0000000..612e340 --- /dev/null +++ b/tests/stubs/curl @@ -0,0 +1,53 @@ +#!/bin/sh +# A stand-in for curl, put first on PATH by tests/installer.sh and by the +# release workflow's verify job. install.sh downloads only from EnvRelay's +# release on GitHub, which does not exist yet when those run; this serves +# RELEASE_DIR, a release packaged by .github/scripts/package-release.sh, in its +# place. It takes only the one way install.sh calls curl, so every install in +# the tests also checks that each download is held to HTTPS and TLS 1.2 and +# asks for EnvRelay's release. A file the release lacks gets curl's answer to a +# 404. +set -eu + +refuse() { + printf 'curl (test stand-in): %s\n' "$*" >&2 + exit 2 +} + +[ -d "${RELEASE_DIR:-}" ] || refuse "RELEASE_DIR is not a directory: ${RELEASE_DIR:-unset}" +proto='' tls=0 out='' url='' +while [ $# -gt 0 ]; do + case $1 in + --proto | --retry | -o) + [ $# -ge 2 ] || refuse "$1 needs a value" + case $1 in + --proto) proto=$2 ;; + -o) out=$2 ;; + esac + shift + ;; + --tlsv1.2) tls=1 ;; + -fsSL) ;; + -*) refuse "unexpected option: $1" ;; + *) + [ -z "$url" ] || refuse "more than one URL: $url $1" + url=$1 + ;; + esac + shift +done +[ "$proto" = =https ] || refuse "not held to HTTPS: $url" +[ "$tls" = 1 ] || refuse "not held to TLS 1.2: $url" +[ -n "$out" ] || refuse "no -o FILE: $url" +releases=https://github.com/FutrixDev/envrelay-skill/releases +case $url in +"$releases"/latest/download/* | "$releases"/download/v*/*) ;; +*) refuse "not a download from EnvRelay's release on GitHub: $url" ;; +esac + +file=$RELEASE_DIR/${url##*/} +if [ ! -f "$file" ]; then + echo 'curl: (22) The requested URL returned error: 404' >&2 + exit 22 +fi +cp "$file" "$out" From 887b7bc3c0528516633d939e7c8f5775bbc93f3e Mon Sep 17 00:00:00 2001 From: "dylan.wang" Date: Fri, 25 Sep 2026 21:04:01 +0800 Subject: [PATCH 2/2] docs: ADR-024 says plainly when CI installs through the stand-in curl Co-Authored-By: Claude Opus 5.5 --- docs/decisions/024-one-command-install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/024-one-command-install.md b/docs/decisions/024-one-command-install.md index 2d8ceb3..f41709d 100644 --- a/docs/decisions/024-one-command-install.md +++ b/docs/decisions/024-one-command-install.md @@ -94,7 +94,7 @@ and the arm64 Linux runners and the attestations need a public repository too. user clicks through; on Linux it prints the distribution's install command for the user to run. - `--dry-run` downloads and changes nothing, and says what would happen. -- CI and `tests/installer.sh` install a release that has not been published +- Before a release is published, CI and `tests/installer.sh` install it through `tests/stubs/curl`, a stand-in for curl that serves it in GitHub's place (ADR-027).