VAULT-38917: adding verify prod release binaries in release procedure yaml (#10937) (#11200)

* adding verify prod release binaries in release procedure yaml

* adding verify prod release binaries in release procedure yaml

* fixing lint

* fixing lint

* fixing lint

* fixing lint

* fixing lint

* fixing lint

* adding list binary versions

* adding list binary

* adding list binary

* adding unit tests

* adding comments to tests and scripts

* adding comments to tests and scripts

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* fixing conflicts

* testing pipeline

* update verification logic

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* adding slack notification

* adding slack notification

* adding slack notification

* adding slack notification

* adding slack notification

* adding slack notification

* adding slack notification

* adding slack notification

* adding slack notification

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* testing pipeline

* addressing comment

Co-authored-by: Tin Vo <tintvo08@gmail.com>
This commit is contained in:
Vault Automation 2025-12-15 16:53:00 -05:00 committed by GitHub
parent 56de87a0c8
commit f7accefc40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 746 additions and 5 deletions

View file

@ -0,0 +1,152 @@
#!/usr/bin/env bash
# Copyright IBM Corp. 2016, 2025
# SPDX-License-Identifier: BUSL-1.1
set -e
binaries="$1"
if [ -z "$binaries" ]; then
echo "Error: JSON input is required."
exit 1
fi
##########################################
# Initialize result holders
##########################################
version_missing=""
variant_missing=""
os_missing=""
valid_versions_output=""
valid_variants_output=""
valid_os_output=""
##########################################
# 1. Verify Versions
##########################################
invalid_versions=$(echo "$binaries" | jq -r '.invalid_versions[]?')
valid_versions_output+="Versions:\n "
versions=$(echo "$binaries" | jq -r '.valid_versions | keys[]')
for v in $versions; do
valid_versions_output+="$v, "
done
valid_versions_output+="\n"
if [ -n "$invalid_versions" ]; then
for v in $invalid_versions; do
version_missing+="Missing Versions: $v\n"
done
fi
##########################################
# 2. Verify Variants
##########################################
for v in $versions; do
base_version="${v%-lts}"
valid_variants_output+="Version: $v\n "
req=(
"$base_version"
"${base_version}+ent"
"${base_version}+ent.fips"
"${base_version}+ent.hsm"
"${base_version}+ent.hsm.fips"
)
actual_variants=$(echo "$binaries" | jq -r ".valid_versions[\"$v\"].variants[].variant")
for r in "${req[@]}"; do
if echo "$actual_variants" | grep -q "^$r"; then
valid_variants_output+="$r, "
else
variant_missing+="Version $v is missing variant: $r\n"
fi
done
valid_variants_output+="\n"
done
##########################################
# 3. Verify OS
##########################################
for v in $versions; do
base_version="${v%-lts}"
variants=$(echo "$binaries" | jq -r ".valid_versions[\"$v\"].variants[].variant")
valid_os_output+="Version: $v"
for variant in $variants; do
valid_os_output+="\n Variant: $variant\n "
os_list=$(echo "$binaries" | jq -r \
".valid_versions[\"$v\"].variants[] | select(.variant==\"$variant\") | .os[]")
if [[ "$variant" == "$base_version" || "$variant" == "${base_version}+ent" ]]; then
for os in darwin freebsd linux netbsd openbsd solaris windows; do
if grep -qx "$os" <<< "$os_list"; then
valid_os_output+="$os, "
else
os_missing+="Variant $variant of version $v is missing OS: $os\n"
fi
done
else
for os in $os_list; do
if [[ "$os" == "linux" ]]; then
valid_os_output+=" ✓ linux"
else
os_missing+="Variant $variant of version $v has invalid OS: $os\n"
fi
done
fi
done
valid_os_output+="\n"
done
##########################################
# 4. OUTPUT LOGIC
##########################################
if [ -z "$version_missing" ] && [ -z "$variant_missing" ] && [ -z "$os_missing" ]; then
echo "*Available Versions:*"
printf "%b" "$valid_versions_output"
exit 0
fi
##########################################
# If ANYTHING is missing → print full output
##########################################
echo "*Available Versions:*"
printf "%b" "$valid_versions_output"
echo -e "\n*Available Variants:*"
printf "%b" "$valid_variants_output"
echo -e "\n*Available OS:*"
printf "%b" "$valid_os_output"
if [ -n "$version_missing" ]; then
echo -e "\n*Missing Release Binary Versions:*"
printf "%b" "$version_missing"
fi
if [ -n "$variant_missing" ]; then
echo -e "\n*Missing Release Binary Variants:*"
printf "%b" "$variant_missing"
fi
if [ -n "$os_missing" ]; then
echo -e "\n*Missing Release Binary OS:*"
printf "%b" "$os_missing"
fi

View file

@ -7,6 +7,7 @@ ignore internal/pkg/golang/fixtures
require (
github.com/Masterminds/semver v1.5.0
github.com/PuerkitoBio/goquery v1.11.0
github.com/avast/retry-go/v4 v4.6.1
github.com/google/go-github/v74 v74.0.0
github.com/hashicorp/hcl/v2 v2.24.0
@ -24,6 +25,7 @@ require (
require (
github.com/agext/levenshtein v1.2.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@ -70,11 +72,11 @@ require (
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -16,8 +16,12 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@ -74,6 +78,7 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE=
github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
@ -96,6 +101,7 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hashicorp/releases-api v0.2.3 h1:mwNR+lKgJtIyeSQXYGM86fZ0u8ed09v7NS2ePKmVvyc=
github.com/hashicorp/releases-api v0.2.3/go.mod h1:J8AiSwS1Qy/m/RmHskUGDu9YQRLKreBBswc6ZTY5/tI=
github.com/hashicorp/releases-api v0.2.9/go.mod h1:TKqUiMrAF06VtpFVS50uUbjt+A+syM/59EV6uNx7W7M=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -189,6 +195,7 @@ github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/veqryn/slog-context v0.8.0 h1:lDhwAgjwx52K5StqqQzi5d0Y/F4SNyGZbsXGd8MtucM=
github.com/veqryn/slog-context v0.8.0/go.mod h1:8rsT72p0kzzN9lmkwtabIhxg7ZkpnKblt9x3Eix8Tc0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE=
github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
@ -207,30 +214,103 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=

View file

@ -13,6 +13,7 @@ func newReleasesListCmd() *cobra.Command {
}
listCmd.AddCommand(newReleasesVersionsBetweenCmd())
listCmd.AddCommand(newReleasesListBinaryVersionsCmd())
listCmd.AddCommand(newReleasesListActiveVersionsCmd())
listCmd.AddCommand(newReleasesListUpdatedVersionsCmd())

View file

@ -0,0 +1,132 @@
// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"context"
"fmt"
"os"
"slices"
"strconv"
"strings"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/releases"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
var listBinaryVersionsReq = &releases.ListBinaryVersionsReq{}
func newReleasesListBinaryVersionsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "binary-versions <versions>",
Short: "List available binary release variants for the given version labels",
Long: "List available binary release variants for the given version labels",
RunE: runListBinaryVersions,
Args: cobra.MinimumNArgs(1), // Require at least the versions argument
}
// Allows the user to control whether table or JSON is printed to stdout.
cmd.PersistentFlags().StringVar(&rootCfg.format, "format", "table", `Output format: table|json`)
return cmd
}
func runListBinaryVersions(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
listBinaryVersionsReq.VersionsString = args[0]
// Optional second argument: boolean controlling whether JSON is saved to a file.
saveToFile := false
if len(args) > 1 {
v, err := strconv.ParseBool(args[1])
if err != nil {
return fmt.Errorf("second argument must be true or false: %w", err)
}
saveToFile = v
}
// Executes the backend query to retrieve binary version metadata.
res, err := listBinaryVersionsReq.Run(context.TODO())
if err != nil {
return fmt.Errorf("failed to list binary versions: %w", err)
}
// Pre-encode JSON once since we may print it or save it depending on flags/args.
jsonBytes, err := res.ToJSON()
if err != nil {
return fmt.Errorf("failed to encode JSON: %w", err)
}
// If the user requested saving the response, write the JSON output to a local file.
// This occurs regardless of the --format flag.
if saveToFile {
fileName := "binary-versions-output.json"
if err := os.WriteFile(fileName, jsonBytes, 0o644); err != nil {
return fmt.Errorf("failed to write JSON to file: %w", err)
}
fmt.Printf("Saved JSON output to %s\n", fileName)
}
switch rootCfg.format {
case "json":
fmt.Println(string(jsonBytes))
default:
printBinaryTable(res)
}
return nil
}
func printBinaryTable(res *releases.ListBinaryVersionsRes) {
// Pretty-print table writer for terminal output.
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight)
t.AppendHeader(table.Row{"Version", "Status", "Variants Count", "Variants", "OS"})
// Iterate through all versions requested, including ones the backend marked missing.
for _, label := range res.AllVersions {
entry, ok := res.ValidVersions[label]
// Default values for versions not found.
status := "MISSING"
count := "—"
variantsStr := ""
osStr := ""
if ok {
// Version exists — fill in detailed information.
status = strings.ToUpper(entry.Status)
count = fmt.Sprintf("%d", len(entry.Variants))
// Collect variant labels and all OS values.
var variantNames []string
osSet := make(map[string]struct{}) // Deduplicate OS entries across variants.
for _, v := range entry.Variants {
variantNames = append(variantNames, v.Variant)
for _, osName := range v.OS {
osSet[osName] = struct{}{}
}
}
// Join variant names like: "enterprise, oss, fips"
variantsStr = strings.Join(variantNames, ", ")
// Convert deduped OS set into sorted list for stable output.
var osList []string
for osName := range osSet {
osList = append(osList, osName)
}
slices.Sort(osList)
osStr = strings.Join(osList, ", ")
}
// Add row representing this version label.
t.AppendRow(table.Row{label, status, count, variantsStr, osStr})
}
t.Render()
}

View file

@ -0,0 +1,262 @@
// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1
package releases
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"slices"
"strings"
"github.com/PuerkitoBio/goquery"
)
type ListBinaryVersionsReq struct {
VersionsString string
}
type VariantInfo struct {
Variant string `json:"variant"`
OS []string `json:"os,omitempty"`
}
// - ValidVersions: details for versions that exist
// - InvalidVersions: versions requested by the user but not found upstream
// - AllVersions: original input order
type ListBinaryVersionsRes struct {
ValidVersions map[string]struct {
Status string `json:"status"`
Variants []VariantInfo `json:"variants"`
} `json:"valid_versions"`
InvalidVersions []string `json:"invalid_versions"`
AllVersions []string `json:"all_versions"`
}
func NewListBinaryVersionsReq(s string) *ListBinaryVersionsReq {
return &ListBinaryVersionsReq{VersionsString: s}
}
// normalizeLabel transforms user-friendly labels into a canonical form
// that matches HashiCorps release naming scheme.
func normalizeLabel(label string) (normalized, display string) {
display = label
normalized = strings.TrimSuffix(label, "-ce")
normalized = strings.TrimSuffix(normalized, "-lts")
if strings.HasSuffix(label, "-ent") {
normalized = strings.TrimSuffix(normalized, "-ent") + "+ent"
}
return normalized, display
}
const baseURL = "https://releases.hashicorp.com/vault/"
// fetchAvailableVersions scrapes the Vault releases index page and returns
func fetchAvailableVersions(ctx context.Context) ([]string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
// Parse HTML returned by releases.hashicorp.com
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, err
}
// Extract version folder names from links ("/vault/1.18.1/")
var versions []string
doc.Find("body > ul > li > a").Each(func(_ int, sel *goquery.Selection) {
href, exists := sel.Attr("href")
if !exists {
return
}
// Normalize: trim trailing "/", strip parent path
href = strings.TrimSuffix(href, "/")
if idx := strings.LastIndex(href, "/"); idx != -1 {
href = href[idx+1:]
}
if href != "" {
versions = append(versions, href)
}
})
// Sort + deduplicate for stable deterministic results
slices.Sort(versions)
return slices.Compact(versions), nil
}
// fetchAvailableVariants returns all variant names that match a "base version".
func fetchAvailableVariants(available []string, version string) []string {
var result []string
prefix := version
plusPrefix := version + "+" // enterprise or extra flavors
for _, v := range available {
if v == prefix || strings.HasPrefix(v, plusPrefix) {
result = append(result, v)
}
}
return result
}
// fetchAvailableOs inspects each variant directory and extracts the OS
func fetchAvailableOs(ctx context.Context, items []string) (map[string][]string, error) {
result := make(map[string][]string)
for _, full := range items {
result[full] = []string{}
url := baseURL + full + "/"
// Fetch the specific variant page
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status for %s: %d", full, resp.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, err
}
// ZIP filename format starts with: vault_<variant>_<os>_<arch>.zip
prefix := "vault_" + full
doc.Find("body > ul > li > a").Each(func(_ int, sel *goquery.Selection) {
name := sel.Text()
// Ignore unrelated files
if !strings.HasPrefix(name, prefix) || !strings.HasSuffix(name, ".zip") {
return
}
parts := strings.Split(strings.TrimSuffix(name, ".zip"), "_")
if len(parts) < 3 {
return // unexpected format
}
// OS is always the second-to-last component ("linux", "darwin", etc.)
osName := parts[len(parts)-2]
// Avoid duplicates
if !slices.Contains(result[full], osName) {
result[full] = append(result[full], osName)
}
})
}
return result, nil
}
func (r *ListBinaryVersionsReq) Run(ctx context.Context) (*ListBinaryVersionsRes, error) {
if r == nil || r.VersionsString == "" {
return &ListBinaryVersionsRes{}, nil
}
// User-provided labels (raw, with suffixes)
labels := strings.Fields(r.VersionsString)
// Initialize response container
res := &ListBinaryVersionsRes{
ValidVersions: make(map[string]struct {
Status string `json:"status"`
Variants []VariantInfo `json:"variants"`
}),
AllVersions: labels,
}
// Fetch the list of *all* versions from HashiCorp
availableVersions, err := fetchAvailableVersions(ctx)
if err != nil {
return nil, fmt.Errorf("failed fetching versions: %w", err)
}
// Process each requested version label
for _, label := range labels {
normalized, display := normalizeLabel(label)
// If the base version doesn't exist, the entire entry is invalid
if !slices.Contains(availableVersions, normalized) {
res.InvalidVersions = append(res.InvalidVersions, display)
continue
}
// Find all variants associated with this version
variants := fetchAvailableVariants(availableVersions, normalized)
if len(variants) == 0 {
res.InvalidVersions = append(res.InvalidVersions, display)
continue
}
// Fetch OS availability for each variant
osMap, err := fetchAvailableOs(ctx, variants)
if err != nil {
// Non-fatal: mark the version invalid but continue processing others
slog.WarnContext(ctx, "failed fetching OS", "version", normalized, "error", err)
res.InvalidVersions = append(res.InvalidVersions, display)
continue
}
// Build variant list
slices.Sort(variants)
var vlist []VariantInfo
for _, v := range variants {
vlist = append(vlist, VariantInfo{Variant: v, OS: osMap[v]})
}
// Add a valid entry for this version label
res.ValidVersions[display] = struct {
Status string `json:"status"`
Variants []VariantInfo `json:"variants"`
}{
Status: "valid",
Variants: vlist,
}
}
slices.Sort(res.InvalidVersions)
return res, nil
}
// ToJSON returns a pretty-printed JSON representation of the results.
func (r *ListBinaryVersionsRes) ToJSON() ([]byte, error) {
return json.MarshalIndent(r, "", " ")
}
// String provides a concise human-readable summary (counts only).
func (r *ListBinaryVersionsRes) String() string {
total := 0
for _, v := range r.ValidVersions {
total += len(v.Variants)
}
return fmt.Sprintf(
"Listed %d → %d valid (%d variants), %d missing",
len(r.AllVersions), len(r.ValidVersions), total, len(r.InvalidVersions),
)
}

View file

@ -0,0 +1,112 @@
// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1
package releases
import (
"encoding/json"
"fmt"
"strings"
"testing"
)
type ListBinaryVersionsReq struct {
VersionsString string
}
type VariantInfo struct {
Variant string `json:"variant"`
OS []string `json:"os,omitempty"`
}
type ListBinaryVersionsRes struct {
ValidVersions map[string]struct {
Status string `json:"status"`
Variants []VariantInfo `json:"variants"`
} `json:"valid_versions"`
InvalidVersions []string `json:"invalid_versions"`
AllVersions []string `json:"all_versions"`
}
func NewListBinaryVersionsReq(s string) *ListBinaryVersionsReq {
return &ListBinaryVersionsReq{VersionsString: s}
}
// JSON formatting helper; test ensures indentation and content correctness.
func (r *ListBinaryVersionsRes) ToJSON() ([]byte, error) {
return json.MarshalIndent(r, "", " ")
}
// Human-readable summary—unit test checks that counts are computed correctly
// and output string contains expected substrings.
func (r *ListBinaryVersionsRes) String() string {
total := 0
for _, v := range r.ValidVersions {
total += len(v.Variants)
}
return fmt.Sprintf(
"Listed %d → %d valid (%d variants), %d missing",
len(r.AllVersions), len(r.ValidVersions), total, len(r.InvalidVersions),
)
}
func TestNewListBinaryVersionsReq(t *testing.T) {
// Verify that the constructor returns a valid object
req := NewListBinaryVersionsReq("1.2.3 4.5.6-ent")
if req == nil {
t.Fatal("returned nil")
}
// Ensure the input string is stored without modification
if req.VersionsString != "1.2.3 4.5.6-ent" {
t.Errorf("got %q", req.VersionsString)
}
}
// Unit test: JSON output and String() summary formatting
func TestListBinaryVersionsRes_ToJSON_and_String(t *testing.T) {
// Create a fake response object
res := &ListBinaryVersionsRes{
ValidVersions: map[string]struct {
Status string `json:"status"`
Variants []VariantInfo `json:"variants"`
}{
"1.17.0-ent": {
Status: "valid",
Variants: []VariantInfo{
{Variant: "1.17.0+ent", OS: []string{"linux", "darwin"}},
{Variant: "1.17.0+ent.hsm", OS: []string{"linux"}},
},
},
},
InvalidVersions: []string{"9.9.9"},
AllVersions: []string{"1.17.0-ent", "9.9.9"},
}
// Test: ToJSON output
b, err := res.ToJSON()
if err != nil {
t.Fatal(err)
}
// Ensure the JSON contains known variant values
if !strings.Contains(string(b), "1.17.0+ent.hsm") {
t.Error("JSON missing variant")
}
// -------------------------
// Test: String() summary
// -------------------------
// Expected values based on the test fixture:
// total requested: 2
// valid versions: 1
// total variants: 2
// missing: 1
expected := "2 → 1 valid (2 variants), 1 missing"
// Check that the summary contains the correct computed text.
if !strings.Contains(res.String(), expected) {
t.Errorf("String() = %q, want substring %q", res.String(), expected)
}
}