CI release workflow
Multi-platform GitHub Actions workflow that builds a native binary for each target platform, then packages and publishes a signed GitHub Release.
Node.js vs Bun
| Node.js | Bun | |
|---|---|---|
| Cross-compilation | ✗ — one native runner per platform | ✓ — single runner, all targets |
| CI jobs for a full release | 5–7 build jobs + 1 release job | 1 build job + 1 release job |
| Binary runtime | Node.js 25.5+ | Bun 0.6.0+ |
GitHub Actions workflow
Node.js SEA does not support cross-compilation — a binary built on macOS will not run on Linux. The solution is a CI matrix that builds natively on each platform, then a single packaging step that assembles all binaries into the release assets the Terraform Registry requires.
CI matrix (one job per platform)
└─ terrably build → bin/terraform-provider-mycloud_{os}_{arch}
Packaging job
└─ terrably publish → release/
terraform-provider-mycloud_1.0.0_linux_amd64.zip
terraform-provider-mycloud_1.0.0_linux_arm64.zip
terraform-provider-mycloud_1.0.0_darwin_amd64.zip
terraform-provider-mycloud_1.0.0_darwin_arm64.zip
terraform-provider-mycloud_1.0.0_windows_amd64.zip
terraform-provider-mycloud_1.0.0_manifest.json
terraform-provider-mycloud_1.0.0_SHA256SUMS
terraform-provider-mycloud_1.0.0_SHA256SUMS.sigCreate .github/workflows/release.yml in your provider repository. Replace mycloud with your provider's short name.
name: Release
# Triggers on any tag matching v* (e.g. v1.0.0, v1.2.3-beta)
on:
push:
tags:
- 'v*'
permissions:
contents: write # needed to create the GitHub Release
env:
PROVIDER_NAME: mycloud # ← change this
jobs:
# ── Build: one job per OS/arch, runs terrably build natively ─────────────
build:
name: Build (${{ matrix.os }}_${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
# Primary targets (required for HCP Terraform compatibility)
- { os: linux, arch: amd64, runner: ubuntu-slim }
- { os: darwin, arch: amd64, runner: macos-15 intel }
- { os: darwin, arch: arm64, runner: macos-latest }
- { os: windows, arch: amd64, runner: windows-latest }
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '25'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build SEA binary
shell: bash
run: npx terrably build --name "$PROVIDER_NAME"
- name: Rename binary with platform suffix
shell: bash
run: |
EXT=""
if [ "${{ matrix.os }}" = "windows" ]; then EXT=".exe"; fi
mv "bin/terraform-provider-${PROVIDER_NAME}${EXT}" \
"bin/terraform-provider-${PROVIDER_NAME}_${{ matrix.os }}_${{ matrix.arch }}${EXT}"
- uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.os }}-${{ matrix.arch }}
path: bin/terraform-provider-${{ env.PROVIDER_NAME }}_${{ matrix.os }}_${{ matrix.arch }}*
if-no-files-found: error
retention-days: 1
# ── Release: collect all binaries, package, sign, publish ────────────────
release:
name: Publish to GitHub Releases
needs: build
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '25'
cache: 'npm'
- name: Install dependencies
run: npm ci
- uses: actions/download-artifact@v8
with:
path: bin/
pattern: binary-*
merge-multiple: true
- name: Import GPG key
run: |
echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
FP=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')
echo "GPG_FINGERPRINT=${FP}" >> "$GITHUB_ENV"
- name: Package, sign, and publish release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx terrably publish \
--release-version "${{ github.ref_name }}" \
--gpg-key "${GPG_FINGERPRINT}" \
--github-releaseCross-compilation is not supported with Node.js SEA. Each platform target requires its own native runner in the CI matrix. Use Bun to cross-compile from a single job instead.
Bun supports cross-compilation natively — a single runner can produce binaries for Linux, macOS, and Windows in one job, eliminating the need for a build matrix. Pass any target from Bun's supported targets list to --target.
Single build job (one runner, all platforms)
└─ terrably build --target bun-linux-x64 → bin/terraform-provider-mycloud
└─ terrably build --target bun-linux-arm64 → bin/terraform-provider-mycloud
└─ terrably build --target bun-darwin-arm64 → bin/terraform-provider-mycloud
└─ terrably build --target bun-darwin-x64 → bin/terraform-provider-mycloud
└─ terrably build --target bun-windows-x64 → bin/terraform-provider-mycloud.exe
Packaging job
└─ terrably publish → release/
terraform-provider-mycloud_1.0.0_linux_amd64.zip
...Create .github/workflows/release.yml in your provider repository. Replace mycloud with your provider's short name.
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
PROVIDER_NAME: mycloud # ← change this
jobs:
# ── Build: single job, Bun cross-compiles all targets ────────────────────
build:
name: Build (all platforms)
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Build for all targets
shell: bash
run: |
TARGETS=(
"bun-linux-x64 linux amd64"
"bun-linux-arm64 linux arm64"
"bun-darwin-arm64 darwin arm64"
"bun-darwin-x64 darwin amd64"
"bun-windows-x64 windows amd64"
)
for row in "${TARGETS[@]}"; do
read -r TARGET OS ARCH <<< "$row"
EXT=""
if [ "$OS" = "windows" ]; then EXT=".exe"; fi
bun node_modules/terrably/dist/src/cli/index.js build \
--name "$PROVIDER_NAME" \
--target "$TARGET"
mv "bin/terraform-provider-${PROVIDER_NAME}${EXT}" \
"bin/terraform-provider-${PROVIDER_NAME}_${OS}_${ARCH}${EXT}"
done
- uses: actions/upload-artifact@v4
with:
name: binaries
path: bin/terraform-provider-${{ env.PROVIDER_NAME }}_*
if-no-files-found: error
retention-days: 1
# ── Release: package, sign, publish ──────────────────────────────────────
release:
name: Publish to GitHub Releases
needs: build
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- uses: actions/download-artifact@v8
with:
name: binaries
path: bin/
- name: Import GPG key
run: |
echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
FP=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')
echo "GPG_FINGERPRINT=${FP}" >> "$GITHUB_ENV"
- name: Package, sign, and publish release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bun node_modules/terrably/dist/src/cli/index.js publish \
--release-version "${{ github.ref_name }}" \
--gpg-key "${GPG_FINGERPRINT}" \
--github-releasemacOS binaries built via cross-compilation are not ad-hoc codesigned (codesign requires running on macOS). If you need signed macOS binaries, add a macos-latest runner step for the Darwin targets, or sign them as a post-processing step with your Apple Developer certificate.
Required GitHub Actions secrets
| Secret | How to generate |
|---|---|
GPG_PRIVATE_KEY | gpg --armor --export-secret-keys your@email.com |
| (passphrase) | GPG key passphrase — omit the import step if the key has no passphrase |
GITHUB_TOKEN is injected automatically — no setup required.
To trigger a release: git tag v1.0.0 && git push origin v1.0.0
Platform support matrix
| Platform | Bun target | Runner |
|---|---|---|
Linux x86-64 (linux_amd64) | bun-linux-x64 | ubuntu-slim |
Linux arm64 (linux_arm64) | bun-linux-arm64 | — |
macOS arm64 (darwin_arm64) | bun-darwin-arm64 | macos-latest |
macOS x86-64 (darwin_amd64) | bun-darwin-x64 | macos-15-intel |
Windows x86-64 (windows_amd64) | bun-windows-x64 | windows-latest |
Windows arm64 (windows_arm64) | bun-windows-arm64 | — |
This list is not exhaustive, and might be out of date. See https://github.com/actions/runner-images#available-images for the latest runner images, and Bun's supported targets list for the targets supported by Bun.
Last updated on