feat(ci): add release-binaries and release-os-package composite actions

Two reusable composite actions wrap the CI side of the release pipeline:

- release-binaries: setup-go, install mage + upx, cache xgo, invoke
  `mage release:build <project>` from build/, GPG-sign the zip bundles,
  upload to S3, store binaries and zips as workflow artifacts.

- release-os-package: download a binaries artifact, install mage,
  `mage release:prepare-nfpm-config <project> <arch>`, stage the binary,
  nfpm pack (with rpm signing inline and archlinux signing after), upload
  to S3, store the package as an artifact.

Both actions are parameterized on project name, output paths, artifact
names, S3 target, and GPG/S3 secrets — adding a third Go binary to the
monorepo just means defining its project in build/magefile.go and adding
a four-line call site in release.yml.
This commit is contained in:
kolaente 2026-05-27 11:50:48 +02:00 committed by kolaente
parent 8313d032ea
commit c690f74d75
2 changed files with 333 additions and 0 deletions

View File

@ -0,0 +1,144 @@
name: Release Go binaries
description: >
Cross-compile a Go binary from this monorepo through the centralized build/
magefile (xgo + upx + sha256 + zip), GPG-sign the per-target zip bundles,
upload them to S3, and store the binaries and zip bundles as workflow
artifacts. Any project-specific pre-build steps (downloading frontend dist,
generating config.yml.sample) belong in the calling workflow — this action
assumes the working tree is ready to compile.
inputs:
project:
description: 'Project name passed to `mage release:build` (e.g., vikunja, veans).'
required: true
release-version:
description: 'RELEASE_VERSION env value — usually the raw `git describe` output.'
required: true
xgo-out-name:
description: 'XGO_OUT_NAME env value — basename xgo prefixes onto every binary (e.g., vikunja-v1.2.3, veans-unstable).'
required: true
output-directory:
description: 'Where the project writes dist/ (e.g., "." for vikunja, "veans" for veans). Used for signing, S3 upload, and artifact paths.'
required: true
xgo-cache-key:
description: 'Primary cache key for /home/runner/.xgo-cache.'
required: true
s3-target-path:
description: 'S3 target path for the zip bundles (e.g., /vikunja/v1.2.3 or /veans/unstable).'
required: true
artifact-binaries-name:
description: 'Name of the upload-artifact entry for the raw binaries under dist/binaries/.'
required: true
artifact-zips-name:
description: 'Name of the upload-artifact entry for the zip bundles under dist/zip/.'
required: true
upload-zips-as-artifact:
description: '"true" to also upload the zip bundles as a workflow artifact (typically only on tags).'
required: false
default: 'false'
gpg-key-id:
description: 'Long key ID GPG should sign with.'
required: true
gpg-passphrase:
required: true
gpg-sign-key:
description: 'ASCII-armored GPG private key.'
required: true
s3-access-key-id:
required: true
s3-secret-access-key:
required: true
s3-endpoint:
required: true
s3-bucket:
required: true
s3-region:
required: true
runs:
using: composite
steps:
- uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6
with:
go-version: stable
- name: Install mage
# build/ is its own module — install a fresh mage so it picks up
# build/magefile.go on the fly.
shell: bash
run: go install github.com/magefile/mage@v1.17.2
- name: Install upx
shell: bash
run: |
wget https://github.com/upx/upx/releases/download/v5.0.0/upx-5.0.0-amd64_linux.tar.xz
echo 'b32abf118d721358a50f1aa60eacdbf3298df379c431c3a86f139173ab8289a1 upx-5.0.0-amd64_linux.tar.xz' > upx-5.0.0-amd64_linux.tar.xz.sha256
sha256sum -c upx-5.0.0-amd64_linux.tar.xz.sha256
tar xf upx-5.0.0-amd64_linux.tar.xz
mv upx-5.0.0-amd64_linux/upx /usr/local/bin
- name: Setup xgo cache
uses: useblacksmith/cache@71c7c918062ba3861252d84b07fe5ab2a6b467a6 # v5
with:
path: /home/runner/.xgo-cache
key: ${{ inputs.xgo-cache-key }}
restore-keys: |
${{ runner.os }}-go-
- name: Build and release
shell: bash
working-directory: build
env:
RELEASE_VERSION: ${{ inputs.release-version }}
XGO_OUT_NAME: ${{ inputs.xgo-out-name }}
PROJECT: ${{ inputs.project }}
run: |
export PATH=$PATH:$GOPATH/bin
mage release:build "$PROJECT"
- name: GPG setup
uses: kolaente/action-gpg@main
with:
gpg-passphrase: ${{ inputs.gpg-passphrase }}
gpg-sign-key: ${{ inputs.gpg-sign-key }}
- name: Sign zip bundles
shell: bash
working-directory: ${{ inputs.output-directory }}
env:
GPG_KEY_ID: ${{ inputs.gpg-key-id }}
GPG_PASSPHRASE: ${{ inputs.gpg-passphrase }}
run: |
echo "=== Signing files ==="
ls -hal dist/zip/*
for file in dist/zip/*; do
gpg -v --default-key "$GPG_KEY_ID" -b --batch --yes \
--passphrase "$GPG_PASSPHRASE" \
--pinentry-mode loopback \
--sign "$file"
done
- name: Upload zips to S3
uses: kolaente/s3-action@main
with:
s3-access-key-id: ${{ inputs.s3-access-key-id }}
s3-secret-access-key: ${{ inputs.s3-secret-access-key }}
s3-endpoint: ${{ inputs.s3-endpoint }}
s3-bucket: ${{ inputs.s3-bucket }}
s3-region: ${{ inputs.s3-region }}
target-path: ${{ inputs.s3-target-path }}
files: ${{ inputs.output-directory }}/dist/zip/*
strip-path-prefix: ${{ inputs.output-directory }}/dist/zip/
- name: Store binaries
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: ${{ inputs.artifact-binaries-name }}
path: ${{ inputs.output-directory }}/dist/binaries/*
- name: Store zip bundles
if: inputs.upload-zips-as-artifact == 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: ${{ inputs.artifact-zips-name }}
path: ${{ inputs.output-directory }}/dist/zip/*

View File

@ -0,0 +1,189 @@
name: Release OS package
description: >
Build a single deb/rpm/apk/archlinux package for the given project + arch
via nfpm, optionally GPG-sign it (archlinux is signed inline; rpm is signed
by nfpm itself), upload it to S3, and store it as a workflow artifact.
Templating of the project's nfpm.yaml happens via the centralized build/
magefile (`mage release:prepare-nfpm-config <project> <arch>`).
inputs:
project:
description: 'Project name passed to `mage release:prepare-nfpm-config` (e.g., vikunja, veans).'
required: true
release-version:
description: 'RELEASE_VERSION env value — the same version that ended up in the binaries artifact.'
required: true
nfpm-bin-path:
description: 'NFPM_BIN_PATH override for the <binlocation> substitution. Leave empty to use the project default.'
required: false
default: ''
packager:
description: 'nfpm packager: rpm | deb | apk | archlinux.'
required: true
nfpm-arch:
description: 'nfpm arch field (amd64 | arm64 | arm7 | 386).'
required: true
pkg-arch:
description: 'Package-format arch used in the output filename (x86_64 | aarch64 | armv7).'
required: true
binaries-artifact-name:
description: 'Name of the binaries artifact to download (e.g., vikunja_bins, veans_bins).'
required: true
binaries-download-path:
description: 'Where to extract the binaries artifact (relative to workspace root).'
required: true
binary-glob:
description: 'Glob (under binaries-download-path) that matches the single binary to package.'
required: true
staged-binary-path:
description: 'Final path of the binary the nfpm config will read (relative to workspace root).'
required: true
nfpm-config-path:
description: 'Path to the project''s nfpm.yaml (relative to workspace root). Passed to nfpm via `--config`.'
required: true
package-output-dir:
description: 'Directory (relative to workspace root) where nfpm writes the resulting package.'
required: true
package-filename:
description: 'Filename of the produced package (e.g., vikunja-v1.2.3-x86_64.deb).'
required: true
artifact-name:
description: 'Name of the upload-artifact entry for the produced package.'
required: true
s3-target-path:
description: 'S3 target path for the package (e.g., /vikunja/v1.2.3 or /veans/unstable).'
required: true
gpg-key-id:
description: 'Long key ID GPG should sign with (used for archlinux signing).'
required: true
gpg-passphrase:
required: true
gpg-sign-key:
description: 'ASCII-armored GPG private key.'
required: true
s3-access-key-id:
required: true
s3-secret-access-key:
required: true
s3-endpoint:
required: true
s3-bucket:
required: true
s3-region:
required: true
runs:
using: composite
steps:
- name: Download project binaries
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: ${{ inputs.binaries-artifact-name }}
path: ${{ inputs.binaries-download-path }}
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: stable
- name: Install mage
shell: bash
run: go install github.com/magefile/mage@v1.17.2
- name: Write GPG key for nfpm
if: inputs.packager == 'rpm'
shell: bash
env:
RELEASE_GPG_SIGN_KEY: ${{ inputs.gpg-sign-key }}
run: printf '%s' "$RELEASE_GPG_SIGN_KEY" > /tmp/nfpm-signing-key.gpg
- name: GPG setup for archlinux signing
if: inputs.packager == 'archlinux'
uses: kolaente/action-gpg@main
with:
gpg-passphrase: ${{ inputs.gpg-passphrase }}
gpg-sign-key: ${{ inputs.gpg-sign-key }}
- name: Prepare nfpm config and stage binary
shell: bash
working-directory: build
env:
RELEASE_VERSION: ${{ inputs.release-version }}
NFPM_ARCH: ${{ inputs.nfpm-arch }}
NFPM_BIN_PATH: ${{ inputs.nfpm-bin-path }}
PROJECT: ${{ inputs.project }}
run: |
export PATH=$PATH:$GOPATH/bin
mage release:prepare-nfpm-config "$PROJECT" "$NFPM_ARCH"
- name: Stage binary
shell: bash
env:
BINARY_GLOB: ${{ inputs.binary-glob }}
DOWNLOAD_DIR: ${{ inputs.binaries-download-path }}
STAGED: ${{ inputs.staged-binary-path }}
run: |
# Resolve the single matching binary and mv it into place. Using
# extglob would be tidier, but a tiny shell loop keeps this readable.
matched=()
for f in $DOWNLOAD_DIR/$BINARY_GLOB; do
[ -e "$f" ] || continue
matched+=("$f")
done
if [ ${#matched[@]} -ne 1 ]; then
echo "::error::expected exactly 1 binary matching '$DOWNLOAD_DIR/$BINARY_GLOB', found ${#matched[@]}"
ls -la "$DOWNLOAD_DIR" || true
exit 1
fi
mkdir -p "$(dirname "$STAGED")"
mv "${matched[0]}" "$STAGED"
chmod +x "$STAGED"
- name: Ensure package output dir exists
shell: bash
env:
DIR: ${{ inputs.package-output-dir }}
run: mkdir -p "$DIR"
- name: Create package
uses: kolaente/action-gh-nfpm@master
with:
packager: ${{ inputs.packager }}
target: ${{ inputs.package-output-dir }}/${{ inputs.package-filename }}
config: ${{ inputs.nfpm-config-path }}
env:
NFPM_GPG_KEY_FILE: ${{ inputs.packager == 'rpm' && '/tmp/nfpm-signing-key.gpg' || '' }}
NFPM_PASSPHRASE: ${{ inputs.packager == 'rpm' && inputs.gpg-passphrase || '' }}
- name: Sign archlinux package
if: inputs.packager == 'archlinux'
shell: bash
env:
GPG_KEY_ID: ${{ inputs.gpg-key-id }}
GPG_PASSPHRASE: ${{ inputs.gpg-passphrase }}
PKG: ${{ inputs.package-output-dir }}/${{ inputs.package-filename }}
run: |
gpg --default-key "$GPG_KEY_ID" \
--batch --yes \
--passphrase "$GPG_PASSPHRASE" \
--pinentry-mode loopback \
--detach-sign \
"$PKG"
- name: Upload to S3
uses: kolaente/s3-action@main
with:
s3-access-key-id: ${{ inputs.s3-access-key-id }}
s3-secret-access-key: ${{ inputs.s3-secret-access-key }}
s3-endpoint: ${{ inputs.s3-endpoint }}
s3-bucket: ${{ inputs.s3-bucket }}
s3-region: ${{ inputs.s3-region }}
target-path: ${{ inputs.s3-target-path }}
files: ${{ inputs.package-output-dir }}/*
strip-path-prefix: ${{ inputs.package-output-dir }}/
- name: Store OS package
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: ${{ inputs.artifact-name }}
path: ${{ inputs.package-output-dir }}/*