diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0f617c33..eeae64bc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -72,29 +72,38 @@ jobs: run: | set -e missing=0 - for f in dist/helm-diff-*.tgz; do + # Archives wrap their content in a "diff/" directory: the layout + # every released install/update hook expects (issues #1071, #1076). + # Each platform is published under two names (see .goreleaser.yml): + # the historical "helm-diff--.tgz" and the versioned + # "diff---.tgz" installable by helm 4 directly + # from a local tarball. + for f in dist/*.tgz; do echo "== $f ==" tar tzf "$f" - # Archives are wrapped in a directory named after the archive - # itself (required by helm 4 installs from a tarball, see #1071) - base="$(basename "$f" .tgz)" - for member in "$base/plugin.yaml" "$base/install-binary.sh" "$base/install-binary.ps1"; do + for member in "diff/plugin.yaml" "diff/install-binary.sh" "diff/install-binary.ps1"; do if ! tar tzf "$f" | grep -q "^${member}$"; then echo "ERROR: ${member} missing from ${f}" missing=1 fi done # the binary has a .exe suffix on windows archives - if ! tar tzf "$f" | grep -qE "^${base}/bin/diff(\\.exe)?$"; then - echo "ERROR: ${base}/bin/diff missing from ${f}" + if ! tar tzf "$f" | grep -qE "^diff/bin/diff(\\.exe)?$"; then + echo "ERROR: diff/bin/diff missing from ${f}" missing=1 fi done + legacy_count="$(find dist -maxdepth 1 -name 'helm-diff-*.tgz' | wc -l)" + versioned_count="$(find dist -maxdepth 1 -name 'diff-*.tgz' | wc -l)" + if [ "$legacy_count" -eq 0 ] || [ "$legacy_count" -ne "$versioned_count" ]; then + echo "ERROR: expected an equal, non-zero number of legacy (helm-diff-*) and versioned (diff-*) archives" + missing=1 + fi if [ "$missing" -ne 0 ]; then echo "Smoke test failed: required plugin files missing from one or more archives" exit 1 fi - echo "Smoke test passed: all archives bundle plugin.yaml, install scripts, and binary" + echo "Smoke test passed: all archives bundle plugin.yaml, install scripts, and binary in a diff/ directory" - name: Set up Helm if: ${{ !startsWith(github.ref, 'refs/tags/v') }} @@ -110,9 +119,9 @@ jobs: mkdir -p /tmp/archive-test tar xzf dist/helm-diff-linux-amd64.tgz -C /tmp/archive-test echo "Extracted archive layout:" - find /tmp/archive-test/helm-diff-linux-amd64 -maxdepth 2 -type f | sort + find /tmp/archive-test/diff -maxdepth 2 -type f | sort - out="$(helm plugin install /tmp/archive-test/helm-diff-linux-amd64 2>&1)" + out="$(helm plugin install /tmp/archive-test/diff 2>&1)" echo "$out" # The install hook must find the bundled binary already staged in # HELM_PLUGIN_DIR and skip the network download. @@ -129,17 +138,46 @@ jobs: uses: azure/setup-helm@v5 with: version: v4.3.0 + - + # Regression test for issue #1076: `helm plugin update` runs the + # *already installed* copy of install-binary.sh, so release tarballs + # must keep the layout every previously released hook expects + # ("$HELM_TMP/diff/bin/diff" after extraction). Simulate updating a + # pre-3.15.14 install (v3.15.13 hook) and a 3.15.14 install by + # running their exact install scripts, in update mode, against the + # archives just built. + name: Legacy update hook regression test (snapshot only) + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + run: | + set -e + archive="$PWD/dist/helm-diff-linux-amd64.tgz" + for hook_version in v3.15.13 v3.15.14; do + echo "== simulating 'helm plugin update' with the $hook_version install hook ==" + plugin_dir="$(mktemp -d)/diff" + mkdir -p "$plugin_dir/bin" + git show "${hook_version}:install-binary.sh" > "$plugin_dir/install-binary.sh" + HELM_BIN=helm HELM_PLUGIN_DIR="$plugin_dir" \ + HELM_DIFF_BIN_TGZ="$archive" \ + sh "$plugin_dir/install-binary.sh" -u + test -x "$plugin_dir/bin/diff" + "$plugin_dir/bin/diff" version + echo "$hook_version hook updated the plugin successfully" + done + echo "Legacy update hook regression test passed" - # Regression test for issue #1071: helm 4 must be able to install - # directly from a local tarball (which it verifies by default), so - # archives must be wrapped in a directory named after the tarball. + # directly from a local tarball (which it verifies by default). When + # installing from a local file, helm 4 derives the directory it + # expects inside the archive from the tarball file name + # ("diff---.tgz" -> "diff/"), hence the versioned + # archive names. name: Helm 4 offline tarball install test (snapshot only) if: ${{ !startsWith(github.ref, 'refs/tags/v') }} env: HELM_DATA_HOME: /tmp/helm4-data run: | set -e - archive="dist/helm-diff-linux-amd64.tgz" + archive="$(find dist -maxdepth 1 -name 'diff-*-linux-amd64.tgz')" export GNUPGHOME="$(mktemp -d)" chmod 700 "$GNUPGHOME" diff --git a/.goreleaser.yml b/.goreleaser.yml index 573a83fe..cdee6288 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -40,6 +40,22 @@ builds: - "6" - "7" +# Archives wrap their content in a "diff/" directory. This is the layout +# every install/update hook released so far expects when it downloads a +# release tarball (it copies "$HELM_TMP/diff/bin/diff"), so it must not be +# changed: wrapping in any other directory breaks `helm plugin update` on +# all pre-existing installs (issue #1076). +# +# Helm 4, when installing from a *local* tarball, derives the directory it +# expects inside the archive from the tarball file name: "--…" +# must contain "/" (helm strips a trailing version component from the +# name; see stripPluginName in helm's plugin installer). To support that +# flow (issue #1071) each platform is published a second time under a +# versioned name, "diff---.tgz": the historical +# "helm-diff--.tgz" name cannot satisfy helm 4 because it carries +# no version component to strip. Both names wrap the identical "diff/" +# layout, and each gets its own ".prov" provenance signature (helm's +# verification matches file names against the checksums in the ".prov"). archives: - id: default builds: @@ -47,12 +63,22 @@ archives: - linux-additional format: tgz name_template: '{{ .ProjectName }}-{{ if eq .Os "darwin" }}macos{{ else }}{{ .Os }}{{ end }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - # Wrap archives in a directory named after the archive itself - # (e.g. helm-diff-linux-amd64/). Helm 4's plugin installer requires this - # when installing from a local tarball: it derives the expected directory - # from the tarball filename and fails with "plugin.yaml not found in - # expected directory" otherwise (see issue #1071). - wrap_in_directory: true + wrap_in_directory: diff + files: + - README.md + - plugin.yaml + - LICENSE + - install-binary.sh + - install-binary.ps1 + # The same archive under a versioned, helm 4-friendly name so it can be + # installed (and provenance-verified) directly from a local tarball. + - id: versioned + builds: + - default + - linux-additional + format: tgz + name_template: 'diff-{{ .Version }}-{{ if eq .Os "darwin" }}macos{{ else }}{{ .Os }}{{ end }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + wrap_in_directory: diff files: - README.md - plugin.yaml diff --git a/Makefile b/Makefile index 125cd254..8de708b2 100644 --- a/Makefile +++ b/Makefile @@ -69,20 +69,25 @@ docker-run-release: -w $(pkg) helm-diff-release make release # dist-package builds the plugin for a single platform and packs it into -# release/$(1).tgz. The archive content is wrapped in a directory named -# after the archive itself (e.g. helm-diff-linux-amd64/). Helm 4's plugin -# installer requires this when installing directly from a tarball: it -# derives the expected directory from the tarball filename and fails with -# "plugin.yaml not found in expected directory" otherwise (see issue #1071). +# release/helm-diff-$(1).tgz, plus a byte-identical copy named +# release/diff-$(VERSION)-$(1).tgz. Archives wrap their content in a "diff/" +# directory: that is the layout the install/update hooks of all previously +# released versions expect when they download a release tarball, so it must +# not change (issue #1076). The versioned copy exists because helm 4 derives +# the directory it expects inside a locally installed tarball from the +# tarball file name: "diff---.tgz" must contain "diff/" +# (issue #1071); the historical name carries no version to strip, so it +# cannot serve that flow. # Usage: $(call dist-package,,,[,]) DIST_PACKAGE_FILES := README.md LICENSE plugin.yaml install-binary.sh install-binary.ps1 define dist-package -mkdir -p build/helm-diff-$(1)/bin -cp $(DIST_PACKAGE_FILES) build/helm-diff-$(1)/ +mkdir -p build/diff/bin +cp $(DIST_PACKAGE_FILES) build/diff/ goarch=$(3); bin=diff; [ "$(2)" = "windows" ] && bin=$$bin.exe; \ - $(4) GOOS=$(2) GOARCH=$$goarch $(GO) build -o build/helm-diff-$(1)/bin/$$bin -trimpath -ldflags="$(LDFLAGS)" -tar -C build/ -zcvf $(CURDIR)/release/helm-diff-$(1).tgz helm-diff-$(1)/ -rm -rf build/helm-diff-$(1) + $(4) GOOS=$(2) GOARCH=$$goarch $(GO) build -o build/diff/bin/$$bin -trimpath -ldflags="$(LDFLAGS)" +tar -C build/ -zcvf $(CURDIR)/release/helm-diff-$(1).tgz diff/ +cp $(CURDIR)/release/helm-diff-$(1).tgz $(CURDIR)/release/diff-$(VERSION)-$(1).tgz +rm -rf build/diff endef diff --git a/README.md b/README.md index 23250075..a5c341df 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,13 @@ helm plugin install https://github.com/databus23/helm-diff If installing this in an offline/airgapped environment, download the platform-specific binary archive (e.g., `helm-diff-linux-amd64.tgz` or `helm-diff-windows-amd64.tgz`) from [releases](https://github.com/databus23/helm-diff/releases). Make sure to select the correct `.tgz` file for your operating system and architecture. -The release archives include everything needed to install the plugin (binary, `plugin.yaml`, and the install scripts). Each archive wraps its content in a directory named after the archive itself (e.g. `helm-diff-linux-amd64/`), which is what Helm 4 expects when installing from a tarball. +The release archives include everything needed to install the plugin (binary, `plugin.yaml`, and the install scripts). Each archive wraps its content in a `diff/` directory — the layout the plugin's install scripts have always expected. The simplest way to install offline is to extract the archive and point `helm plugin install` at the extracted directory: ``` -tar xzf helm-diff-linux-amd64.tgz # extracts into a ./helm-diff-linux-amd64 directory -helm plugin install ./helm-diff-linux-amd64 +tar xzf helm-diff-linux-amd64.tgz # extracts into a ./diff directory +helm plugin install ./diff ``` The install script detects that the binary is already bundled and skips the GitHub download. @@ -65,15 +65,15 @@ gpg --list-keys --with-fingerprint EA17A2A206AFF8CD helm plugin install https://github.com/databus23/helm-diff/releases/latest/download/helm-diff-linux-amd64.tgz ``` -For offline/airgapped environments with Helm 4, transfer the release tarball **together with its `.prov` file** (and keep the original tarball file name — provenance verification matches the file name against the checksums recorded in the `.prov` file) and install directly from the tarball: +For offline/airgapped environments with Helm 4, transfer the release tarball **together with its `.prov` file** and install directly from the tarball. Use the **versioned** asset name (`diff---.tgz`, e.g. `diff-3.15.15-linux-amd64.tgz`): when installing from a local file, Helm 4 derives the directory it expects inside the archive from the tarball file name (`diff-3.15.15-linux-amd64.tgz` → `diff/`), so keep the original file name as well — provenance verification also matches the file name against the checksums recorded in the `.prov` file: ```shell -curl -LO https://github.com/databus23/helm-diff/releases/download//helm-diff-linux-amd64.tgz -curl -LO https://github.com/databus23/helm-diff/releases/download//helm-diff-linux-amd64.tgz.prov -helm plugin install helm-diff-linux-amd64.tgz --keyring +curl -LO https://github.com/databus23/helm-diff/releases/download//diff--linux-amd64.tgz +curl -LO https://github.com/databus23/helm-diff/releases/download//diff--linux-amd64.tgz.prov +helm plugin install diff--linux-amd64.tgz --keyring ``` -(Replace `` with the release you are installing. The direct tarball install requires Helm 4. On Helm 3, extract the archive and install from the extracted directory as described above.) +(Replace ``/`` with the release you are installing. The versioned asset is only needed for installs directly from a local tarball; on Helm 3, extract the archive and install from the extracted directory as described above.) For offline/airgapped environments, download the public key from the maintainer's GitHub profile on a connected machine, transfer it, and import it locally: diff --git a/install-binary.ps1 b/install-binary.ps1 index 6b5c563a..84c3fc65 100644 --- a/install-binary.ps1 +++ b/install-binary.ps1 @@ -64,11 +64,13 @@ function Install-Plugin { tar -xzf $ArchiveName -C . Pop-Location New-Item -ItemType Directory -Path $Destination -Force - # Release archives wrap their content in a directory named after the - # archive itself (e.g. helm-diff-windows-amd64/bin/diff.exe), as required - # by helm 4 when installing directly from a tarball (issue #1071). - # Archives from earlier releases wrap the content in a - # directory named "diff" instead. + # Release archives wrap their content in a "diff" directory — the layout + # the install/update hooks of every released version expect, so it must not + # change (issue #1076). The 3.15.14 release instead wrapped the content in a + # directory named after the archive itself (e.g. helm-diff-windows-amd64/ + # bin/diff.exe), as required by helm 4 when installing directly from a + # tarball (issue #1071); support that layout as well so 3.15.14 tarballs + # keep installing and updating. $wrapDir = [System.IO.Path]::GetFileNameWithoutExtension($ArchiveName) $binary = Join-Path $ArchiveDirectory $wrapDir "bin" "diff.exe" if (-not (Test-Path $binary -PathType Leaf)) { @@ -81,9 +83,9 @@ $ErrorActionPreference = "Stop" $arch = Get-Architecture -# Archives wrap their content in a directory named after the archive -# itself (see Install-Plugin below), so the temporary copy must keep the -# original file name. +# The 3.15.14 release archives wrapped their content in a directory named +# after the archive itself (see Install-Plugin below), so the temporary copy +# of HELM_DIFF_BIN_TGZ must keep its original file name. $archiveName = "helm-diff-windows-${arch}.tgz" # If installing (not updating) and the binary is already staged in the diff --git a/install-binary.sh b/install-binary.sh index 7f0df84c..a90e3c8d 100755 --- a/install-binary.sh +++ b/install-binary.sh @@ -110,8 +110,9 @@ rmTempDir() { # for that binary. downloadFile() { # If HELM_DIFF_BIN_TGZ is set, copy the local file instead of downloading. - # Keep its original file name: release archives wrap their content in a - # directory named after the archive (see installFile below). + # Keep its original file name: the 3.15.14 release archives wrapped their + # content in a directory named after the archive, which installFile locates + # via the file name (see below). if [ -n "$HELM_DIFF_BIN_TGZ" ]; then echo "Using local package at $HELM_DIFF_BIN_TGZ" if [ ! -f "$HELM_DIFF_BIN_TGZ" ]; then @@ -162,11 +163,13 @@ installFile() { if [ "${OS}" = "windows" ]; then bin="$bin.exe" fi - # Release archives wrap their content in a directory named after the + # Release archives wrap their content in a "diff/" directory — the layout + # the install/update hooks of every released version expect when they + # download a tarball, so it must not change (issue #1076). The 3.15.14 + # release instead wrapped the content in a directory named after the # archive itself (e.g. helm-diff-linux-amd64/bin/diff), as required by - # helm 4 when installing directly from a tarball (issue #1071). - # Archives from earlier releases wrap the content in a directory named - # "diff" instead. + # helm 4 when installing directly from a tarball (issue #1071); support + # that layout as well so 3.15.14 tarballs keep installing and updating. wrap_dir="$(basename "$PLUGIN_TMP_FILE" .tgz)" HELM_TMP_BIN="$HELM_TMP/$wrap_dir/bin/$bin" if [ ! -f "$HELM_TMP_BIN" ] && [ -f "$HELM_TMP/diff/bin/$bin" ]; then