From c690f74d75cb5dae7120916b08afe1201f05d5f1 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 27 May 2026 11:50:48 +0200 Subject: [PATCH] feat(ci): add release-binaries and release-os-package composite actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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 `, 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. --- .github/actions/release-binaries/action.yml | 144 +++++++++++++ .github/actions/release-os-package/action.yml | 189 ++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 .github/actions/release-binaries/action.yml create mode 100644 .github/actions/release-os-package/action.yml diff --git a/.github/actions/release-binaries/action.yml b/.github/actions/release-binaries/action.yml new file mode 100644 index 000000000..abbaa9538 --- /dev/null +++ b/.github/actions/release-binaries/action.yml @@ -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/* diff --git a/.github/actions/release-os-package/action.yml b/.github/actions/release-os-package/action.yml new file mode 100644 index 000000000..808764460 --- /dev/null +++ b/.github/actions/release-os-package/action.yml @@ -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 `). + +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 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 }}/*