Skip to content

Commit 50d704b

Browse files
Copilotyxxhero
andauthored
Publish Helm plugin provenance artifacts in release pipeline (#1006)
* Initial plan * Add release provenance artifact generation * Fix GPG secret handling: use passphrase-file and restrict secrets to tag runs * Improve provenance signing: add GPG agent setup, smoke test, and docs - Add gpgconf --launch gpg-agent before GPG key import in CI - Fix provenance separator format to match Helm parser (\n...\n -> ...\n) - Add provenance-smoke-test job that validates signing with disposable key - Add workflow_dispatch trigger for manual testing - Document required GPG secrets and key rotation in workflow header - Update README with public key import guidance for Helm 4 users Signed-off-by: yxxhero <aiopsclub@163.com> * Address PR review comments - Add GPG_FINGERPRINT guard with clear error message - Add public key download instructions and fingerprint note to README - Fix header comment referencing provenance-smoke-test job name - Fix double trap overwriting GNUPGHOME cleanup in smoke test - Consolidate cleanup into single trap statement Signed-off-by: yxxhero <aiopsclub@163.com> * Address remaining PR review comments - Use printf instead of echo for GPG key import to avoid corruption - Use --with-colons --list-secret-keys for reliable fingerprint extraction - Use HKPS keyserver URL in README for TLS-protected key fetch Signed-off-by: yxxhero <aiopsclub@163.com> * Extract signing logic to shared script and address review comments - Extract provenance signing to scripts/sign-provenance.sh (used by both goreleaser and smoke test) to prevent logic drift - Add sha256sum fallback to shasum for macOS compatibility - Fix grammar in README key fingerprint sentence Signed-off-by: yxxhero <aiopsclub@163.com> * Add offline GPG key import instructions for airgapped environments Signed-off-by: yxxhero <aiopsclub@163.com> * Add comment explaining Helm required ...\n provenance separator Signed-off-by: yxxhero <aiopsclub@163.com> --------- Signed-off-by: yxxhero <aiopsclub@163.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: yxxhero <aiopsclub@163.com>
1 parent b663472 commit 50d704b

4 files changed

Lines changed: 128 additions & 3 deletions

File tree

.github/workflows/release.yaml

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# Release workflow
2+
#
3+
# Prerequisites (configure in Settings > Secrets and variables > Actions):
4+
# - GPG_PRIVATE_KEY: base64-encoded GPG private key for signing release artifacts
5+
# - GPG_FINGERPRINT: Fingerprint of the GPG key
6+
# - GPG_PASSPHRASE: Passphrase for the GPG private key
7+
#
8+
# Key management notes:
9+
# - Use a key with no expiration or set a calendar reminder before expiry
10+
# - To rotate: generate a new keypair, update all three secrets, and verify
11+
# with a test release (see the provenance-smoke-test job)
12+
113
name: Release
214

315
on:
@@ -11,6 +23,7 @@ on:
1123
branches:
1224
- 'main'
1325
- 'master'
26+
workflow_dispatch:
1427

1528
permissions:
1629
contents: write
@@ -21,7 +34,7 @@ jobs:
2134
steps:
2235
-
2336
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
24-
run: echo "flags=--snapshot" >> $GITHUB_ENV
37+
run: echo "flags=--snapshot --skip=sign" >> $GITHUB_ENV
2538
-
2639
name: Checkout
2740
uses: actions/checkout@v6
@@ -32,6 +45,18 @@ jobs:
3245
uses: actions/setup-go@v6
3346
with:
3447
go-version-file: 'go.mod'
48+
-
49+
name: Import GPG key
50+
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
51+
run: |
52+
gpgconf --launch gpg-agent
53+
printf '%s' "${{ secrets.GPG_PRIVATE_KEY }}" | base64 --decode | gpg --batch --import
54+
-
55+
name: Set GPG environment for signing
56+
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
57+
run: |
58+
echo "GPG_FINGERPRINT=${{ secrets.GPG_FINGERPRINT }}" >> "$GITHUB_ENV"
59+
echo "GPG_PASSPHRASE=${{ secrets.GPG_PASSPHRASE }}" >> "$GITHUB_ENV"
3560
-
3661
name: Run GoReleaser
3762
uses: goreleaser/goreleaser-action@v7
@@ -41,3 +66,49 @@ jobs:
4166
args: release --clean ${{ env.flags }}
4267
env:
4368
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69+
70+
provenance-smoke-test:
71+
runs-on: ubuntu-latest
72+
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
73+
steps:
74+
-
75+
name: Checkout
76+
uses: actions/checkout@v6
77+
-
78+
name: Test provenance signing with disposable key
79+
run: |
80+
export GNUPGHOME="$(mktemp -d)"
81+
tmpdir="$(mktemp -d)"
82+
trap 'rm -rf "$GNUPGHOME" "$tmpdir"' EXIT
83+
chmod 700 "$GNUPGHOME"
84+
85+
gpg --batch --pinentry-mode loopback --passphrase '' \
86+
--quick-generate-key "helm-diff-test" ed25519 sign 0
87+
GPG_FINGERPRINT=$(gpg --batch --with-colons --list-secret-keys "helm-diff-test" \
88+
| grep '^fpr:' | head -1 | cut -d: -f10)
89+
export GPG_FINGERPRINT
90+
export GPG_PASSPHRASE=""
91+
92+
echo "dummy binary" > "$tmpdir/bin"
93+
tar czf "$tmpdir/helm-diff-linux-amd64.tgz" -C "$tmpdir" bin
94+
95+
./scripts/sign-provenance.sh "$tmpdir/helm-diff-linux-amd64.tgz" "$tmpdir/helm-diff-linux-amd64.tgz.prov"
96+
97+
if [ ! -f "$tmpdir/helm-diff-linux-amd64.tgz.prov" ]; then
98+
echo "ERROR: provenance file was not created"
99+
exit 1
100+
fi
101+
102+
echo "=== gpg --verify ==="
103+
gpg --verify "$tmpdir/helm-diff-linux-amd64.tgz.prov"
104+
105+
echo ""
106+
echo "=== Signed .prov content ==="
107+
cat "$tmpdir/helm-diff-linux-amd64.tgz.prov"
108+
109+
echo ""
110+
echo "=== Parsed provenance block ==="
111+
gpg --batch --output - "$tmpdir/helm-diff-linux-amd64.tgz.prov" 2>/dev/null
112+
113+
echo ""
114+
echo "Provenance smoke test passed"

.goreleaser.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ archives:
5252
- README.md
5353
- plugin.yaml
5454
- LICENSE
55+
56+
signs:
57+
- id: plugin-provenance
58+
artifacts: archive
59+
signature: "${artifact}.prov"
60+
cmd: ./scripts/sign-provenance.sh
61+
args:
62+
- ${artifact}
63+
- ${signature}
5564
changelog:
5665
use: github-native
5766

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,21 @@ The install script will skip the GitHub download and instead install from the `.
4343

4444
**For Helm 4 users:**
4545

46-
Helm 4 requires plugin verification by default. Since this plugin does not yet provide provenance artifacts, you need to use the `--verify=false` flag:
46+
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`:
4747
4848
```shell
49-
helm plugin install https://github.com/databus23/helm-diff --verify=false
49+
gpg --keyserver hkps://keys.openpgp.org --recv-keys <KEY_FINGERPRINT>
50+
helm plugin install https://github.com/databus23/helm-diff
51+
```
52+
53+
For offline/airgapped environments, download the public key from the GitHub release assets on a connected machine, transfer it, and import it locally:
54+
55+
```shell
56+
gpg --import <public-key.asc>
5057
```
5158
59+
The public key fingerprint is published in the notes for each GitHub release.
60+
5261
For more information about Helm 4's plugin verification, see:
5362
- [Helm 4 Overview](https://helm.sh/docs/overview)
5463
- [HIP-0026: Plugin Provenance](https://github.com/helm/community/blob/main/hips/hip-0026.md)

scripts/sign-provenance.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [ $# -lt 2 ]; then
5+
echo "Usage: $0 <artifact> <signature> [plugin.yaml path]"
6+
exit 1
7+
fi
8+
9+
artifact="$1"
10+
signature="$2"
11+
plugin_yaml="${3:-plugin.yaml}"
12+
13+
if [ -z "${GPG_FINGERPRINT:-}" ]; then
14+
echo "ERROR: GPG_FINGERPRINT is not set. Cannot sign provenance artifact."
15+
exit 1
16+
fi
17+
18+
filename="$(basename "$artifact")"
19+
digest="$(sha256sum "$artifact" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$artifact" | cut -d' ' -f1)"
20+
21+
passphrase_file="$(mktemp)"
22+
trap 'rm -f "$passphrase_file"' EXIT
23+
printf '%s' "${GPG_PASSPHRASE:-}" > "$passphrase_file"
24+
chmod 600 "$passphrase_file"
25+
26+
{
27+
cat "$plugin_yaml"
28+
printf '...\n'
29+
printf 'files:\n %s: "sha256:%s"\n' "$filename" "$digest"
30+
# NOTE: The ...\n separator is required by Helm's provenance parser.
31+
# See helm/helm pkg/provenance/sign.go: parseMessageBlock splits on "\n...\n"
32+
# and messageBlock writes the same separator between metadata and checksums.
33+
} | gpg --batch --yes --armor --pinentry-mode loopback \
34+
--passphrase-file "$passphrase_file" \
35+
--local-user "$GPG_FINGERPRINT" \
36+
--clearsign --output "$signature"

0 commit comments

Comments
 (0)