testcontainers and Github Actions Matrix Strategies to dynamically run tests against HashiCorp Vaults last 3 major versions
TL;DR testcontainers
testcontainers is a powerful testing library available in multiple languages. It allows you to spin up (and tear down) containers during your unit or end-to-end tests with ease. I've been using it for years in my open source projects to mock APIs and dependencies like Vault, localstack, or Redis. testcontainers includes a wide range of supported modules ready for use in your tests.
Running Tests Against the Last 3 Major Versions of HashiCorp Vault
Recently, I wanted to use testcontainers to test vkv — a Go CLI tool for importing and exporting KV secrets — against the last three major versions of HashiCorp Vault. Needless to say, I wanted to avoid manually updating the versions every time a new Vault version was released.
💡 I got it working with a little curl and shell magic
Step 1: Fetch the Last 3 Major Versions of HashiCorp Vault
We start with a GitHub Actions job to fetch Vault’s latest tags from its GitHub repo. We then filter the tags to keep only those that end in .0, extract the top three, and write them to GITHUB_OUTPUT:
[...]
vault-versions:
runs-on: ubuntu-latest
outputs:
vault-versions: ${{ steps.vault-versions.outputs.versions }}
steps:
- name: Fetch latest tags
id: vault-versions
run: |
echo "versions=$(curl -s "https://api.github.com/repos/hashicorp/vault/tags?per_page=100" \
| jq -r '.[].name' \
| grep -E '^v?[0-9]+\.[0-9]+\.0$' \
| sed 's/^v//' \
| head -n 3 \
| jq -cRn '[inputs]')" >> "$GITHUB_OUTPUT"
- name: Last 3 Vault major versions
run: echo "${{ steps.vault-versions.outputs.versions }}"
Step 2: Run Tests Using a Matrix Strategy
We then use the output from the previous step to run a test job against each version using GitHub’s matrix strategy. The current version is passed to the test via the VAULT_VERSION environment variable:
test:
runs-on: ubuntu-latest
needs: vault-versions
strategy:
matrix:
version: ${{ fromJson(needs.vault-versions.outputs.vault-versions) }}
steps:
- uses: actions/checkout@v4
- run: go generate -tags tools tools/tools.go
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: go get
run: go get ./...
- name: Run coverage
run: |
gotestsum -- -v -race -coverprofile="coverage.out" -covermode=atomic ./...
env:
VAULT_VERSION: ${{ matrix.version }}
Step 3: Use the Version in Go Code
In the Go test code, Im checking for the presence of the VAULT_VERSION environment variable. If it’s set, we use its value; otherwise, we fall back to a default version:
var VaultVersion ="1.20.0"
func StartTestContainer(commands ...string) (*TestContainer, error) {
ctx := context.Background()
if v, ok := os.LookupEnv(VaultVersionEnv); ok {
VaultVersion = v
}
vaultContainer, err := vault.Run(ctx, "hashicorp/vault:" + VaultVersion,
vault.WithToken(token),
vault.WithInitCommand(commands...),
)
if err != nil {
return nil, fmt.Errorf("failed to start container: %w", err)
}
uri, err := vaultContainer.HttpHostAddress(ctx)
if err != nil {
return nil, fmt.Errorf("error returning container mapped port: %w", err)
}
return &TestContainer{
Container: vaultContainer,
URI: uri,
Token: token,
}, nil
}
Result
✅ As you can see here, this creates a separate test job for each of the last three major versions of HashiCorp Vault!
Now every time a new major Vault version is released, your test matrix stays up to date — without any manual updates.
