diff --git a/.github/scripts/verify-prod-release-binaries.sh b/.github/scripts/verify-prod-release-binaries.sh new file mode 100755 index 0000000000..c26754b406 --- /dev/null +++ b/.github/scripts/verify-prod-release-binaries.sh @@ -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 diff --git a/tools/pipeline/go.mod b/tools/pipeline/go.mod index 845ae0a3f1..9432f9bd30 100644 --- a/tools/pipeline/go.mod +++ b/tools/pipeline/go.mod @@ -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 ) diff --git a/tools/pipeline/go.sum b/tools/pipeline/go.sum index d2aa4c3cd9..68f127a7c7 100644 --- a/tools/pipeline/go.sum +++ b/tools/pipeline/go.sum @@ -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= diff --git a/tools/pipeline/internal/cmd/releases_list.go b/tools/pipeline/internal/cmd/releases_list.go index 64bb1dcbcf..5846bc42d3 100644 --- a/tools/pipeline/internal/cmd/releases_list.go +++ b/tools/pipeline/internal/cmd/releases_list.go @@ -13,6 +13,7 @@ func newReleasesListCmd() *cobra.Command { } listCmd.AddCommand(newReleasesVersionsBetweenCmd()) + listCmd.AddCommand(newReleasesListBinaryVersionsCmd()) listCmd.AddCommand(newReleasesListActiveVersionsCmd()) listCmd.AddCommand(newReleasesListUpdatedVersionsCmd()) diff --git a/tools/pipeline/internal/cmd/releases_list_binary_versions.go b/tools/pipeline/internal/cmd/releases_list_binary_versions.go new file mode 100644 index 0000000000..0f66c6e0da --- /dev/null +++ b/tools/pipeline/internal/cmd/releases_list_binary_versions.go @@ -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 ", + 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() +} diff --git a/tools/pipeline/internal/pkg/releases/list_binary_versions.go b/tools/pipeline/internal/pkg/releases/list_binary_versions.go new file mode 100644 index 0000000000..53aa2cd26a --- /dev/null +++ b/tools/pipeline/internal/pkg/releases/list_binary_versions.go @@ -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 HashiCorp’s 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___.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), + ) +} diff --git a/tools/pipeline/internal/pkg/releases/list_binary_versions_test.go b/tools/pipeline/internal/pkg/releases/list_binary_versions_test.go new file mode 100644 index 0000000000..43e7f38456 --- /dev/null +++ b/tools/pipeline/internal/pkg/releases/list_binary_versions_test.go @@ -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) + } +}