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 }}/*