Release Automation

6 minute read

Release Automation Overview

Automating releases ensures consistency, reduces errors, and saves time. GoReleaser and GitHub Actions provide complete automation for Terraform provider releases.

GoReleaser

GoReleaser builds, archives, checksums, and publishes Go projects.

Installation

# macOS
brew install goreleaser/tap/goreleaser

# Linux
curl -sfL https://goreleaser.com/static/run | sh

# Verify installation
goreleaser --version

Basic Configuration

Create .goreleaser.yml:

# .goreleaser.yml
version: 2

before:
  hooks:
    # Ensure dependencies are tidy
    - go mod tidy
    # Generate any code
    - go generate ./...

builds:
  - env:
      # Disable CGO for static binaries
      - CGO_ENABLED=0
    mod_timestamp: '{{ .CommitTimestamp }}'
    flags:
      - -trimpath
    ldflags:
      - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - amd64
      - '386'
      - arm
      - arm64
    ignore:
      - goos: darwin
        goarch: '386'
      - goos: windows
        goarch: arm64
      - goos: windows
        goarch: arm
    binary: '{{ .ProjectName }}_v{{ .Version }}'

archives:
  - format: zip
    name_template: '{{ .ProjectName }}_{{ .Version}}_{{ .Os }}_{{ .Arch }}'

checksum:
  name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
  algorithm: sha256

signs:
  - artifacts: checksum
    args:
      - "--batch"
      - "--local-user"
      - "{{ .Env.GPG_FINGERPRINT }}"
      - "--output"
      - "${signature}"
      - "--detach-sign"
      - "${artifact}"

release:
  draft: false
  prerelease: auto

changelog:
  sort: asc
  use: github
  filters:
    exclude:
      - '^docs:'
      - '^test:'
      - Merge pull request
      - Merge branch
  groups:
    - title: Features
      regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
      order: 0
    - title: 'Bug fixes'
      regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
      order: 1
    - title: Others
      order: 999

Testing Locally

# Test release without publishing
goreleaser release --snapshot --clean

# Check dist/ directory
ls -la dist/

# Verify archives
unzip -l dist/terraform-provider-cachecloud_1.0.0_darwin_amd64.zip

GitHub Actions Workflow

Release Workflow

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version-file: 'go.mod'
          cache: true
      
      - name: Import GPG key
        id: import_gpg
        uses: crazy-max/ghaction-import-gpg@v6
        with:
          gpg_private_key: $
          passphrase: $
      
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v5
        with:
          distribution: goreleaser
          version: latest
          args: release --clean
        env:
          GPG_FINGERPRINT: $
          GITHUB_TOKEN: $

Test Workflow

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-go@v5
        with:
          go-version-file: 'go.mod'
          cache: true
      
      - run: go mod download
      
      - run: go build -v .
  
  generate:
    name: Go Generate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-go@v5
        with:
          go-version-file: 'go.mod'
          cache: true
      
      - run: go generate ./...
      
      - name: git diff
        run: |
          git diff --compact-summary --exit-code || \
            (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1)
  
  test:
    name: Terraform Provider Acceptance Tests
    needs: build
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        terraform:
          - '1.5.*'
          - '1.6.*'
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-go@v5
        with:
          go-version-file: 'go.mod'
          cache: true
      
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: $
          terraform_wrapper: false
      
      - run: go mod download
      
      - env:
          TF_ACC: "1"
        run: go test -v -cover ./internal/provider/
        timeout-minutes: 10

GPG Key Management

Generate GPG Key

# Generate key
gpg --full-generate-key

# Select options:
# - Kind: RSA and RSA
# - Size: 4096 bits
# - Expiration: 2 years
# - Name: Your Name
# - Email: your.email@example.com
# - Passphrase: (strong passphrase)

Export Keys

# List keys
gpg --list-secret-keys --keyid-format=long

# Output:
# sec   rsa4096/ABCD1234EFGH5678 2026-01-01 [SC] [expires: 2028-01-01]
# uid                 [ultimate] Your Name <your.email@example.com>

# Export private key (for GitHub Secrets)
gpg --armor --export-secret-keys ABCD1234EFGH5678 > private-key.asc

# Export public key (for Terraform Registry)
gpg --armor --export ABCD1234EFGH5678 > public-key.asc

# Get fingerprint
gpg --fingerprint ABCD1234EFGH5678

Store in GitHub Secrets

Settings → Secrets and variables → Actions → New repository secret

GPG_PRIVATE_KEY: (contents of private-key.asc)
PASSPHRASE: (your GPG passphrase)

Multi-Platform Builds

Supported Platforms

# .goreleaser.yml
builds:
  - goos:
      - linux
      - windows
      - darwin
      - freebsd
    goarch:
      - amd64
      - 386
      - arm
      - arm64
    goarm:
      - 6
      - 7
    ignore:
      - goos: darwin
        goarch: 386
      - goos: darwin
        goarch: arm
      - goos: windows
        goarch: arm

Binary Naming

binary: '{{ .ProjectName }}_v{{ .Version }}'

Generates:

terraform-provider-cachecloud_v1.0.0

Archive Naming

archives:
  - format: zip
    name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'

Generates:

terraform-provider-cachecloud_1.0.0_linux_amd64.zip
terraform-provider-cachecloud_1.0.0_darwin_amd64.zip
terraform-provider-cachecloud_1.0.0_windows_amd64.zip

Checksums and Signatures

Checksums

checksum:
  name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
  algorithm: sha256

Generates:

terraform-provider-cachecloud_1.0.0_SHA256SUMS

Content:

a1b2c3d4... terraform-provider-cachecloud_1.0.0_linux_amd64.zip
e5f6g7h8... terraform-provider-cachecloud_1.0.0_darwin_amd64.zip
i9j0k1l2... terraform-provider-cachecloud_1.0.0_windows_amd64.zip

Signatures

signs:
  - artifacts: checksum
    args:
      - "--batch"
      - "--local-user"
      - "{{ .Env.GPG_FINGERPRINT }}"
      - "--output"
      - "${signature}"
      - "--detach-sign"
      - "${artifact}"

Generates:

terraform-provider-cachecloud_1.0.0_SHA256SUMS.sig

Verification

# Verify signature
gpg --verify terraform-provider-cachecloud_1.0.0_SHA256SUMS.sig \
              terraform-provider-cachecloud_1.0.0_SHA256SUMS

# Verify checksum
sha256sum -c terraform-provider-cachecloud_1.0.0_SHA256SUMS

Changelog Generation

Automatic Changelog

changelog:
  sort: asc
  use: github
  filters:
    exclude:
      - '^docs:'
      - '^test:'
      - 'Merge pull request'
      - 'Merge branch'
  groups:
    - title: Features
      regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
      order: 0
    - title: 'Bug fixes'
      regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
      order: 1
    - title: 'Enhancements'
      regexp: '^.*?chore|perf|refactor(\([[:word:]]+\))??!?:.+$'
      order: 2
    - title: Others
      order: 999

Commit Message Format

# Feature
git commit -m "feat: add backup_window attribute"

# Bug fix
git commit -m "fix: correct cache deletion timeout"

# Enhancement
git commit -m "chore: update dependencies"

# Breaking change
git commit -m "feat!: rename num_nodes to node_count"

Generated changelog:

## Features
* feat: add backup_window attribute

## Bug fixes
* fix: correct cache deletion timeout

## Enhancements
* chore: update dependencies

## Breaking Changes
* feat!: rename num_nodes to node_count

Release Notes

Custom Release Notes

release:
  github:
    owner: example
    name: terraform-provider-cachecloud
  
  draft: false
  prerelease: auto
  
  header: |
    ## What's Changed
  
  footer: |
    ## Installation
    
    ```terraform
    terraform {
      required_providers {
        cachecloud = {
          source  = "example/cachecloud"
          version = "{{ .Version }}"
        }
      }
    }
    ```
    
    **Full Changelog**: https://github.com/example/terraform-provider-cachecloud/compare/{{ .PreviousTag }}...{{ .Tag }}

Complete Example

Project Structure

terraform-provider-cachecloud/
├── .github/
│   └── workflows/
│       ├── release.yml
│       └── test.yml
├── .goreleaser.yml
├── internal/
│   └── provider/
├── main.go
├── go.mod
└── go.sum

.goreleaser.yml

version: 2

before:
  hooks:
    - go mod tidy

builds:
  - env:
      - CGO_ENABLED=0
    mod_timestamp: '{{ .CommitTimestamp }}'
    flags:
      - -trimpath
    ldflags:
      - '-s -w -X main.version={{.Version}}'
    goos:
      - freebsd
      - windows
      - linux
      - darwin
    goarch:
      - amd64
      - '386'
      - arm
      - arm64
    ignore:
      - goos: darwin
        goarch: '386'
      - goos: windows
        goarch: arm64
    binary: '{{ .ProjectName }}_v{{ .Version }}'

archives:
  - format: zip
    name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'

checksum:
  extra_files:
    - glob: 'terraform-registry-manifest.json'
      name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
  name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
  algorithm: sha256

signs:
  - artifacts: checksum
    args:
      # if you are using this in a GitHub action or some other automated pipeline, you 
      # need to pass the batch flag to indicate its not interactive.
      - "--batch"
      - "--local-user"
      - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
      - "--output"
      - "${signature}"
      - "--detach-sign"
      - "${artifact}"

release:
  extra_files:
    - glob: 'terraform-registry-manifest.json'
      name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'

changelog:
  use: github
  sort: asc
  abbrev: 0
  groups:
    - title: Features
      regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
      order: 0
    - title: 'Bug Fixes'
      regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
      order: 1
    - title: 'Enhancements'
      regexp: '^.*?chore(\([[:word:]]+\))??!?:.+$'
      order: 2
    - title: Others
      order: 999
  filters:
    exclude:
      - '^docs:'
      - '^test:'

Testing the Release Process

Dry Run

# Test without creating release
goreleaser release --snapshot --clean

# Check output
ls -la dist/

Local Release Test

# Set environment variables
export GPG_FINGERPRINT=$(gpg --list-secret-keys --keyid-format=long | grep sec | awk '{print $2}' | cut -d'/' -f2)

# Test release
goreleaser release --snapshot --clean --skip=publish

# Verify archives
unzip -l dist/terraform-provider-cachecloud_*.zip

# Verify checksums
cd dist
sha256sum -c *_SHA256SUMS

Troubleshooting

Issue: GPG Signing Fails

Problem:

error: gpg: signing failed: Inappropriate ioctl for device

Solution:

# Add to ~/.bashrc or ~/.zshrc
export GPG_TTY=$(tty)

# Reload
source ~/.bashrc

Issue: Build Fails

Problem:

error: Build failed for darwin/386

Solution: Add to ignore list in .goreleaser.yml:

ignore:
  - goos: darwin
    goarch: '386'

Issue: Missing Artifacts

Problem: Release created but artifacts missing

Solution: Check GitHub Actions logs:

- name: Debug
  run: ls -la dist/

Key Takeaways

  • ✅ Use GoReleaser for consistent releases
  • ✅ Automate with GitHub Actions
  • ✅ Sign releases with GPG
  • ✅ Build for multiple platforms
  • ✅ Generate checksums
  • ✅ Auto-generate changelogs
  • ✅ Test releases locally first
  • ✅ Store GPG keys securely
  • ✅ Follow commit message conventions
  • ✅ Monitor release workflow

What’s Next?

Learn about signing and security best practices for provider releases.

Next: Signing & Security →


Previous: ← Versioning & Releases
Next: Signing & Security →

Last updated: June 30, 2026

Updated: