Repository: crhuber/kelp
Branch: master
Commit: abde55811893
Files: 24
Total size: 164.1 KB
Directory structure:
gitextract_f2uvh5_n/
├── .github/
│ └── workflows/
│ ├── build.yaml
│ ├── codeql.yml
│ └── release.yaml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .tool-versions
├── README.md
├── TODO.txt
├── Taskfile.yaml
├── go.mod
├── go.sum
├── main.go
├── pkg/
│ ├── config/
│ │ └── config.go
│ ├── install/
│ │ ├── install.go
│ │ └── install_test.go
│ ├── logging/
│ │ └── logging.go
│ ├── rm/
│ │ └── rm.go
│ ├── types/
│ │ ├── github.go
│ │ ├── github_test.go
│ │ └── os.go
│ └── utils/
│ ├── utils.go
│ └── utils_test.go
└── testdata/
└── helm-latest.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yaml
================================================
name: Build and Test
permissions:
contents: read
on:
pull_request:
branches: [ master ]
push:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Cache Go modules
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Verify dependencies
run: go mod verify
- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Lint, Test, and Build
run: |
task ci
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: latest
args: --timeout=5m
================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '44 20 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: go
build-mode: autobuild
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
================================================
FILE: .github/workflows/release.yaml
================================================
name: goreleaser
on:
push:
tags:
- '*'
jobs:
goreleaser:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
kelp
build/
dist/
# llm files
.cursor
.claude
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
go: "1.23"
linters:
enable:
- gocritic
- gosec
- misspell
- revive
disable:
- errcheck
settings:
gosec:
excludes:
- G404
revive:
rules:
- name: exported
disabled: false
- name: unreachable-code
disabled: false
- name: unused-parameter
disabled: false
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gocritic
- gosec
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: .goreleaser.yaml
================================================
# GoReleaser configuration for kelp
version: 2
project_name: kelp
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
main: .
binary: kelp
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
gcflags:
- all=-l
- ./dontoptimizeme=-N
archives:
- formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- README.md
- LICENSE*
checksum:
name_template: "checksums.txt"
changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"
- "^chore:"
- merge conflict
- Merge pull request
- Merge remote-tracking branch
- Merge branch
groups:
- title: "🚀 New Features"
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "🐛 Bug Fixes"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "📚 Documentation"
regexp: "^.*docs[(\\w)]*:+.*$"
order: 2
- title: "🔧 Improvements"
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: "Other Changes"
order: 999
release:
github:
owner: crhuber
name: kelp
draft: false
prerelease: auto
mode: replace
header: |
## kelp {{ .Tag }}
This release includes kelp.
### Installation
Download the appropriate binary for your platform and follow the installation instructions in the README.
================================================
FILE: .tool-versions
================================================
golang 1.24.0
================================================
FILE: README.md
================================================
# KELP
## What is it
A simple replacement for homebrew for installing binary packages on MacOS & Linux written in Go.
## How Does it Work?
1. It downloads a Github releases package matching your operating system and architecture to `~/.kelp/cache`
2. It extracts any binary files to `~/.kelp/bin`
3. It unquarantines the binary (mac)
Example:
```
kelp update --install crumb
🌐 Getting releases for crhuber/crumb:latest...
Latest release v0.0.24. Kelp configured release v0.0.21.
===> Installing crhuber/crumb:v0.0.24...
🌐 Getting releases by tag v0.0.24...
🍏 Finding assets to download...
===> Downloading https://api.github.com/repos/crhuber/crumb/releases/assets/372978000...
Downloading 100%
📂 Extracting /Users/Craig/.kelp/cache/crumb_0.0.24_darwin_arm64.tar.gz
🧐 Checking for binary files in extract...
💾 Copying crumb to kelp bin...
✅ Installed crumb !
🛃 Unquarantining /Users/Craig/.kelp/bin/crumb..
```
## Why?
I built Kelp to scratch my own itch:
* No waiting for a formula to become available on homebrew
* Keep all your computers up to date with a single installation manifest
* No homebrew auto update
* No bloat, no magic
Couldn't this just be a bash script? Probably.
## How To Install
Go to the [releases](https://github.com/crhuber/kelp/releases) page. Download the latest release
Add kelp binary path to your PATH
```
export PATH=~/.kelp/bin/:$PATH
```
## Quick Setup
1. Initialize Kelp
```
kelp init
```
2. Add a new package
```
kelp add junegunn/fzf
```
To use a specific version use the `-r` flag. Where `-r` is the github release version
```
kelp add junegunn/fzf -r 1.0.0
```
3. Install
```
kelp install fzf
```
or
```
kelp add junegunn/fzf --install
```
### Updating a Package
Update
```
kelp update fzf
kelp install fzf
```
Update and install
```
kelp update fzf --install
```
Update to a specific version
```
kelp set fzf -r v0.72.0
kelp install fzf
```
## Configuration
```
GLOBAL OPTIONS:
--config string, -c string path to kelp config file (default: "/Users/Craig/.kelp/kelp.json") [$KELP_CONFIG]
--verbose verbose output [$KELP_VERBOSE]
```
## Troubleshooting
### What if the package I want is not on github releases?
Just kelp add the link to the binary, ie:
```
kelp add hashicorp/terraform -r https://releases.hashicorp.com/terraform/0.11.13/terraform_0.11.13_darwin_amd64.zip
```
Also supported are packages like `helm` that provide external non-github download links in the release description.
### Increase logging level
Use `--verbose` or set `KELP_VERBOSE=1` to get more information, expecially during the installation process.
### Why wasnt my package installed ?
Kelp looks for binaries made for MacOS or Linux. If it cannot file a suitable binary for your machine architecture and operating system it will skip downloading it or wont extract it.
Use inspect to open the cache and bin directories for your package
```
kelp inspect
```
To see what binaries exist use:
```
kelp doctor
```
If your binary has a different filename than the name of the Github project, kelp doctor may not find it. To give it a hint you can add the name of the binary to the kelp config
```
kelp set jira-cli -b "jira"`
```
To see whats in your config use:
```
kelp ls
```
### Does it work for Linux?
Yes!
### What if I'm rate limited by Github Api?
Set a github token environment variable
```
export GITHUB_TOKEN="XYZ"
```
## Contributing
If you find bugs, please open an issue first. If you have feature requests, I may not honor it because this project is being built mostly to suit my personal workflow and preferences.
================================================
FILE: TODO.txt
================================================
- when file already exists in cache do a hash before skipping it
- add post install hooks
================================================
FILE: Taskfile.yaml
================================================
version: '3'
vars:
BINARY_NAME: kelp
BUILD_DIR: ./build
COVERAGE_DIR: ./coverage
tasks:
default:
desc: Show available tasks
cmds:
- task --list
clean:
desc: Clean build artifacts and coverage reports
cmds:
- rm -rf {{.BUILD_DIR}}
- rm -rf {{.COVERAGE_DIR}}
- rm -f {{.BINARY_NAME}}
- rm -f coverage.out coverage.html
deps:
desc: Download and verify dependencies
cmds:
- go mod download
- go mod verify
build:
desc: Build the application
deps: [clean]
cmds:
- mkdir -p {{.BUILD_DIR}}
- go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}} .
test:
desc: Run all tests
cmds:
- echo "Running unit tests..."
- go test -v ./...
test-unit:
desc: Run unit tests only
cmds:
- echo "Running unit tests..."
- go test -v ./... -run "^Test"
test-integration:
desc: Run integration tests only
cmds:
- echo "Running integration tests..."
- go test -v ./... -run "Integration"
test-coverage:
desc: Run tests with coverage report
cmds:
- mkdir -p {{.COVERAGE_DIR}}
- go test -v -cover -coverprofile={{.COVERAGE_DIR}}/coverage.out ./...
- go tool cover -html={{.COVERAGE_DIR}}/coverage.out -o {{.COVERAGE_DIR}}/coverage.html
- 'echo "Coverage report generated: {{.COVERAGE_DIR}}/coverage.html"'
test-coverage-text:
desc: Run tests with coverage report (text output)
cmds:
- go test -v -cover -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out
- rm -f coverage.out
lint:
desc: Run linter
cmds:
- echo "Running linter..."
- go vet ./...
- go fmt ./...
- golangci-lint run --timeout=5m
lint-check:
desc: Check code formatting and linting
cmds:
- echo "Checking code formatting..."
- test -z "$(go fmt ./...)"
- go vet ./...
security:
desc: Run security scanner (requires gosec)
cmds:
- echo "Running security scanner..."
- gosec ./...
preconditions:
- sh: command -v gosec
msg: "gosec is not installed. Run: go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest"
ci:
desc: Run CI pipeline (lint, test, build)
cmds:
- task: lint-check
- task: test-coverage-text
- task: build
ci-full:
desc: Run full CI pipeline with benchmarks
cmds:
- task: deps
- task: lint-check
- task: test-coverage
- task: build
setup-dev:
desc: Setup development environment
cmds:
- echo "Setting up development environment..."
- go mod download
- go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
- go install github.com/go-task/task/v3/cmd/task@latest
- echo "Development environment setup complete!"
test-race:
desc: Run tests with race detection
cmds:
- echo "Running tests with race detection..."
- go test -v -race ./...
release:
desc: Build release binaries for multiple platforms
deps: [clean, test-coverage-text, lint-check]
cmds:
- mkdir -p {{.BUILD_DIR}}/release
- echo "Building release binaries..."
- GOOS=darwin GOARCH=amd64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-darwin-amd64 .
- GOOS=darwin GOARCH=arm64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-darwin-arm64 .
- GOOS=linux GOARCH=amd64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-linux-amd64 .
- GOOS=linux GOARCH=arm64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-linux-arm64 .
- echo "Release binaries built in {{.BUILD_DIR}}/release/"
================================================
FILE: go.mod
================================================
module crhuber/kelp
go 1.23.0
toolchain go1.23.8
require (
github.com/gabriel-vasile/mimetype v1.4.12
github.com/h2non/gock v1.2.0
github.com/mholt/archives v0.1.4
github.com/schollz/progressbar/v3 v3.19.0
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.1
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/nwaples/rardecode/v2 v2.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mholt/archives v0.1.4 h1:sU+/lLNgafUontWFv3AVwO8VUWye3rrtN6hgC2dU11c=
github.com/mholt/archives v0.1.4/go.mod h1:I2ia+SQTtQHej9w1GZM/mz7qfdgQv+BHr3hEKqDcGuk=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew=
github.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/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-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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
================================================
FILE: main.go
================================================
package main
import (
"context"
"crhuber/kelp/pkg/config"
"crhuber/kelp/pkg/install"
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"crhuber/kelp/pkg/utils"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/urfave/cli/v3"
)
var (
version = "dev"
)
func main() {
// default config
var home, _ = os.UserHomeDir()
var KelpConf = filepath.Join(home, "/.kelp/kelp.json")
if types.GetCapabilities() == nil {
fmt.Println("Sorry, your OS is not yet supported.")
os.Exit(1)
}
app := &cli.Command{
Name: "kelp",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Value: KelpConf,
Usage: "path to kelp config file",
Sources: cli.EnvVars("KELP_CONFIG"),
},
&cli.BoolFlag{
Name: "verbose",
Value: false,
Usage: "verbose output",
Sources: cli.EnvVars("KELP_VERBOSE"),
Action: func(_ context.Context, _ *cli.Command, val bool) error {
logging.SetLogVerbose(val)
return nil
},
},
},
Commands: []*cli.Command{
{
Name: "add",
Usage: "add a new package to config",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "release",
Aliases: []string{"r"},
Value: "latest",
Usage: "release for package",
},
&cli.BoolFlag{
Name: "install",
Aliases: []string{"i"},
Value: false,
Usage: "also install package",
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
ownerRepo := strings.Split(project, "/")
if len(ownerRepo) < 2 {
return fmt.Errorf("use owner/repo format")
}
// resolve release version
releaseFlag := cmd.String("release")
var actualRelease string
if releaseFlag == "latest" {
// Get the actual latest release version from GitHub
latestRelease, err := utils.GetGithubRelease(ownerRepo[0], ownerRepo[1], "latest")
if err != nil {
return fmt.Errorf("failed to get latest release for %s/%s: %s", ownerRepo[0], ownerRepo[1], err)
}
actualRelease = latestRelease.TagName
} else {
actualRelease = releaseFlag
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
err = kc.AddPackage(ownerRepo[0], ownerRepo[1], actualRelease)
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("%s", err)
}
// auto install
if cmd.Bool("install") {
err = install.Install(ownerRepo[0], ownerRepo[1], actualRelease)
if err != nil {
return err
}
}
return nil
},
},
{
Name: "browse",
Usage: "browse to project github page",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
p, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
config.Browse(p.Owner, p.Repo)
return nil
},
},
{
Name: "doctor",
Usage: "checks if packages are installed properly",
Action: func(_ context.Context, cmd *cli.Command) error {
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kc.Doctor()
return nil
},
},
{
Name: "get",
Usage: "get package details",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
p, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
fmt.Printf("[%s/%s]\n", p.Owner, p.Repo)
fmt.Printf("Release: %s\n", p.Release)
fmt.Printf("Description: %s\n", p.Description)
fmt.Printf("Url: https://github.com/%s/%s\n", p.Owner, p.Repo)
fmt.Printf("Binary: %s\n", p.Binary)
fmt.Printf("Updated At: %s\n", p.UpdatedAt)
return nil
},
},
{
Name: "init",
Usage: "initialize kelp",
Action: func(_ context.Context, cmd *cli.Command) error {
err := config.Initialize(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
return nil
},
},
{
Name: "inspect",
Usage: "inspect kelp bin directory",
Action: func(_ context.Context, _ *cli.Command) error {
config.Inspect()
return nil
},
},
{
Name: "install",
Usage: "install kelp package",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kp, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
err = install.Install(kp.Owner, kp.Repo, kp.Release)
if err != nil {
return err
}
return nil
},
},
{
Name: "list",
Aliases: []string{"ls"},
Usage: "list kelp packages",
Action: func(_ context.Context, cmd *cli.Command) error {
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kc.List()
return nil
},
},
{
Name: "remove",
Aliases: []string{"rm"},
Usage: "remove a package from config and disk",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kp, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
// remove from config
err = kc.RemovePackage(kp.Repo)
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("error saving: %s", err)
}
return nil
},
},
{
Name: "set",
Usage: "set package configuration in config",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "release",
Aliases: []string{"r"},
Value: "latest",
Usage: "release for package",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Value: "",
Usage: "description of package",
},
&cli.StringFlag{
Name: "binary",
Aliases: []string{"b"},
Value: "",
Usage: "alias of binary",
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
err = kc.SetPackage(project, cmd.String("release"), cmd.String("description"), cmd.String("binary"))
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("%s", err)
}
return nil
},
},
{
Name: "update",
Usage: "update kelp package in config",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "install",
Aliases: []string{"i"},
Value: false,
Usage: "also install package",
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kp, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
// handle http packages
if strings.HasPrefix(kp.Release, "http") {
return errors.New("update functionality not supported for http packages")
}
ghr, err := utils.GetGithubRelease(kp.Owner, kp.Repo, "latest")
if err != nil {
return fmt.Errorf("%s", err)
}
if ghr.TagName == kp.Release {
logging.LogInfo("Latest release %s already matches release %s in kelp config", ghr.TagName, kp.Release)
return nil
}
logging.LogInfo("Latest release %s. Kelp configured release %s. Update config [y/n] ? : ", ghr.TagName, kp.Release)
var confirmation string
confirmation = strings.TrimSpace(confirmation)
confirmation = strings.ToLower(confirmation)
// Taking input from user
fmt.Scanln(&confirmation)
if confirmationUpper := strings.ToUpper(confirmation); confirmationUpper == "Y" || confirmationUpper == "YES" {
err = kc.SetPackage(kp.Repo, ghr.TagName, "", "")
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("%s", err)
}
}
// auto install
if cmd.Bool("install") {
err = install.Install(kp.Owner, kp.Repo, ghr.TagName)
if err != nil {
return err
}
}
return nil
},
},
},
}
if err := app.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}
================================================
FILE: pkg/config/config.go
================================================
package config
import (
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"crhuber/kelp/pkg/utils"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"text/tabwriter"
"time"
)
var home, _ = os.UserHomeDir()
var KelpDir = filepath.Join(home, "/.kelp/")
var KelpBin = filepath.Join(home, "/.kelp/bin/")
var KelpCache = filepath.Join(home, "/.kelp/cache/")
type KelpConfig struct {
Path string `json:"-"`
Packages []KelpPackage
}
type KelpPackage struct {
Owner string `json:"Owner"`
Repo string `json:"Repo"`
Release string `json:"Release"`
UpdatedAt time.Time `json:"UpdatedAt"`
Description string `json:"Description"`
Binary string `json:"Binary"`
}
func (kc *KelpConfig) Pop(index int) []KelpPackage {
return append(kc.Packages[:index], kc.Packages[index+1:]...)
}
func (kc *KelpConfig) GetPackage(repo string) (*KelpPackage, error) {
parts := strings.Split(repo, "/")
// Check if there is an owner part since some projects have the same repo name
// like cli
if len(parts) > 1 {
// If there is an owner, get the more specific project first
for _, kp := range kc.Packages {
if kp.Owner == parts[0] && kp.Repo == parts[1] {
return &kp, nil
}
}
} else {
for _, kp := range kc.Packages {
if kp.Repo == repo {
return &kp, nil
}
}
}
return nil, errors.New("package not found in config, try adding it first")
}
func Load(path string) (*KelpConfig, error) {
bs, _ := os.ReadFile(path)
kc := KelpConfig{}
err := json.Unmarshal(bs, &kc.Packages)
if err != nil {
return nil, err
}
kc.Path = path
return &kc, nil
}
func (kc *KelpConfig) Save() error {
bs, _ := json.MarshalIndent(kc.Packages, "", " ")
err := os.WriteFile(kc.Path, bs, 0600)
if err != nil {
return err
}
logging.LogInfo("Config saved.")
return nil
}
func (kc *KelpConfig) RemovePackage(repo string) error {
for i, kp := range kc.Packages {
if kp.Repo == repo {
kc.Packages = kc.Pop(i)
logging.LogInfo("Package %s removed\n", repo)
return nil
}
}
return errors.New("package not found in config")
}
func (kc *KelpConfig) AddPackage(owner, repo, release string) error {
for _, p := range kc.Packages {
if p.Owner == owner && p.Repo == repo {
return fmt.Errorf("package already exists in config")
}
}
// append a new item
kp := KelpPackage{
Owner: owner,
Repo: repo,
Release: release,
UpdatedAt: time.Now(),
}
kc.Packages = append(kc.Packages, kp)
logging.LogDebug("Config added for %s/%s", owner, repo)
return nil
}
func (kc *KelpConfig) UpdatePackage(repo string) (string, error) {
for _, p := range kc.Packages {
if p.Repo == repo {
ghr, err := utils.GetGithubRelease(p.Owner, p.Repo, "latest")
if err != nil {
return "", err
}
return ghr.TagName, nil
}
}
return "", errors.New("package not found in config")
}
func (kc *KelpConfig) SetPackage(repo, release, description, binary string) error {
for i, p := range kc.Packages {
if p.Repo == repo {
if release != "" {
kc.Packages[i].Release = release
kc.Packages[i].UpdatedAt = time.Now()
}
if description != "" {
kc.Packages[i].Description = description
}
if binary != "" {
kc.Packages[i].Binary = binary
}
logging.LogDebug("Config set for %s", repo)
return nil
}
}
return nil
}
func (kc *KelpConfig) List() {
w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
// sort by date
sort.Slice(kc.Packages, func(i, j int) bool {
return kc.Packages[i].UpdatedAt.Before(kc.Packages[j].UpdatedAt)
})
for _, pkg := range kc.Packages {
// Format the timestamp in a more human-friendly way
humanFriendlyTimestamp := pkg.UpdatedAt.Format("Jan 2 2006")
if humanFriendlyTimestamp == "Jan 1 0001" {
humanFriendlyTimestamp = ""
}
release := ""
if strings.HasPrefix(pkg.Release, "http") {
// Define the regex pattern to extract version numbers
pattern := `[/v-]([\d.]+)`
// Compile the regex pattern
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(pkg.Release)
if len(match) > 1 {
release = fmt.Sprintf("%s (https)", match[1])
} else {
release = "unknown (https)"
}
} else {
release = pkg.Release
}
fmt.Fprintf(w, "%s/%s\t%s\t%s\n", pkg.Owner, pkg.Repo, release, humanFriendlyTimestamp)
}
w.Flush()
}
func Initialize(path string) error {
if !utils.DirExists(KelpDir) {
logging.LogDebug("Creating Kelp dir...")
err := os.Mkdir(KelpDir, 0777)
if err != nil {
return err
}
}
if !utils.DirExists(KelpCache) {
logging.LogDebug("Creating Kelp cache...")
err := os.Mkdir(KelpCache, 0777)
if err != nil {
return err
}
}
if !utils.DirExists(KelpBin) {
logging.LogDebug("Creating Kelp bin...")
err := os.Mkdir(KelpBin, 0777)
if err != nil {
return err
}
}
// create empty config
kp := KelpPackage{
Owner: "crhuber",
Repo: "kelp",
Release: "latest",
UpdatedAt: time.Now(),
Description: "Simple homebrew alternative",
}
kc := KelpConfig{
Path: path,
Packages: []KelpPackage{kp},
}
if !utils.FileExists(path) {
logging.LogDebug("Creating Kelp config file...")
err := kc.Save()
if err != nil {
return err
}
} else {
logging.LogDebug("Skipping Kelp config file creation since one alredy exists...")
}
logging.LogInfo("🌱 Kelp Initialized!")
logging.LogInfo("🗒 Add Kelp to your path by running: \nexport PATH=%s:$PATH >> ~/.bash_profile\n", KelpBin)
return nil
}
func Inspect() {
var err error
switch types.GetOS() {
case types.Darwin:
err = exec.Command("open", KelpDir).Start()
case types.Linux:
err = exec.Command("xdg-open", KelpDir).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func Browse(owner, repo string) {
var err error
url := fmt.Sprintf("https://github.com/%s/%s", owner, repo)
logging.LogDebug("Opening %s\n", url)
switch types.GetOS() {
case types.Darwin:
err = exec.Command("open", url).Start()
case types.Linux:
err = exec.Command("xdg-open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func (kc *KelpConfig) Doctor() {
w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
for _, p := range kc.Packages {
// check alias first
var binary string
if p.Binary != "" {
binary = p.Binary
} else {
binary = p.Repo
}
status := ""
path, err := commandExists(binary)
if err != nil {
status = "❌ Binary not found"
} else {
if strings.HasPrefix(path, KelpBin) {
status = "✅ Installed"
} else {
status = "⛔️ Installed outside kelp"
}
}
logging.LogInfo("%s\t%s\n", binary, status)
}
w.Flush()
}
func commandExists(cmd string) (string, error) {
return exec.LookPath(cmd)
}
================================================
FILE: pkg/install/install.go
================================================
package install
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"crhuber/kelp/pkg/config"
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"crhuber/kelp/pkg/utils"
"github.com/gabriel-vasile/mimetype"
"github.com/mholt/archives"
"github.com/schollz/progressbar/v3"
)
func Install(owner, repo, release string) error {
// handle http packages
tempdir, _ := os.MkdirTemp("", "kelp")
defer os.RemoveAll(tempdir)
var downloadPath string
if strings.HasPrefix(release, "http") {
urlsplit := strings.SplitAfter(release, "/")
filename := urlsplit[len(urlsplit)-1]
downloadPath = filepath.Join(config.KelpCache, filename)
err := downloadFile(downloadPath, release)
if err != nil {
return err
}
} else {
asset, err := downloadGithubRelease(owner, repo, release)
if err != nil {
return err
}
downloadPath = filepath.Join(config.KelpCache, asset.Name)
}
err := extractPackage(downloadPath, tempdir)
if err != nil {
return err
}
destinations := installBinary(tempdir)
if types.IsDarwin() {
for _, d := range destinations {
unquarantineFile(d)
}
}
return nil
}
func unquarantineFile(filepath string) error {
logging.LogInfo("🛃 Unquarantining %s...\n", filepath)
cmd := exec.Command("xattr", "-d", "com.apple.quarantine", filepath)
return cmd.Run()
}
// downloadFile downloads files
func downloadFile(filepath string, url string) error {
logging.LogInfo("===> Downloading %s...\n", url)
logging.LogDebug("To: %s...\n", filepath)
// Get the data
req, _ := http.NewRequest("GET", url, nil)
// set headers for github auth
if ghToken := os.Getenv("GITHUB_TOKEN"); ghToken != "" {
logging.LogDebug("Using Github token in http request")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ghToken))
}
req.Header.Set("Accept", "application/octet-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("\ninvalid HTTP status: %v", resp.StatusCode)
}
defer resp.Body.Close()
// Create the file
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
bar := progressbar.DefaultBytes(
resp.ContentLength,
"Downloading",
)
_, err = io.Copy(io.MultiWriter(out, bar), resp.Body)
return err
}
func extractPackage(downloadPath, tempDir string) error {
logging.LogInfo("📂 Extracting %s\n", downloadPath)
// Handle dmg files
if strings.HasSuffix(downloadPath, ".dmg") {
return errors.New("kelp does not support dmg files")
}
// Open the file
file, err := os.Open(downloadPath)
if err != nil {
return fmt.Errorf("could not open file: %w", err)
}
defer file.Close()
// Try to identify archive format
ctx := context.Background()
format, stream, err := archives.Identify(ctx, downloadPath, file)
if err != nil {
// Not a recognized archive — treat as raw binary
logging.LogDebug("File is not a recognized archive format. Treating as raw binary.")
cleanName := cleanBinaryName(filepath.Base(downloadPath))
destPath := filepath.Join(tempDir, cleanName)
if copyErr := utils.CopyFile(downloadPath, destPath); copyErr != nil {
return fmt.Errorf("could not copy binary to temp dir: %w", copyErr)
}
os.Chmod(destPath, 0o755)
return nil
}
// Check if the format supports extraction
extractor, ok := format.(archives.Extractor)
if !ok {
return fmt.Errorf("archive format does not support extraction")
}
// Extract all files to destination directory
err = extractor.Extract(ctx, stream, func(_ context.Context, f archives.FileInfo) error {
return extractFile(f, tempDir)
})
if err != nil {
return fmt.Errorf("extraction failed: %w", err)
}
return nil
}
// cleanBinaryName strips OS/arch suffixes from binary filenames.
// For example, "direnv.darwin-arm64" becomes "direnv".
func cleanBinaryName(name string) string {
osNames := []string{"darwin", "linux", "macos", "windows"}
archNames := []string{"arm64", "aarch64", "amd64", "x86_64", "x64"}
lower := strings.ToLower(name)
for _, osName := range osNames {
for _, arch := range archNames {
for _, sep := range []string{"-", "_"} {
// os-arch: direnv.darwin-arm64
suffix := "." + osName + sep + arch
if strings.HasSuffix(lower, suffix) {
return name[:len(name)-len(suffix)]
}
// arch-os: direnv.arm64-darwin
suffix = "." + arch + sep + osName
if strings.HasSuffix(lower, suffix) {
return name[:len(name)-len(suffix)]
}
}
}
}
return name
}
// Helper function to extract a single file
func extractFile(f archives.FileInfo, destDir string) error {
extractPath := filepath.Join(destDir, f.NameInArchive)
if f.IsDir() {
return os.MkdirAll(extractPath, f.Mode())
}
if err := os.MkdirAll(filepath.Dir(extractPath), 0o755); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
outFile, err := os.OpenFile(extractPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, rc)
return err
}
func installBinary(tempDir string) []string {
logging.LogInfo("🧐 Checking for binary files in extract...")
files, err := utils.FilePathWalkDir(tempDir)
if err != nil {
log.Panic("Could not walk directory")
}
destinations := []string{}
osCap := types.GetCapabilities()
var foundLibs []string
for _, file := range files {
mime, _ := mimetype.DetectFile(string(file))
// only install binary files
switch mime.String() {
case osCap.ExecutableMime:
destinations = append(destinations, copyToKelpBin(file))
case osCap.SharedLibrary:
splits := strings.SplitAfter(file, "/")
fileName := splits[len(splits)-1]
logging.LogDebug("Shared/Static Library file %s found in extract.\n", fileName)
foundLibs = append(foundLibs, file)
default:
logging.LogDebug("Skipping non executable file: %v - %v\n", file, mime.String())
}
}
if len(destinations) == 0 { // if no binary was found in extract, then filter shared libararies
if len(foundLibs) == 1 {
destinations = append(destinations, copyToKelpBin(foundLibs[0]))
} else {
var filteredLibs []string
for _, currentLib := range foundLibs {
if !strings.HasPrefix(currentLib, "lib") && !strings.HasSuffix(currentLib, "dynlib") {
filteredLibs = append(filteredLibs, currentLib)
} else {
mime, _ := mimetype.DetectFile(string(currentLib))
logging.LogDebug("Skipping non executable file: %v - %v\n", currentLib, mime.String())
}
}
if len(filteredLibs) == 1 {
destinations = append(destinations, copyToKelpBin(filteredLibs[0]))
} else {
for _, currentUnrecognizedLib := range filteredLibs {
mime, _ := mimetype.DetectFile(string(currentUnrecognizedLib))
logging.LogDebug("Skipping non executable file: %v - %v\n", currentUnrecognizedLib, mime.String())
}
}
}
}
return destinations
}
func copyToKelpBin(file string) string {
splits := strings.SplitAfter(file, "/")
fileName := splits[len(splits)-1]
logging.LogDebug("Binary file %s found in extract.\n", fileName)
destination := filepath.Join(config.KelpBin, fileName)
logging.LogInfo("💾 Copying %v to kelp bin...\n", fileName)
utils.CopyFile(file, destination)
logging.LogInfo("✅ Installed %v !\n", fileName)
return destination
}
func downloadGithubRelease(owner, repo, release string) (*types.Asset, error) {
logging.LogInfo("===> Installing %s/%s:%s...\n", owner, repo, release)
ghr, err := utils.GetGithubRelease(owner, repo, release)
if err != nil {
return nil, err
}
logging.LogInfo("🍏 Finding assets to download...")
downloadableAsset, err := ghr.FindBestAsset(types.GetCapabilities())
if err != nil {
return nil, err
}
downloadPath := filepath.Join(config.KelpCache, downloadableAsset.Name)
if utils.FileExists(downloadPath) {
logging.LogDebug("File %v already exists in cache, skipping download.\n", downloadableAsset.Name)
} else {
err := downloadFile(downloadPath, downloadableAsset.URL)
if err != nil {
return nil, err
}
}
return downloadableAsset, nil
}
================================================
FILE: pkg/install/install_test.go
================================================
package install
import "testing"
func TestCleanBinaryName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"direnv.darwin-arm64", "direnv"},
{"direnv.darwin-amd64", "direnv"},
{"direnv.linux-arm64", "direnv"},
{"direnv.linux-amd64", "direnv"},
{"tool.darwin_arm64", "tool"},
{"tool.linux_amd64", "tool"},
{"tool.arm64-darwin", "tool"},
{"tool.amd64-linux", "tool"},
{"tool.macos-arm64", "tool"},
{"tool.windows-x86_64", "tool"},
{"tool.linux-aarch64", "tool"},
{"tool.darwin-x64", "tool"},
// No matching suffix — returned as-is
{"mybinary", "mybinary"},
{"archive.tar.gz", "archive.tar.gz"},
{"tool.v1.2.3", "tool.v1.2.3"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := cleanBinaryName(tt.input)
if got != tt.want {
t.Errorf("cleanBinaryName(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
================================================
FILE: pkg/logging/logging.go
================================================
package logging
import "log"
var logVerbose = false
func SetLogVerbose(verbose bool) {
logVerbose = verbose
}
func LogDebug(message string, args ...any) {
if logVerbose {
log.Printf(message, args...)
}
}
func LogInfo(message string, args ...any) {
log.Printf(message, args...)
}
func init() {
// set log output to the simplest as possible, without anything but the message
log.SetFlags(0)
}
================================================
FILE: pkg/rm/rm.go
================================================
package rm
import (
"crhuber/kelp/pkg/config"
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/utils"
"os"
"path/filepath"
)
func RemoveBinary(binary string) error {
binaryPath := filepath.Join(config.KelpBin, binary)
if utils.FileExists(binaryPath) {
logging.LogInfo("Removing binary %s...", binary)
return os.Remove(binaryPath)
}
return nil
}
================================================
FILE: pkg/types/github.go
================================================
package types
import (
"crhuber/kelp/pkg/logging"
"errors"
"regexp"
"sort"
"strings"
"time"
)
// Asset represents a downloadable asset from a Github release
type Asset struct {
URL string `json:"url"`
ID int `json:"id"`
Name string `json:"name"`
Label string `json:"label"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadURL string `json:"browser_download_url"`
}
// GithubRelease represents a Github release
type GithubRelease struct {
URL string `json:"url"`
AssetsURL string `json:"assets_url"`
UploadURL string `json:"upload_url"`
HTMLURL string `json:"html_url"`
ID int `json:"id"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []Asset `json:"assets"`
Body string `json:"body"`
}
// methods
func (a *Asset) isDownloadableExtension() bool {
downLoadableExtension := []string{".zip", ".tar", ".gz", ".xz", ".dmg", ".pkg", ".tgz", ".bz2"}
for _, word := range downLoadableExtension {
result := strings.HasSuffix(a.BrowserDownloadURL, word)
if result {
return result
}
}
return false
}
func (a *Asset) isChecksumFile() bool {
checksumExtension := []string{".asc", ".sha256.asc", ".sha512.asc", ".sha256sum.asc", ".sha512sum.asc", ".sha1.asc", ".md5.asc"}
for _, word := range checksumExtension {
if strings.HasSuffix(a.BrowserDownloadURL, word) {
return true
}
}
return false
}
func (a *Asset) hasNoExtension() bool {
bdu := strings.SplitAfter(a.BrowserDownloadURL, "/")
filename := bdu[len(bdu)-1]
return !strings.Contains(filename, ".")
}
// IsMacAsset checks if the download url contains "mac", "macos", "darwin", "osx", "apple" and returns true if so
func (a *Asset) isMacAsset() bool {
macIdentifiers := []string{"mac", "macos", "darwin", "osx", "apple"}
for _, word := range macIdentifiers {
result := strings.Contains(strings.ToLower(a.BrowserDownloadURL), word)
if result {
return result
}
}
return false
}
func (a *Asset) isLinuxAsset() bool {
macIdentifiers := []string{"linux"}
for _, word := range macIdentifiers {
result := strings.Contains(strings.ToLower(a.BrowserDownloadURL), word)
if result {
return result
}
}
return false
}
func (a *Asset) isSameOS(capabilities *Capabilities) bool {
switch capabilities.OS {
case Darwin:
return a.isMacAsset()
case Linux:
return a.isLinuxAsset()
}
return false
}
func (a *Asset) isSameArchitecture(capabilities *Capabilities) bool {
lowerURL := strings.ToLower(a.BrowserDownloadURL)
// First check if the URL contains the exact arch name
if strings.Contains(lowerURL, strings.ToLower(capabilities.Arch)) {
return true
}
// Then handle architecture aliases
switch capabilities.Arch {
case "amd64":
return strings.Contains(lowerURL, "x86_64")
case "arm64":
return strings.Contains(lowerURL, "arm64") || strings.Contains(lowerURL, "aarch64")
default:
return false
}
}
const (
MIN_ASSET_SCORE = 6 // minimum score for an asset to be considered suitable for download
)
func (a *Asset) EvaluateSuitability(capabilities *Capabilities) int {
assetScore := 0
if a.isSameOS(capabilities) {
assetScore += 4
}
if a.isSameArchitecture(capabilities) {
assetScore += 3
}
if a.isDownloadableExtension() {
assetScore += 2
}
if a.hasNoExtension() {
assetScore += 1
}
if a.isChecksumFile() {
assetScore -= 10
}
return assetScore
}
func (a *Asset) RealFilename() string {
if a.Name != "" {
return a.Name
}
url := a.URL
if url == "" {
url = a.BrowserDownloadURL
}
filename := strings.Split(url, "/")
return filename[len(filename)-1]
}
// A data structure to hold key/value pairs
type Pair struct {
Key int
Value int
}
// A slice of pairs that implements sort.Interface to sort by values
type PairList []Pair
func (p PairList) Len() int { return len(p) }
func (p PairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p PairList) Less(i, j int) bool { return p[i].Value < p[j].Value }
func (ghr *GithubRelease) FindBestAsset(capabilities *Capabilities) (*Asset, error) {
var bestAsset Asset
assetScores := map[int]int{}
for index, asset := range ghr.Assets {
if assetScore := asset.EvaluateSuitability(capabilities); assetScore >= MIN_ASSET_SCORE {
logging.LogDebug("Found suitable candidate %v for download. Score: %v", asset.RealFilename(), assetScore)
assetScores[index] = assetScore
}
}
if len(assetScores) == 0 {
// inspect the release body for links to downloadable assets
links := ghr.inspectLinksInReleaseBody()
// create a list of assets from the links and evaluate them
assetsFromBodyScores := map[int]int{}
assetLinks := make([]Asset, len(links))
for index, link := range links {
filename := strings.Split(link, "/")
realFilename := filename[len(filename)-1]
a := Asset{
BrowserDownloadURL: link,
URL: link,
Name: realFilename,
}
assetLinks[index] = a
if assetScore := a.EvaluateSuitability(capabilities); assetScore >= MIN_ASSET_SCORE {
logging.LogDebug("Found suitable candidate %v for download in release body. Score: %v", realFilename, assetScore)
assetsFromBodyScores[index] = assetScore
}
}
if len(assetsFromBodyScores) == 0 {
return nil, errors.New("no suitable candidates found in release body")
}
// sort the map by value of score.
highest := getHighestScore(assetsFromBodyScores)
bestAsset = assetLinks[highest.Key]
} else {
// sort the map by value of score.
highest := getHighestScore(assetScores)
bestAsset = ghr.Assets[highest.Key]
}
logging.LogDebug("Adding highest ranked asset %v to download queue.", bestAsset.RealFilename())
return &bestAsset, nil
}
func getHighestScore(assetScores map[int]int) Pair {
// sort the map by value of score.
assetsByScore := make(PairList, len(assetScores))
i := 0
for k, v := range assetScores {
assetsByScore[i] = Pair{k, v}
i++
}
sort.Sort(assetsByScore)
// return highest
return assetsByScore[len(assetsByScore)-1]
}
func (ghr *GithubRelease) inspectLinksInReleaseBody() []string {
const (
NAME_REGEXP = `([a-z][a-z0-9_-]+?)`
ARCH_REGEXP = `[._-](amd64|x86_64|x64|arm64|aarch64)`
OS_REGEXP = `[._-]((unknown[._-])?(linux|linux-gnu|linux-musl))|((apple[._-])?(darwin|macos|osx))`
VERSION_REGEXP = `([_-]v?[0-9.]+)?`
SUFFIX_REGEXP = `([_-][a-z0-9_-]+)?`
EXTENSION_REGEXP = `(\.zip|\.tar\.gz|\.gz|\.tgz|\.tar\.xz|\.txz|\.tar\.bz2|\.tbz)?`
REGEXP = NAME_REGEXP + VERSION_REGEXP + "(" + OS_REGEXP + ARCH_REGEXP + "|" + ARCH_REGEXP + OS_REGEXP + ")" + SUFFIX_REGEXP + EXTENSION_REGEXP
)
re := regexp.MustCompile(`https:\/\/[a-z0-9.\/]+\/` + REGEXP)
matches := re.FindAllString(ghr.Body, -1)
// remove duplicates
sort.Strings(matches)
// remove duplicates
seen := make(map[string]bool)
result := make([]string, 0)
for _, item := range matches {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
================================================
FILE: pkg/types/github_test.go
================================================
package types
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestEvalAssetSuitabilityDarwin(t *testing.T) {
t.Parallel()
// pluto_4.2.0_darwin_amd64.tar.gz = 9
// ruplacer-osx = 6
// croc_9.2.0_macOS-64bit.tar.gz = 7
// conftest_0.28.1_Darwin_x86_64.tar.gz = 7
// conftest_0.28.1_Darwin_arm64.tar.gz = 6
// pandoc-2.14.2-macOS.pkg = 6
// direnv.darwin-amd64 =8
osCap := &Capabilities{
OS: Darwin,
ExecutableMime: "application/x-mach-binary",
Arch: "arm64",
}
asset := Asset{
BrowserDownloadURL: "https://github.com/foo/bar/releases/download/v1.0/direnv.darwin-arm64",
}
require.Equal(t, 7, asset.EvaluateSuitability(osCap))
// pluto_4.2.0_darwin_amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pluto_4.2.0_darwin_arm64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// ruplacer-osx
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/ruplacer-osx"
require.Equal(t, 5, asset.EvaluateSuitability(osCap))
// croc_9.2.0_macOS-64bit.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/croc_9.2.0_macOS-64bit.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_x86_64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Darwin_x86_64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Darwin_arm64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// pandoc-2.14.2-macOS.pkg
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pandoc-2.14.2-macOS.pkg"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-darwin-amd64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-darwin-arm64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
}
func TestEvalAssetSuitabilityLinux(t *testing.T) {
t.Parallel()
// pluto_4.2.0_darwin_amd64.tar.gz = 9
// ruplacer-osx = 6
// croc_9.2.0_macOS-64bit.tar.gz = 7
// conftest_0.28.1_Darwin_x86_64.tar.gz = 7
// conftest_0.28.1_Darwin_arm64.tar.gz = 6
// pandoc-2.14.2-macOS.pkg = 6
// direnv.darwin-amd64 =8
// helm-v4.0.4-linux-arm64.tar.gz.asc <0
osCap := &Capabilities{
OS: Linux,
ExecutableMime: "asdf",
Arch: "amd64",
}
asset := Asset{
BrowserDownloadURL: "https://github.com/foo/bar/releases/download/v1.0/direnv.linux-amd64",
}
require.Equal(t, 7, asset.EvaluateSuitability(osCap))
// pluto_4.2.0_darwin_amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pluto_4.2.0_linux_amd64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// ruplacer-osx
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/ruplacer-linux"
require.Equal(t, 5, asset.EvaluateSuitability(osCap))
// croc_9.2.0_macOS-64bit.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/croc_9.2.0_linuX-64bit.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_x86_64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Linux_x86_64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Linux_arm64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// pandoc-2.14.2-macOS.pkg
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pandoc-2.14.2-linux.pkg"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-linux-arm64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-linux-amd64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// helm-v4.0.4-linux-arm64.tar.gz.asc
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/helm-v4.0.4-linux-arm64.tar.gz.asc"
require.Less(t, asset.EvaluateSuitability(osCap), 0)
}
func TestFindGithubReleaseMacAssets(t *testing.T) {
t.Parallel()
var assets []Asset
asset1 := Asset{
BrowserDownloadURL: "https://github.com/trufflesecurity/trufflehog/releases/download/v3.60.1/trufflehog_3.60.1_linux_amd64.tar.gz",
}
asset2 := Asset{
BrowserDownloadURL: "https://github.com/trufflesecurity/trufflehog/releases/download/v3.60.1/trufflehog_3.60.1_linux_arm64.tar.gz",
}
assets = append(assets, asset1, asset2)
ghr := GithubRelease{
Assets: assets,
}
capAMD64 := &Capabilities{
OS: Linux,
Arch: "amd64",
}
capARM64 := &Capabilities{
OS: Linux,
Arch: "arm64",
}
downloadableAsset, _ := ghr.FindBestAsset(capAMD64)
require.Equal(t, asset1, *downloadableAsset)
downloadableAsset, _ = ghr.FindBestAsset(capARM64)
require.Equal(t, asset2, *downloadableAsset)
}
func TestGetHighestScore(t *testing.T) {
t.Parallel()
assetScores := map[int]int{}
assetScores[0] = 6
assetScores[1] = 8
assetScores[2] = 1
assetScores[3] = 9
assetScores[4] = 3
assetsByScore := getHighestScore(assetScores)
require.Equal(t, assetsByScore.Value, assetScores[3])
}
func TestInspectLinksInReleaseBody(t *testing.T) {
t.Parallel()
filename := filepath.Join("..", "..", "testdata", "helm-latest.json")
jsonBytes, err := os.ReadFile(filename)
if err != nil {
t.Fatal("error reading testdata: ", err)
}
ghr := GithubRelease{}
if err := json.Unmarshal(jsonBytes, &ghr); err != nil {
t.Fatal("error unmarshalling testdata: ", err)
}
asset, err := ghr.FindBestAsset(&Capabilities{
OS: Linux,
Arch: "amd64",
})
require.NoError(t, err)
require.Equal(t, "helm-v4.0.4-linux-amd64.tar.gz", asset.Name)
require.Contains(t, asset.BrowserDownloadURL, "get.helm.sh")
}
================================================
FILE: pkg/types/os.go
================================================
package types
import (
"runtime"
)
type OS int
const (
Darwin OS = iota
Linux
)
func IsDarwin() bool {
return runtime.GOOS == "darwin"
}
func IsLinux() bool {
return runtime.GOOS == "linux"
}
type Capabilities struct {
OS OS
ExecutableMime string
SharedLibrary string
Arch string
}
func GetOS() OS {
if runtime.GOOS == "darwin" {
return Darwin
}
if runtime.GOOS == "linux" {
return Linux
}
return -1
}
var current *Capabilities
func GetCapabilities() *Capabilities {
if current != nil {
return current
}
switch runtime.GOOS {
case "darwin":
current = &Capabilities{OS: Darwin, ExecutableMime: "application/x-mach-binary", SharedLibrary: "application/x-sharedlib"}
case "linux":
current = &Capabilities{OS: Linux, ExecutableMime: "application/x-executable", SharedLibrary: "application/x-sharedlib"}
}
current.Arch = runtime.GOARCH
return current
}
================================================
FILE: pkg/utils/utils.go
================================================
package utils
import (
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
)
func DirExists(dir string) bool {
info, err := os.Stat(dir)
if err != nil {
return false
}
return info.IsDir()
}
func FileExists(filename string) bool {
info, err := os.Stat(filename)
return !os.IsNotExist(err) && !info.IsDir()
}
func FilePathWalkDir(root string) ([]string, error) {
var files []string
err := filepath.Walk(root, func(path string, info os.FileInfo, _ error) error {
if !info.IsDir() {
files = append(files, path)
}
return nil
})
return files, err
}
func CopyFile(source, destination string) error {
from, err := os.Open(source)
if err != nil {
log.Fatal(err)
}
defer from.Close()
to, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE, 0744)
if err != nil {
return err
}
defer to.Close()
_, err = io.Copy(to, from)
if err != nil {
return err
}
return nil
}
func GetGithubRelease(owner, repo, release string) (*types.GithubRelease, error) {
var url string
if release == "latest" {
logging.LogInfo("🌐 Getting releases for %s/%s:%s...", owner, repo, release)
url = fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/%s", owner, repo, release)
} else {
// try by tag
logging.LogInfo("🌐 Getting releases by tag %s...", release)
url = fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, release)
}
// create client
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// set headers for github auth
ghToken := os.Getenv("GITHUB_TOKEN")
if ghToken != "" {
logging.LogDebug("Using Github token in http request")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ghToken))
}
// make request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("invalid HTTP status: %v", resp.StatusCode)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ghr := types.GithubRelease{}
if err := json.Unmarshal(body, &ghr); err != nil {
return nil, err
}
return &ghr, nil
}
================================================
FILE: pkg/utils/utils_test.go
================================================
package utils
import (
"os"
"path/filepath"
"testing"
"github.com/h2non/gock"
"github.com/stretchr/testify/require"
)
func TestGetGithubRelease(t *testing.T) {
defer gock.Off()
filename := filepath.Join("..", "..", "testdata", "helm-latest.json")
jsonBytes, err := os.ReadFile(filename)
if err != nil {
t.Fatal("error reading testdata: ", err)
}
gock.New("https://api.github.com").
Get("/repos/helm/helm/releases/latest").
Reply(200).
JSON(jsonBytes)
ghr, err := GetGithubRelease("helm", "helm", "latest")
require.NoError(t, err)
require.Equal(t, "v4.0.4", ghr.TagName)
}
================================================
FILE: testdata/helm-latest.json
================================================
{
"url": "https://api.github.com/repos/helm/helm/releases/270052681",
"assets_url": "https://api.github.com/repos/helm/helm/releases/270052681/assets",
"upload_url": "https://uploads.github.com/repos/helm/helm/releases/270052681/assets{?name,label}",
"html_url": "https://github.com/helm/helm/releases/tag/v4.0.4",
"id": 270052681,
"author": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"node_id": "RE_kwDOApspmc4QGK1J",
"tag_name": "v4.0.4",
"target_commitish": "main",
"name": "Helm v4.0.4",
"draft": false,
"immutable": false,
"prerelease": false,
"created_at": "2025-12-13T01:05:06Z",
"updated_at": "2025-12-13T01:23:13Z",
"published_at": "2025-12-13T01:22:50Z",
"assets": [
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071815",
"id": 328071815,
"node_id": "RA_kwDOApspmc4TjfqH",
"name": "helm-v4.0.4-darwin-amd64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:943463856d9a7da8abfdb6421e2b7bef3197565b32ecea04bfb0a93fa15db2fe",
"download_count": 266,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:08Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-amd64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071822",
"id": 328071822,
"node_id": "RA_kwDOApspmc4TjfqO",
"name": "helm-v4.0.4-darwin-amd64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:5c0fcfab9868357ab5f540c6d470c3cf0c9373ab33d353f56914ba1731b656be",
"download_count": 170,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:08Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-amd64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071823",
"id": 328071823,
"node_id": "RA_kwDOApspmc4TjfqP",
"name": "helm-v4.0.4-darwin-amd64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:1db897efd998fcadc5edd249a828b5d0401d6c2bd643828d9c528f607362e458",
"download_count": 172,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:08Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-amd64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071827",
"id": 328071827,
"node_id": "RA_kwDOApspmc4TjfqT",
"name": "helm-v4.0.4-darwin-arm64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:fd44bbc3d2b875ad61713fd13de0dff34864d5c52381c7247bd1dfbebbc7316a",
"download_count": 187,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-arm64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071832",
"id": 328071832,
"node_id": "RA_kwDOApspmc4TjfqY",
"name": "helm-v4.0.4-darwin-arm64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:538510d78f375c6b2ef7d7b87ce33ee279680c32a670a33317c5c7a71228eee0",
"download_count": 147,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-arm64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071835",
"id": 328071835,
"node_id": "RA_kwDOApspmc4Tjfqb",
"name": "helm-v4.0.4-darwin-arm64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:6f6b2b3704ba05d7357563868335155f0d4b2852f430b57bd167486ee8b481b9",
"download_count": 153,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-arm64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071838",
"id": 328071838,
"node_id": "RA_kwDOApspmc4Tjfqe",
"name": "helm-v4.0.4-linux-386.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:02fa09a1e5f6f40b33e1fe7a8e404cced85f47480c3d272f4d75f297afe64b8f",
"download_count": 219,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-386.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071840",
"id": 328071840,
"node_id": "RA_kwDOApspmc4Tjfqg",
"name": "helm-v4.0.4-linux-386.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:ced78ed6756b7820b764b9487e7bee13d259ce8d943a1b6ad27f478b1bf019ed",
"download_count": 164,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-386.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071842",
"id": 328071842,
"node_id": "RA_kwDOApspmc4Tjfqi",
"name": "helm-v4.0.4-linux-386.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:b33b73be03e14589873a63b59a01ddd2f75d63d6783b206abcf1ca3a29f9359d",
"download_count": 166,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-386.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071844",
"id": 328071844,
"node_id": "RA_kwDOApspmc4Tjfqk",
"name": "helm-v4.0.4-linux-amd64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:37184ca689c09c8fbe67801b3c0c6da602013f84514f18a5da40351fb16e716d",
"download_count": 6591,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-amd64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071849",
"id": 328071849,
"node_id": "RA_kwDOApspmc4Tjfqp",
"name": "helm-v4.0.4-linux-amd64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:970ce8c0f959eb632867991e769ac0bcd8121bce15c4960f89d4cfe369268b03",
"download_count": 6101,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-amd64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071850",
"id": 328071850,
"node_id": "RA_kwDOApspmc4Tjfqq",
"name": "helm-v4.0.4-linux-amd64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:d3255c9bb0a7a12fa18998ba2b0bb0f1704c0fea07bf45c8a078274376bfebe0",
"download_count": 205,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-amd64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071851",
"id": 328071851,
"node_id": "RA_kwDOApspmc4Tjfqr",
"name": "helm-v4.0.4-linux-arm.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:3bfc27ba402781a4ebd20a2afddca7ec31f3ae0ef66317f769ae9c4840cd03cd",
"download_count": 146,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071853",
"id": 328071853,
"node_id": "RA_kwDOApspmc4Tjfqt",
"name": "helm-v4.0.4-linux-arm.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:b7d7ed2334381758e159154c649b4045bca08c8aff72caf22ced967e2e10746d",
"download_count": 137,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071856",
"id": 328071856,
"node_id": "RA_kwDOApspmc4Tjfqw",
"name": "helm-v4.0.4-linux-arm.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:98694fe87b905ca078bc844dd1e9b6ed8bf73e761fda33fcb11acfb0fdd4a56f",
"download_count": 139,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071858",
"id": 328071858,
"node_id": "RA_kwDOApspmc4Tjfqy",
"name": "helm-v4.0.4-linux-arm64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:29f58b92bc616ff27a96157c89f4c71b44ad74ff2b93225ef7e5bf6f04ca6444",
"download_count": 1328,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071859",
"id": 328071859,
"node_id": "RA_kwDOApspmc4Tjfqz",
"name": "helm-v4.0.4-linux-arm64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:85c2c1b29af5020aee6f865830ca7c78535582c08961e442c3841edfc4ae9bc6",
"download_count": 1305,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071868",
"id": 328071868,
"node_id": "RA_kwDOApspmc4Tjfq8",
"name": "helm-v4.0.4-linux-arm64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:7008fa1fe790ce223945651da31dec9e2147412ee6c77ffe9dfdb3a36e1fdde0",
"download_count": 133,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071876",
"id": 328071876,
"node_id": "RA_kwDOApspmc4TjfrE",
"name": "helm-v4.0.4-linux-loong64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:6494e3aa743c4e7e18cc9d7f5afdadbc6798a5a0e8fc63dd2285b3a951982481",
"download_count": 143,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-loong64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071889",
"id": 328071889,
"node_id": "RA_kwDOApspmc4TjfrR",
"name": "helm-v4.0.4-linux-loong64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:8ae2f601edf1110027f54459dc40130a4395308bcdf75ec17a68f40b7706169a",
"download_count": 143,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-loong64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071891",
"id": 328071891,
"node_id": "RA_kwDOApspmc4TjfrT",
"name": "helm-v4.0.4-linux-loong64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:51fc79563532f1e3949bdf3b254f2d3c255dc8b79b399fd94262b675e97d20d8",
"download_count": 144,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-loong64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071892",
"id": 328071892,
"node_id": "RA_kwDOApspmc4TjfrU",
"name": "helm-v4.0.4-linux-ppc64le.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:61848dd0d2c20ad62145b89adc9ddcf7bb3d26bb486292662b6593a210645720",
"download_count": 51,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-ppc64le.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071894",
"id": 328071894,
"node_id": "RA_kwDOApspmc4TjfrW",
"name": "helm-v4.0.4-linux-ppc64le.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:7c5eb882502e657db2ea23d08974021f1d6585a2fd6acc8e7e2c9d2c65910f3a",
"download_count": 54,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-ppc64le.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071897",
"id": 328071897,
"node_id": "RA_kwDOApspmc4TjfrZ",
"name": "helm-v4.0.4-linux-ppc64le.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:ab546edb11ea05ad21b3a10ed94c289c8cd1e91be856914bcda68feb30a74bc5",
"download_count": 51,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-ppc64le.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071898",
"id": 328071898,
"node_id": "RA_kwDOApspmc4Tjfra",
"name": "helm-v4.0.4-linux-riscv64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:91c1382737e7426f51c25e3643e24cd1e2c53a79d6564105b657dfc9ec33dcde",
"download_count": 142,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-riscv64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071899",
"id": 328071899,
"node_id": "RA_kwDOApspmc4Tjfrb",
"name": "helm-v4.0.4-linux-riscv64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:a9d3b44f24caafa628ace829f96043f13bebfe19635134bd3e81aa16a51809dc",
"download_count": 141,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-riscv64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071900",
"id": 328071900,
"node_id": "RA_kwDOApspmc4Tjfrc",
"name": "helm-v4.0.4-linux-riscv64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:f6ec68ffd30bf9b01083699097e67353eea81bba60164ba7c4d561f6bdce25e9",
"download_count": 144,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-riscv64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071901",
"id": 328071901,
"node_id": "RA_kwDOApspmc4Tjfrd",
"name": "helm-v4.0.4-linux-s390x.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:699c5924c1ca2d02229e43f3f0120ad98a74007e52e9eae82cd54e0f30adb098",
"download_count": 52,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-s390x.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071902",
"id": 328071902,
"node_id": "RA_kwDOApspmc4Tjfre",
"name": "helm-v4.0.4-linux-s390x.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:72348b11c9e2c4868fd201a15657f4049f9c91c5cc51df5c0f2d7efb2eee7bd9",
"download_count": 49,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-s390x.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071903",
"id": 328071903,
"node_id": "RA_kwDOApspmc4Tjfrf",
"name": "helm-v4.0.4-linux-s390x.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:c6c42a06b3d02ee3b45f74ae7799dcddfdb3d626d8447f0bfa915ae96d5d8394",
"download_count": 51,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-s390x.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071904",
"id": 328071904,
"node_id": "RA_kwDOApspmc4Tjfrg",
"name": "helm-v4.0.4-windows-amd64.zip.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:eb6d3871d204f77552c6e6783eec76b7182df3bd3ab60c4b95a34e6e73eba409",
"download_count": 340,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-amd64.zip.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071905",
"id": 328071905,
"node_id": "RA_kwDOApspmc4Tjfrh",
"name": "helm-v4.0.4-windows-amd64.zip.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:afd5bccf1a5ae673e7e765dc358e19f4a1ecc1a33b8bf1fedd8c04e085788779",
"download_count": 181,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-amd64.zip.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071906",
"id": 328071906,
"node_id": "RA_kwDOApspmc4Tjfri",
"name": "helm-v4.0.4-windows-amd64.zip.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:a813554ad03442e5b2ce6bd53ec6dd71c27b06f6bec449d1f5ebf2091d38db19",
"download_count": 161,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-amd64.zip.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071907",
"id": 328071907,
"node_id": "RA_kwDOApspmc4Tjfrj",
"name": "helm-v4.0.4-windows-arm64.zip.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:63cfea9ba48829939f459ade799583247955116dd3bcaae7eecb4ca575aca933",
"download_count": 166,
"created_at": "2025-12-13T01:23:13Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-arm64.zip.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071909",
"id": 328071909,
"node_id": "RA_kwDOApspmc4Tjfrl",
"name": "helm-v4.0.4-windows-arm64.zip.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:463780016e11b617f87a9c79b20513c61a0ddc9c27ebb42ee267c5a2e4654fed",
"download_count": 128,
"created_at": "2025-12-13T01:23:13Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-arm64.zip.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071910",
"id": 328071910,
"node_id": "RA_kwDOApspmc4Tjfrm",
"name": "helm-v4.0.4-windows-arm64.zip.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:4f27866327d78cca9bcafd836796bec8a0c79b15ac48a8001607593913a635ce",
"download_count": 129,
"created_at": "2025-12-13T01:23:13Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-arm64.zip.sha256sum.asc"
}
],
"tarball_url": "https://api.github.com/repos/helm/helm/tarball/v4.0.4",
"zipball_url": "https://api.github.com/repos/helm/helm/zipball/v4.0.4",
"body": "Helm v4.0.4 is a security fix for a Go CVE in the previous tag. This patch release rebuilds the Helm `v4.0.2` release with the latest Go toolchain, to fix the Go CVE. Users are encouraged to upgrade. Note that tag v4.0.3 was skipped due to a build failure.\r\n\r\nThe community keeps growing, and we'd love to see you there!\r\n\r\n- Join the discussion in [Kubernetes Slack](https://kubernetes.slack.com):\r\n - for questions and just to hang out\r\n - for discussing PRs, code, and bugs\r\n- Hang out at the Public Developer Call: Thursday, 9:30 Pacific via [Zoom](https://zoom.us/j/696660622)\r\n- Test, debug, and contribute charts: [ArtifactHub/packages](https://artifacthub.io/packages/search?kind=0)\r\n\r\n## Installation and Upgrading\r\n\r\nDownload Helm v4.0.4. The common platform binaries are here:\r\n\r\n- [MacOS amd64](https://get.helm.sh/helm-v4.0.4-darwin-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-darwin-amd64.tar.gz.sha256sum) / 73bcfd6ab000fdc95acf9fe1c59e8e47179426a653e45ae485889869d4a00523)\r\n- [MacOS arm64](https://get.helm.sh/helm-v4.0.4-darwin-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-darwin-arm64.tar.gz.sha256sum) / a7ea99937a9679b3935fa0a2b70e577aa1ea84e5856e7c0821ca6ffa064ea976)\r\n- [Linux amd64](https://get.helm.sh/helm-v4.0.4-linux-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-amd64.tar.gz.sha256sum) / 29454bc351f4433e66c00f5d37841627cbbcc02e4c70a6d796529d355237671c)\r\n- [Linux arm](https://get.helm.sh/helm-v4.0.4-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-arm.tar.gz.sha256sum) / 9255732e31b5aa5ee7b55be8497eea4723e3dfb08a63c37603ae0d15a9a9d82c)\r\n- [Linux arm64](https://get.helm.sh/helm-v4.0.4-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-arm64.tar.gz.sha256sum) / 16b88acc6503d646b7537a298e7389bef469c5cc9ebadf727547abe9f6a35903)\r\n- [Linux i386](https://get.helm.sh/helm-v4.0.4-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-386.tar.gz.sha256sum) / e6dbf45313bab48e51a2b7a5f3271a19bb3d8b9f07b4bb48ba342389d902af53)\r\n- [Linux loong64](https://get.helm.sh/helm-v4.0.4-linux-loong64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-loong64.tar.gz.sha256sum) / BlobNotFoundThe specified blob does not exist.\r\nRequestId:11673868-901e-003e-10cd-6b624b000000\r\nTime:2025-12-13T01:15:26.0922049Z)\r\n- [Linux ppc64le](https://get.helm.sh/helm-v4.0.4-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-ppc64le.tar.gz.sha256sum) / c108d181a0e29dadf281fbb4f4a0e0f2149922b119ec745ced1a5ae6f0918703)\r\n- [Linux s390x](https://get.helm.sh/helm-v4.0.4-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-s390x.tar.gz.sha256sum) / cdf172c59379f0a3fe1db4743c16f122745fdaaebb2fbbfa40ce5722a4787717)\r\n- [Linux riscv64](https://get.helm.sh/helm-v4.0.4-linux-riscv64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-riscv64.tar.gz.sha256sum) / 2cf1c77d993bf5386e85249007bdaf38358d2516b18454212206a81b132e1330)\r\n- [Windows amd64](https://get.helm.sh/helm-v4.0.4-windows-amd64.zip) ([checksum](https://get.helm.sh/helm-v4.0.4-windows-amd64.zip.sha256sum) / 135bffadd3c87aff8856e06efb366bea2a48ac4d1742d73af80250410246f14d)\r\n- [Windows arm64](https://get.helm.sh/helm-v4.0.4-windows-arm64.zip) ([checksum](https://get.helm.sh/helm-v4.0.4-windows-arm64.zip.sha256sum) / b65d05f15260e78311f463773f54fe68f6d74444b3c3e84cecf270cdb927cd8a)\r\n\r\nThis release was signed with `208D D36E D5BB 3745 A167 43A4 C7C6 FBB5 B91C 1155` and can be found at @scottrigby [keybase account](https://keybase.io/r6by). Please use the attached signatures for verifying this release using `gpg`.\r\n\r\nThe [Quickstart Guide](https://helm.sh/docs/intro/quickstart/) will get you going from there. For **upgrade instructions** or detailed installation notes, check the [install guide](https://helm.sh/docs/intro/install/). You can also use a [script to install](https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3) on any system with `bash`.\r\n\r\n## What's Next\r\n\r\n- 3.19.5 and 4.0.5 are the next patch releases and will be on January 14, 2026\r\n- 3.20.0 and 4.1.0 is the next minor releases and will be on January 21, 2026\r\n\r\n## Changelog\r\n\r\n- Bump v4.0.2 CVE deps cd700e0627b8d9a4997a7ab2bc3b712d0de4dcd3 (George Jenkins)\r\n- Use latest patch release of Go in releases 9db13ee5c343196f642c568a03e58d3221b324d6 (Matt Farina)",
"reactions": {
"url": "https://api.github.com/repos/helm/helm/releases/270052681/reactions",
"total_count": 8,
"+1": 8,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"mentions_count": 1
}