From bc89f6d53401b311f6af488bbcc1a8876ac7b064 Mon Sep 17 00:00:00 2001 From: Jan Ludwina Date: Thu, 16 Jul 2026 18:47:27 +0200 Subject: [PATCH 1/5] Add Helm plugin distribution with GPG-signed releases Ship helm-scribe as a Helm plugin (helm scribe). Adds plugin.yaml and an install-binary.sh hook that downloads the matching prebuilt binary per OS/arch from GitHub Releases. GoReleaser now embeds the plugin files into each archive so verified tarball installs work on Helm v4, and emits a checksums.txt that the install hook verifies. sign-plugin.sh creates Helm provenance (.prov) files per archive and GPG clear-signs them; the release workflow runs it and uploads the .prov files when the HELM_SCRIBE_SIGNING_ENABLED repo variable is set. A one-time scripts/setup-signing-key.sh generates the dedicated signing key, exports the public signing-key.asc, and prints the secret material to add to GitHub. Documented both install paths (git URL with --verify=false, and verified release tarball) in the README. --- .github/workflows/release.yml | 17 ++++ .goreleaser.yml | 9 ++ README.md | 22 +++++ install-binary.sh | 177 ++++++++++++++++++++++++++++++++++ plugin.yaml | 8 ++ scripts/setup-signing-key.sh | 59 ++++++++++++ sign-plugin.sh | 63 ++++++++++++ 7 files changed, 355 insertions(+) create mode 100755 install-binary.sh create mode 100644 plugin.yaml create mode 100755 scripts/setup-signing-key.sh create mode 100755 sign-plugin.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09f3f5c..455d757 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,23 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Sign plugin archives + if: ${{ vars.HELM_SCRIBE_SIGNING_ENABLED == 'true' }} + env: + HELM_SCRIBE_SIGNING_KEY: ${{ secrets.HELM_SCRIBE_SIGNING_KEY }} + HELM_SCRIBE_SIGNING_KEY_ID: ${{ secrets.HELM_SCRIBE_SIGNING_KEY_ID }} + run: | + set -euo pipefail + echo "$HELM_SCRIBE_SIGNING_KEY" | gpg --batch --import + version="${GITHUB_REF_NAME#v}" + ./sign-plugin.sh "$version" dist + - name: Upload provenance files + if: ${{ vars.HELM_SCRIBE_SIGNING_ENABLED == 'true' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh release upload "${GITHUB_REF_NAME}" dist/*.prov --clobber - name: Summary run: | tag="${GITHUB_REF_NAME}" diff --git a/.goreleaser.yml b/.goreleaser.yml index f9a4506..96319f3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -18,6 +18,15 @@ archives: formats: [zip] name_template: >- {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} + files: + - plugin.yaml + - install-binary.sh + - sign-plugin.sh + - README.md + - LICENSE + +checksum: + name_template: checksums.txt changelog: sort: asc diff --git a/README.md b/README.md index 55c3dc4..e46b6cc 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,28 @@ Or, if you wish to install with go: go install github.com/miosp/helm-scribe@latest ``` +### As a Helm plugin + +Install as a Helm plugin and run it as `helm scribe`: + +```sh +# Helm v3 or v4 (git sources cannot be signature-verified) +helm plugin install https://github.com/Miosp/helm-scribe --verify=false +``` + +For a verified install on Helm v4 (from a release tarball), import the signing +key first, then point `helm plugin install` at a release archive: + +```sh +curl -fsSL https://raw.githubusercontent.com/Miosp/helm-scribe/main/signing-key.asc \ + | gpg --dearmor > helm-scribe-keyring.gpg +helm plugin install \ + https://github.com/Miosp/helm-scribe/releases/download/v0.3.1/helm-scribe_v0.3.1_linux_amd64.tar.gz \ + --verify +``` + +Then run `helm scribe` in any chart directory. + ## Add README markers Insert these where the parameters table should appear: diff --git a/install-binary.sh b/install-binary.sh new file mode 100755 index 0000000..7691de7 --- /dev/null +++ b/install-binary.sh @@ -0,0 +1,177 @@ +#!/bin/sh +# Download and install the helm-scribe binary into the Helm plugin directory. +# Invoked by Helm as the plugin install/update hook. +# +# Adapted from the helm-template / helm-schema plugin installers. + +set -eu + +PROJECT_NAME="helm-scribe" +BINARY_NAME="helm-scribe" +PROJECT_GH="Miosp/$PROJECT_NAME" +PLUGIN_MANIFEST="plugin.yaml" +BIN_DIR_SUFFIX="bin" + +# Convert HELM_BIN and HELM_PLUGIN_DIR to unix paths when running under +# MSYS2/Cygwin on Windows, where helm returns Windows paths. +if command -v cygpath >/dev/null 2>&1; then + HELM_BIN="$(cygpath -u "${HELM_BIN}")" + HELM_PLUGIN_DIR="$(cygpath -u "${HELM_PLUGIN_DIR}")" +fi + +[ -z "$HELM_BIN" ] && HELM_BIN="$(command -v helm || true)" + +if [ "${SKIP_BIN_INSTALL:-}" = "1" ]; then + echo "Skipping binary install" + exit 0 +fi + +# install mode pins the binary to plugin.yaml's version; update mode (-u) +# follows the latest release. +SCRIPT_MODE="install" +if [ "${1:-}" = "-u" ]; then + SCRIPT_MODE="update" +fi + +# initArch maps uname -m to the goreleaser arch names we publish. +initArch() { + ARCH="$(uname -m)" + case "$ARCH" in + x86_64|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) + echo "Architecture '$(uname -m)' is not supported." >&2 + exit 1 + ;; + esac +} + +# initOS maps uname -s to the lowercase goreleaser os names we publish. +initOS() { + OS="$(uname -s)" + case "$OS" in + Windows_NT|MSYS*|MINGW*|CYGWIN*) OS="windows" ;; + Darwin) OS="darwin" ;; + Linux) OS="linux" ;; + *) + echo "OS '$(uname)' is not supported." >&2 + exit 1 + ;; + esac +} + +# verifySupported rejects unsupported os/arch combos and requires a downloader. +verifySupported() { + supported="linux-amd64 +linux-arm64 +darwin-amd64 +darwin-arm64 +windows-amd64 +windows-arm64" + if ! echo "$supported" | grep -q "^${OS}-${ARCH}$"; then + echo "No prebuilt binary for ${OS}-${ARCH}." >&2 + exit 1 + fi + if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + echo "Either curl or wget is required." >&2 + exit 1 + fi +} + +# downloadFile fetches a URL to stdout. +downloadFile() { + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$1" + else + wget -qO- "$1" + fi +} + +# getDownloadURL sets DOWNLOAD_URL and CHECKSUM_URL based on mode and version. +# Release tags are v-prefixed (e.g. v0.3.1); archive filenames use the bare +# version (e.g. helm-scribe_0.3.1_linux_amd64.tar.gz). +getDownloadURL() { + ext="tar.gz" + if [ "$OS" = "windows" ]; then + ext="zip" + fi + version="$(grep <"$HELM_PLUGIN_DIR/$PLUGIN_MANIFEST" '^version:' | head -n1 | sed 's/[^:]*: *"\{0,1\}\([^"]*\)"\{0,1\}/\1/')" + version="${version#v}" + tag="v${version}" + archive="${PROJECT_NAME}_${version}_${OS}_${ARCH}.${ext}" + base="https://github.com/${PROJECT_GH}/releases" + if [ "$SCRIPT_MODE" = "install" ] && [ -n "$version" ]; then + DOWNLOAD_URL="${base}/download/${tag}/${archive}" + CHECKSUM_URL="${base}/download/${tag}/checksums.txt" + else + DOWNLOAD_URL="${base}/latest/download/${archive}" + CHECKSUM_URL="${base}/latest/download/checksums.txt" + fi +} + +mkTempDir() { + HELM_TMP="$(mktemp -d 2>/dev/null || mktemp -d -t "${PROJECT_NAME}-XXXXXX")" +} + +rmTempDir() { + if [ -n "${HELM_TMP:-}" ] && [ -d "$HELM_TMP" ]; then + rm -rf "$HELM_TMP" + fi +} + +# extractFile extracts the downloaded archive into the plugin bin dir. +extractFile() { + bin_dir="${HELM_PLUGIN_DIR}/${BIN_DIR_SUFFIX}" + mkdir -p "$bin_dir" + archive="$(basename "$DOWNLOAD_URL")" + if [ "$OS" = "windows" ]; then + unzip -o "$HELM_TMP/$archive" -d "$HELM_TMP/unpacked" >/dev/null + mv "$HELM_TMP/unpacked/${BINARY_NAME}.exe" "${bin_dir}/${BINARY_NAME}.exe" 2>/dev/null \ + || mv "$HELM_TMP/unpacked/${BINARY_NAME}" "${bin_dir}/${BINARY_NAME}.exe" + else + tar -xzf "$HELM_TMP/$archive" -C "$HELM_TMP" + mv "$HELM_TMP/${BINARY_NAME}" "${bin_dir}/${BINARY_NAME}" + chmod +x "${bin_dir}/${BINARY_NAME}" + fi +} + +# verifyChecksum checks the archive hash against checksums.txt. +verifyChecksum() { + archive="$(basename "$DOWNLOAD_URL")" + checksums="$(downloadFile "$CHECKSUM_URL" || true)" + if [ -z "$checksums" ]; then + echo "Warning: could not download checksums.txt; skipping verification." >&2 + return 0 + fi + expected="$(echo "$checksums" | grep -E " ${archive}\$" | awk '{print $1}')" + if [ -z "$expected" ]; then + echo "Warning: ${archive} not found in checksums.txt; skipping verification." >&2 + return 0 + fi + actual="$(sha256sum "$HELM_TMP/$archive" | awk '{print $1}')" + if [ "$actual" != "$expected" ]; then + echo "Checksum mismatch for ${archive}." >&2 + echo " expected: ${expected}" >&2 + echo " actual: ${actual}" >&2 + rmTempDir + exit 1 + fi +} + +initArch +initOS +verifySupported +getDownloadURL + +echo "Downloading ${DOWNLOAD_URL}" +mkTempDir +trap rmTempDir EXIT + +archive="$(basename "$DOWNLOAD_URL")" +downloadFile "$DOWNLOAD_URL" > "$HELM_TMP/$archive" + +verifyChecksum +extractFile + +echo "$PROJECT_NAME installed into ${HELM_PLUGIN_DIR}/${BIN_DIR_SUFFIX}/${BINARY_NAME}" +echo "Run 'helm scribe --help' to get started." diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..fdfb3db --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,8 @@ +name: "scribe" +version: "0.3.1" +usage: "generate a Helm values schema and README parameters table" +description: "Generate a values.schema.json and README parameters table from annotated values.yaml" +command: "$HELM_PLUGIN_DIR/bin/helm-scribe" +hooks: + install: "$HELM_PLUGIN_DIR/install-binary.sh" + update: "$HELM_PLUGIN_DIR/install-binary.sh -u" diff --git a/scripts/setup-signing-key.sh b/scripts/setup-signing-key.sh new file mode 100755 index 0000000..6b6cbae --- /dev/null +++ b/scripts/setup-signing-key.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# One-time helper: generate the helm-scribe GPG signing key. +# +# Run locally (never in CI). Produces: +# 1. signing-key.asc -> commit to the repository (public key) +# 2. private-key armor -> print to stdout for the GitHub secret +# HELM_SCRIBE_SIGNING_KEY +# 3. the key fingerprint -> for the GitHub secret +# HELM_SCRIBE_SIGNING_KEY_ID +# +# Usage: ./scripts/setup-signing-key.sh + +set -euo pipefail + +KEY_FILE="signing-key.asc" +KEY_ID_FILE=".signing-key-id" + +if [ -f "$KEY_FILE" ] && [ "${1:-}" != "--force" ]; then + echo "$KEY_FILE already exists. Re-run with --force to regenerate." >&2 + exit 1 +fi + +echo "Generating a new RSA 4096 signing key for helm-scribe..." +gpg --batch --full-generate-key <<'EOF' +%no-protection +Key-Type: RSA +Key-Length: 4096 +Key-Usage: sign +Name-Real: helm-scribe +Name-Email: helm-scribe@users.noreply.github.com +Expire-Date: 0 +%commit +EOF + +fingerprint="$(gpg --list-keys --with-colons "helm-scribe@users.noreply.github.com" | awk -F: '/^fpr:/ {print $10; exit}')" + +if [ -z "$fingerprint" ]; then + echo "Failed to locate the generated key fingerprint." >&2 + exit 1 +fi + +gpg --armor --export "$fingerprint" > "$KEY_FILE" +echo "$fingerprint" > "$KEY_ID_FILE" +grep -qx "$KEY_ID_FILE" .gitignore 2>/dev/null || echo "$KEY_ID_FILE" >> .gitignore + +cat < [dist-dir] + +set -euo pipefail + +PROJECT_NAME="helm-scribe" +PLUGIN_NAME="scribe" +PROJECT_GH="Miosp/${PROJECT_NAME}" +DESCRIPTION="Generate a values.schema.json and README parameters table from annotated values.yaml" +VERSION="${1:-}" +DIST_DIR="${2:-dist}" +GPG_KEY="${HELM_SCRIBE_SIGNING_KEY_ID:-}" + +if [ -z "$VERSION" ]; then + echo "Usage: $0 [dist-dir]" >&2 + exit 1 +fi +if [ -z "$GPG_KEY" ]; then + echo "Error: HELM_SCRIBE_SIGNING_KEY_ID must be set." >&2 + exit 1 +fi + +shopt -s nullglob +archives=("$DIST_DIR"/*/*.tar.gz "$DIST_DIR"/*/*.zip "$DIST_DIR"/*.tar.gz "$DIST_DIR"/*.zip) +shopt -u nullglob + +if [ "${#archives[@]}" -eq 0 ]; then + echo "No archives found in $DIST_DIR." >&2 + exit 1 +fi + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +for archive in "${archives[@]}"; do + [ -f "$archive" ] || continue + name="$(basename "$archive")" + hash="$(sha256sum "$archive" | awk '{print $1}')" + + cat > "$tmp/payload.yaml" < Date: Thu, 16 Jul 2026 19:22:36 +0200 Subject: [PATCH 2/5] Add signing public key --- .gitignore | 2 ++ signing-key.asc | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 signing-key.asc diff --git a/.gitignore b/.gitignore index 4a89ba1..01d6318 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ go.work.sum helm-scribe docs/ .test-fixture/ +.signing-key-id +.signing-private-key.asc diff --git a/signing-key.asc b/signing-key.asc new file mode 100644 index 0000000..d7b8c3c --- /dev/null +++ b/signing-key.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGpZEzMBEAC/eaQI7661Tgc3EuCEqVjRAZwHfCHiohZy5ZpKxZl0ziw2sXqw +Gb3I/pXwrmEG6KOqHC6keU2i+xAysf+pOYS7uo43I6HFxK7spsnsqm4s4uMMCBya +mYcjzFB1NBAijq+s6c3JfOfwQe7tRjG2x12vqDUWtlfNbpkpOHF6LVkqR0om//m1 +mCHkkQcV2EUtaFbUzc6K4AFA7GjBpGXOkEBOveOnKhGbLyEDGCLJdJXsTcsRN0EI +2PI673J3c+e01ro8Ozt5e6IUghud8RmDCYlJZXMG2putm1AVTPpsrBAOJzlb63+Q +RD/5/mHFiv6MKgqbzHiumVMFrXEia4ROJLJU26aqmOxzqDTyc86xzxq1dx9WsVIc +p8aiKGGO7baPsD/Wiy8Mv/2D/1qPReSMGrof2g5PnCA536VqHaYhpaEPyqqgF/GA +e3WIbKDDwAyEzF2Nh8ahDkEspFt8KSaTG4vsRxdb1rfFkrrM+6NTkg6ckrC0s52y +n9b5n1cSQEI14swMd8cJ88AKwM8tb4NkV7Ngovv9wVTFELfZ4YsDtvBxblYOUJ75 +7ce21udUt9pTFdsimFTQnE+6lIw+xaXazGnyC/8O2/fgxXD9r+jbUsGw6GAjkZ5m +NSkWURwFu4T9V3u2iRKLsHSlMQrUT3p6/fEEsbqdLqPLunuSS2D9oNbK6QARAQAB +tDJoZWxtLXNjcmliZSA8aGVsbS1zY3JpYmVAdXNlcnMubm9yZXBseS5naXRodWIu +Y29tPokCTgQTAQoAOBYhBAge2yYcxFx1Niao6paPadkZJiDIBQJqWRMzAhsDBQsJ +CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEJaPadkZJiDIktEQAKdFpFrRLrPISH7i +rJ3/B8I7jG8D3fGzVImeL/dFFhN7NvDeBkHHw/TtgzGQAkTq8zZuVneFPTl1GxOX +ZmrBM9J2mQWyET3LsKVKYhDOkkzrXkjbcJzCvPhb974+pYMepdbmECihEpr6+QaD +mMTAiZY9zrwBrJzs4X3bgaqbDa5VWP/WPO6/Mu0aeRBvCmq2msrdjlZqKoXF4fHT +e9gFis/4/wpqlTc9CoJiyrbGzjd0J3UkSp9HVZliu2unEV3/b2TMjQF8sS1iuJcs +fLqQHKeZ5ap7jgeu+iOmR5XwzDuoeAbn+omKoKwV8pmEMKOnIrvgJjduJFBEKHG+ +JhVdfLGuFVYVMytt9bU/TVY9M8BJdVQ6p/87WQsWH5Y88T5/DhG9qQT7TgZiYElb +EAQ1/dv6tx08Te/HGaBg1XCPVASCH81coI1ZD0k2ZR4Eh0+bL+LJYxj9U7SZjdq1 +JS5IfgEkEvJwVfjEfNRZ1kaeP5D/LfRESBa32E9WFzhPSgFhsyvnUZDsXoxy6BSl +eIDeeKFIW++Qha+dCKQSlZjktDSeXNb1RKsLd5DeckKCFgriPTWfQvQAMVKR6RXw +tuuLWg9eaB6Jn3+c0qaSgIWtFwSKdcYCXM+S8ekXpOR9FMWLJWmL1SOVCautZeR4 +WBFo2YRuyC2JTchE2bF9lXql/MoS +=jW0U +-----END PGP PUBLIC KEY BLOCK----- From 75ce88e4554162c82640abb8838bfdc6d8f7b38c Mon Sep 17 00:00:00 2001 From: Jan Ludwina Date: Thu, 16 Jul 2026 19:26:01 +0200 Subject: [PATCH 3/5] Auto-stamp plugin version and clean up key setup install-binary.sh now resolves the latest release tag itself via GitHub's releases/latest redirect, so it no longer reads plugin.yaml's version field. Both install and update pull the newest release, which removes the need to manually bump plugin.yaml per release. The Release workflow stamps plugin.yaml's version from the git tag before GoReleaser packs it, so archive installs show the correct version in 'helm plugin list'. plugin.yaml's committed version is now a placeholder. Also smooths scripts/setup-signing-key.sh: writes the private armor to a gitignored file for easy clipboard copy, and prints clear GitHub secret/variable instructions. --- .github/workflows/release.yml | 5 +++++ install-binary.sh | 39 +++++++++++++++++++++-------------- plugin.yaml | 4 +++- scripts/setup-signing-key.sh | 33 ++++++++++++++++++++++------- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 455d757..2ad92c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,11 @@ jobs: - uses: actions/setup-go@v6 with: go-version-file: go.mod + - name: Stamp plugin version + run: | + version="${GITHUB_REF_NAME#v}" + sed -i "s/^version:.*/version: \"${version}\"/" plugin.yaml + echo "Stamped plugin.yaml version: ${version}" - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/install-binary.sh b/install-binary.sh index 7691de7..f717843 100755 --- a/install-binary.sh +++ b/install-binary.sh @@ -9,7 +9,6 @@ set -eu PROJECT_NAME="helm-scribe" BINARY_NAME="helm-scribe" PROJECT_GH="Miosp/$PROJECT_NAME" -PLUGIN_MANIFEST="plugin.yaml" BIN_DIR_SUFFIX="bin" # Convert HELM_BIN and HELM_PLUGIN_DIR to unix paths when running under @@ -26,12 +25,9 @@ if [ "${SKIP_BIN_INSTALL:-}" = "1" ]; then exit 0 fi -# install mode pins the binary to plugin.yaml's version; update mode (-u) -# follows the latest release. -SCRIPT_MODE="install" -if [ "${1:-}" = "-u" ]; then - SCRIPT_MODE="update" -fi +# Helm passes -u for updates. Both install and update resolve the latest +# release tag, so plugin.yaml's version field is never load-bearing here. +# install-binary.sh reads $1 only to stay compatible with Helm's hook contract. # initArch maps uname -m to the goreleaser arch names we publish. initArch() { @@ -87,7 +83,19 @@ downloadFile() { fi } -# getDownloadURL sets DOWNLOAD_URL and CHECKSUM_URL based on mode and version. +# resolveLatestTag returns the latest release tag (e.g. v0.3.1) by following +# GitHub's /releases/latest redirect. No API call, so no rate limit. +resolveLatestTag() { + url="https://github.com/${PROJECT_GH}/releases/latest" + if command -v curl >/dev/null 2>&1; then + headers="$(curl -fsSI "$url" 2>/dev/null || true)" + else + headers="$(wget -Sq --spider "$url" 2>&1 >/dev/null || true)" + fi + echo "$headers" | tr -d '\r' | awk 'tolower($1)=="location:"{print $2}' | sed 's:.*/tag/::; s:.*/::' +} + +# getDownloadURL sets DOWNLOAD_URL and CHECKSUM_URL against the latest release. # Release tags are v-prefixed (e.g. v0.3.1); archive filenames use the bare # version (e.g. helm-scribe_0.3.1_linux_amd64.tar.gz). getDownloadURL() { @@ -95,18 +103,17 @@ getDownloadURL() { if [ "$OS" = "windows" ]; then ext="zip" fi - version="$(grep <"$HELM_PLUGIN_DIR/$PLUGIN_MANIFEST" '^version:' | head -n1 | sed 's/[^:]*: *"\{0,1\}\([^"]*\)"\{0,1\}/\1/')" + version="$(resolveLatestTag)" version="${version#v}" + if [ -z "$version" ]; then + echo "Could not resolve the latest release tag for ${PROJECT_GH}." >&2 + exit 1 + fi tag="v${version}" archive="${PROJECT_NAME}_${version}_${OS}_${ARCH}.${ext}" base="https://github.com/${PROJECT_GH}/releases" - if [ "$SCRIPT_MODE" = "install" ] && [ -n "$version" ]; then - DOWNLOAD_URL="${base}/download/${tag}/${archive}" - CHECKSUM_URL="${base}/download/${tag}/checksums.txt" - else - DOWNLOAD_URL="${base}/latest/download/${archive}" - CHECKSUM_URL="${base}/latest/download/checksums.txt" - fi + DOWNLOAD_URL="${base}/download/${tag}/${archive}" + CHECKSUM_URL="${base}/download/${tag}/checksums.txt" } mkTempDir() { diff --git a/plugin.yaml b/plugin.yaml index fdfb3db..afd5d89 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,7 @@ name: "scribe" -version: "0.3.1" +# Auto-stamped from the release tag by the Release workflow; not load-bearing +# for installs (install-binary.sh resolves the latest release itself). +version: "0.0.0" usage: "generate a Helm values schema and README parameters table" description: "Generate a values.schema.json and README parameters table from annotated values.yaml" command: "$HELM_PLUGIN_DIR/bin/helm-scribe" diff --git a/scripts/setup-signing-key.sh b/scripts/setup-signing-key.sh index 6b6cbae..2fe58a4 100755 --- a/scripts/setup-signing-key.sh +++ b/scripts/setup-signing-key.sh @@ -14,6 +14,7 @@ set -euo pipefail KEY_FILE="signing-key.asc" KEY_ID_FILE=".signing-key-id" +PRIVATE_FILE=".signing-private-key.asc" if [ -f "$KEY_FILE" ] && [ "${1:-}" != "--force" ]; then echo "$KEY_FILE already exists. Re-run with --force to regenerate." >&2 @@ -41,19 +42,35 @@ fi gpg --armor --export "$fingerprint" > "$KEY_FILE" echo "$fingerprint" > "$KEY_ID_FILE" -grep -qx "$KEY_ID_FILE" .gitignore 2>/dev/null || echo "$KEY_ID_FILE" >> .gitignore +gpg --armor --export-secret-keys "$fingerprint" > "$PRIVATE_FILE" +for f in "$KEY_ID_FILE" "$PRIVATE_FILE"; do + grep -qx "$f" .gitignore 2>/dev/null || echo "$f" >> .gitignore +done cat < Secrets and variables > Actions > Secrets + > New repository secret. Add two secrets: + + Name: HELM_SCRIBE_SIGNING_KEY_ID + Value: ${fingerprint} + + Name: HELM_SCRIBE_SIGNING_KEY + Value: + (also written to ${PRIVATE_FILE}, e.g. \`pbcopy < ${PRIVATE_FILE}\`) + + 3. GitHub: Settings > Secrets and variables > Actions > Variables + > New repository variable: + + Name: HELM_SCRIBE_SIGNING_ENABLED + Value: true + +----- BEGIN HELM_SCRIBE_SIGNING_KEY ----- +$(cat "$PRIVATE_FILE")----- END HELM_SCRIBE_SIGNING_KEY ----- EOF From cdb3c5380f7ee0f9e7924b7e79a13b22f5a19477 Mon Sep 17 00:00:00 2001 From: Jan Ludwina Date: Thu, 16 Jul 2026 19:29:36 +0200 Subject: [PATCH 4/5] Move plugin scripts into scripts/ install-binary.sh and sign-plugin.sh were at the repo root only because that mirrors helm-schema's layout. helm itself only requires plugin.yaml at the root; hook scripts can live anywhere. This repo keeps all helpers under scripts/, so move them there for consistency and update plugin.yaml, the GoReleaser files list, and the release workflow. Also drop sign-plugin.sh from the published archives since it is a CI-time script users never run. --- .github/workflows/release.yml | 2 +- .goreleaser.yml | 3 +-- plugin.yaml | 4 ++-- install-binary.sh => scripts/install-binary.sh | 0 sign-plugin.sh => scripts/sign-plugin.sh | 0 5 files changed, 4 insertions(+), 5 deletions(-) rename install-binary.sh => scripts/install-binary.sh (100%) rename sign-plugin.sh => scripts/sign-plugin.sh (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ad92c2..522d7aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: set -euo pipefail echo "$HELM_SCRIBE_SIGNING_KEY" | gpg --batch --import version="${GITHUB_REF_NAME#v}" - ./sign-plugin.sh "$version" dist + ./scripts/sign-plugin.sh "$version" dist - name: Upload provenance files if: ${{ vars.HELM_SCRIBE_SIGNING_ENABLED == 'true' }} env: diff --git a/.goreleaser.yml b/.goreleaser.yml index 96319f3..674f59f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -20,8 +20,7 @@ archives: {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} files: - plugin.yaml - - install-binary.sh - - sign-plugin.sh + - scripts/install-binary.sh - README.md - LICENSE diff --git a/plugin.yaml b/plugin.yaml index afd5d89..92b377a 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -6,5 +6,5 @@ usage: "generate a Helm values schema and README parameters table" description: "Generate a values.schema.json and README parameters table from annotated values.yaml" command: "$HELM_PLUGIN_DIR/bin/helm-scribe" hooks: - install: "$HELM_PLUGIN_DIR/install-binary.sh" - update: "$HELM_PLUGIN_DIR/install-binary.sh -u" + install: "$HELM_PLUGIN_DIR/scripts/install-binary.sh" + update: "$HELM_PLUGIN_DIR/scripts/install-binary.sh -u" diff --git a/install-binary.sh b/scripts/install-binary.sh similarity index 100% rename from install-binary.sh rename to scripts/install-binary.sh diff --git a/sign-plugin.sh b/scripts/sign-plugin.sh similarity index 100% rename from sign-plugin.sh rename to scripts/sign-plugin.sh From c98c5e421ed4cdf6600f731c326709322906f136 Mon Sep 17 00:00:00 2001 From: Jan Ludwina Date: Thu, 16 Jul 2026 19:41:54 +0200 Subject: [PATCH 5/5] Add native Windows support to the Helm plugin Helm execs plugin commands and hooks directly with no shell, so the previous manifest (a bare command with no .exe and a .sh install hook) could not install or run on Windows. Switch plugin.yaml to platformCommand/platformHooks with per-OS entries: linux/darwin keep the .sh hook and the extensionless binary, windows points the command at helm-scribe.exe and runs a new scripts/install-binary.ps1 install hook. install-binary.ps1 mirrors install-binary.sh: it resolves the latest release tag via GitHub's releases/latest redirect, downloads the matching windows zip, verifies it against checksums.txt when available, and extracts helm-scribe.exe into the plugin bin dir. It targets Windows PowerShell 5.1 (shipped with Windows) so it works without PowerShell 7. platformCommand/platformHooks require Helm 3.18+; the plugin therefore needs Helm 3.18 or 4 on all platforms. GoReleaser now packs install-binary.ps1 into every archive so verified tarball installs are self-contained on Windows. --- .goreleaser.yml | 1 + plugin.yaml | 28 +++++++++--- scripts/install-binary.ps1 | 91 ++++++++++++++++++++++++++++++++++++++ scripts/install-binary.sh | 1 - 4 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 scripts/install-binary.ps1 diff --git a/.goreleaser.yml b/.goreleaser.yml index 674f59f..72ba0fd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -21,6 +21,7 @@ archives: files: - plugin.yaml - scripts/install-binary.sh + - scripts/install-binary.ps1 - README.md - LICENSE diff --git a/plugin.yaml b/plugin.yaml index 92b377a..9fe95a2 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,10 +1,28 @@ name: "scribe" # Auto-stamped from the release tag by the Release workflow; not load-bearing -# for installs (install-binary.sh resolves the latest release itself). +# for installs (the install hooks resolve the latest release themselves). version: "0.0.0" usage: "generate a Helm values schema and README parameters table" description: "Generate a values.schema.json and README parameters table from annotated values.yaml" -command: "$HELM_PLUGIN_DIR/bin/helm-scribe" -hooks: - install: "$HELM_PLUGIN_DIR/scripts/install-binary.sh" - update: "$HELM_PLUGIN_DIR/scripts/install-binary.sh -u" +platformCommand: + - os: linux + command: $HELM_PLUGIN_DIR/bin/helm-scribe + - os: darwin + command: $HELM_PLUGIN_DIR/bin/helm-scribe + - os: windows + command: $HELM_PLUGIN_DIR/bin/helm-scribe.exe +platformHooks: + install: + - os: linux + command: $HELM_PLUGIN_DIR/scripts/install-binary.sh + - os: darwin + command: $HELM_PLUGIN_DIR/scripts/install-binary.sh + - os: windows + command: powershell -ExecutionPolicy Bypass -File $HELM_PLUGIN_DIR/scripts/install-binary.ps1 + update: + - os: linux + command: $HELM_PLUGIN_DIR/scripts/install-binary.sh + - os: darwin + command: $HELM_PLUGIN_DIR/scripts/install-binary.sh + - os: windows + command: powershell -ExecutionPolicy Bypass -File $HELM_PLUGIN_DIR/scripts/install-binary.ps1 diff --git a/scripts/install-binary.ps1 b/scripts/install-binary.ps1 new file mode 100644 index 0000000..110cc18 --- /dev/null +++ b/scripts/install-binary.ps1 @@ -0,0 +1,91 @@ +# Download and install the helm-scribe binary into the Helm plugin directory. +# Invoked by Helm as the plugin install/update hook on Windows. +# Targets Windows PowerShell 5.1 (built into Windows) and PowerShell 7+. +#Requires -Version 5.1 + +$ErrorActionPreference = "Stop" + +$ProjectName = "helm-scribe" +$ProjectGh = "Miosp/$ProjectName" + +$PluginDir = $env:HELM_PLUGIN_DIR +if (-not $PluginDir) { throw "HELM_PLUGIN_DIR is not set." } + +# GitHub requires TLS 1.2. +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + +# Resolve the latest release tag via GitHub's /releases/latest redirect (no API, +# no rate limit). AllowAutoRedirect=$false makes GetResponse throw on the 302; +# the redirect Location lives on the exception's response. +function Resolve-LatestTag { + $req = [System.Net.HttpWebRequest]::Create("https://github.com/$ProjectGh/releases/latest") + $req.AllowAutoRedirect = $false + try { + $resp = $req.GetResponse() + } catch [System.Net.WebException] { + $resp = $_.Exception.Response + } + if (-not $resp) { throw "Could not resolve the latest release tag for $ProjectGh." } + $location = $resp.Headers["Location"] + $resp.Close() + if (-not $location) { throw "Could not resolve the latest release tag for $ProjectGh." } + return ($location -split '/')[-1] +} + +# Map the Windows architecture to the goreleaser arch name we publish. +switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { $arch = "amd64" } + "ARM64" { $arch = "arm64" } + default { throw "Architecture '$env:PROCESSOR_ARCHITECTURE' is not supported." } +} + +$tag = Resolve-LatestTag +$version = $tag -replace '^v', '' +$archive = "${ProjectName}_${version}_windows_${arch}.zip" +$base = "https://github.com/$ProjectGh/releases/download/$tag" +$downloadUrl = "$base/$archive" +$checksumUrl = "$base/checksums.txt" + +$tmp = Join-Path ([System.IO.Path]::GetTempPath()) "helm-scribe-install-$([System.Guid]::NewGuid())" +New-Item -ItemType Directory -Path $tmp -Force | Out-Null +try { + $zip = Join-Path $tmp $archive + Write-Host "Downloading $downloadUrl" + Invoke-WebRequest -Uri $downloadUrl -OutFile $zip -UseBasicParsing + + # Checksum verification against checksums.txt (best-effort; older releases + # may not ship one). + try { + $checksums = (Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing).Content + $expected = $null + foreach ($line in ($checksums -split "`n")) { + if ($line -match "\s$([regex]::Escape($archive))\s*$") { + $expected = ($line -split '\s+')[0] + break + } + } + if ($expected) { + $actual = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected.ToLower()) { + throw "Checksum mismatch for $archive.`n expected: $expected`n actual: $actual" + } + } else { + Write-Warning "$archive not found in checksums.txt; skipping verification." + } + } catch { + Write-Warning "Could not download checksums.txt; skipping verification." + } + + # Extract and install the binary. + Expand-Archive -Path $zip -DestinationPath $tmp -Force + $binDir = Join-Path $PluginDir "bin" + New-Item -ItemType Directory -Path $binDir -Force | Out-Null + $exe = Join-Path $tmp "$ProjectName.exe" + if (-not (Test-Path $exe)) { throw "Binary $ProjectName.exe not found in the archive." } + Move-Item -Force $exe (Join-Path $binDir "$ProjectName.exe") + + Write-Host "$ProjectName installed into $binDir\$ProjectName.exe" + Write-Host "Run 'helm scribe --help' to get started." +} finally { + Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue +} diff --git a/scripts/install-binary.sh b/scripts/install-binary.sh index f717843..2d2e57a 100755 --- a/scripts/install-binary.sh +++ b/scripts/install-binary.sh @@ -27,7 +27,6 @@ fi # Helm passes -u for updates. Both install and update resolve the latest # release tag, so plugin.yaml's version field is never load-bearing here. -# install-binary.sh reads $1 only to stay compatible with Helm's hook contract. # initArch maps uname -m to the goreleaser arch names we publish. initArch() {