Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 72 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Release workflow
#
# Prerequisites (configure in Settings > Secrets and variables > Actions):
# - GPG_PRIVATE_KEY: base64-encoded GPG private key for signing release artifacts
# - GPG_FINGERPRINT: Fingerprint of the GPG key
# - GPG_PASSPHRASE: Passphrase for the GPG private key
#
# Key management notes:
# - Use a key with no expiration or set a calendar reminder before expiry
# - To rotate: generate a new keypair, update all three secrets, and verify
# with a test release (see the provenance-smoke-test job)

name: Release

on:
Expand All @@ -11,6 +23,7 @@ on:
branches:
- 'main'
- 'master'
workflow_dispatch:

permissions:
contents: write
Expand All @@ -21,7 +34,7 @@ jobs:
steps:
-
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
run: echo "flags=--snapshot" >> $GITHUB_ENV
run: echo "flags=--snapshot --skip=sign" >> $GITHUB_ENV
-
name: Checkout
uses: actions/checkout@v6
Expand All @@ -32,6 +45,18 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
-
name: Import GPG key
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
gpgconf --launch gpg-agent
printf '%s' "${{ secrets.GPG_PRIVATE_KEY }}" | base64 --decode | gpg --batch --import
-
name: Set GPG environment for signing
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
echo "GPG_FINGERPRINT=${{ secrets.GPG_FINGERPRINT }}" >> "$GITHUB_ENV"
echo "GPG_PASSPHRASE=${{ secrets.GPG_PASSPHRASE }}" >> "$GITHUB_ENV"
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
Expand All @@ -41,3 +66,49 @@ jobs:
args: release --clean ${{ env.flags }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

provenance-smoke-test:
runs-on: ubuntu-latest
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
steps:
-
name: Checkout
uses: actions/checkout@v6
-
name: Test provenance signing with disposable key
run: |
export GNUPGHOME="$(mktemp -d)"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$GNUPGHOME" "$tmpdir"' EXIT
chmod 700 "$GNUPGHOME"

gpg --batch --pinentry-mode loopback --passphrase '' \
--quick-generate-key "helm-diff-test" ed25519 sign 0
GPG_FINGERPRINT=$(gpg --batch --with-colons --list-secret-keys "helm-diff-test" \
| grep '^fpr:' | head -1 | cut -d: -f10)
export GPG_FINGERPRINT
export GPG_PASSPHRASE=""

echo "dummy binary" > "$tmpdir/bin"
tar czf "$tmpdir/helm-diff-linux-amd64.tgz" -C "$tmpdir" bin

./scripts/sign-provenance.sh "$tmpdir/helm-diff-linux-amd64.tgz" "$tmpdir/helm-diff-linux-amd64.tgz.prov"

if [ ! -f "$tmpdir/helm-diff-linux-amd64.tgz.prov" ]; then
echo "ERROR: provenance file was not created"
exit 1
fi

echo "=== gpg --verify ==="
gpg --verify "$tmpdir/helm-diff-linux-amd64.tgz.prov"

echo ""
echo "=== Signed .prov content ==="
cat "$tmpdir/helm-diff-linux-amd64.tgz.prov"

echo ""
echo "=== Parsed provenance block ==="
gpg --batch --output - "$tmpdir/helm-diff-linux-amd64.tgz.prov" 2>/dev/null

echo ""
echo "Provenance smoke test passed"
9 changes: 9 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ archives:
- README.md
- plugin.yaml
- LICENSE

signs:
- id: plugin-provenance
artifacts: archive
signature: "${artifact}.prov"
cmd: ./scripts/sign-provenance.sh
args:
- ${artifact}
- ${signature}
Comment thread
yxxhero marked this conversation as resolved.
changelog:
use: github-native

Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,21 @@ The install script will skip the GitHub download and instead install from the `.

**For Helm 4 users:**

Helm 4 requires plugin verification by default. Since this plugin does not yet provide provenance artifacts, you need to use the `--verify=false` flag:
Helm 4 verifies plugin provenance by default. This project publishes GPG-signed provenance artifacts (`.prov`) alongside release tarballs. To verify, import the project's public key into your keyring before running `helm plugin install`:

```shell
helm plugin install https://github.com/databus23/helm-diff --verify=false
gpg --keyserver hkps://keys.openpgp.org --recv-keys <KEY_FINGERPRINT>
helm plugin install https://github.com/databus23/helm-diff
Comment thread
yxxhero marked this conversation as resolved.
```

For offline/airgapped environments, download the public key from the GitHub release assets on a connected machine, transfer it, and import it locally:

```shell
gpg --import <public-key.asc>
```

The public key fingerprint is published in the notes for each GitHub release.

For more information about Helm 4's plugin verification, see:
- [Helm 4 Overview](https://helm.sh/docs/overview)
- [HIP-0026: Plugin Provenance](https://github.com/helm/community/blob/main/hips/hip-0026.md)
Expand Down
36 changes: 36 additions & 0 deletions scripts/sign-provenance.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail

if [ $# -lt 2 ]; then
echo "Usage: $0 <artifact> <signature> [plugin.yaml path]"
exit 1
fi

artifact="$1"
signature="$2"
plugin_yaml="${3:-plugin.yaml}"

if [ -z "${GPG_FINGERPRINT:-}" ]; then
echo "ERROR: GPG_FINGERPRINT is not set. Cannot sign provenance artifact."
exit 1
fi

filename="$(basename "$artifact")"
digest="$(sha256sum "$artifact" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$artifact" | cut -d' ' -f1)"

passphrase_file="$(mktemp)"
trap 'rm -f "$passphrase_file"' EXIT
printf '%s' "${GPG_PASSPHRASE:-}" > "$passphrase_file"
chmod 600 "$passphrase_file"

{
cat "$plugin_yaml"
printf '...\n'
printf 'files:\n %s: "sha256:%s"\n' "$filename" "$digest"
Comment on lines +26 to +29
# NOTE: The ...\n separator is required by Helm's provenance parser.
# See helm/helm pkg/provenance/sign.go: parseMessageBlock splits on "\n...\n"
# and messageBlock writes the same separator between metadata and checksums.
} | gpg --batch --yes --armor --pinentry-mode loopback \
--passphrase-file "$passphrase_file" \
--local-user "$GPG_FINGERPRINT" \
--clearsign --output "$signature"
Loading