From 2f965166ba55b62f7ae5b338aa84ea071a86de60 Mon Sep 17 00:00:00 2001 From: mixeme Date: Wed, 8 Jul 2026 08:32:07 +0300 Subject: [PATCH] ci: add GitHub and Codeberg release build workflows Add tag-triggered CI that builds and publishes the linux/amd64, linux/arm64, and windows/amd64 release binaries on both forges. - scripts/ci-build-release.sh: shared, Docker-free build+package for all three targets (Windows cross-compiled via MinGW), reused by both workflows so the build logic lives in one place. - .github/workflows/release.yml: GitHub Actions, publishes via softprops/action-gh-release using the built-in token. - .forgejo/workflows/release.yml: Forgejo Actions for Codeberg, publishes via forgejo-release using a RELEASE_TOKEN secret. - .gitattributes: force LF on workflow YAML so bash run: blocks don't break on Linux runners. - docs/DEVELOPMENT.md: document the tag -> release flow and token setup. Co-Authored-By: Claude Opus 4.8 --- .forgejo/workflows/release.yml | 90 +++++++++++++++++++++++++++++ .gitattributes | 5 ++ .github/workflows/release.yml | 93 ++++++++++++++++++++++++++++++ docs/DEVELOPMENT.md | 37 ++++++++++++ scripts/ci-build-release.sh | 100 +++++++++++++++++++++++++++++++++ 5 files changed, 325 insertions(+) create mode 100644 .forgejo/workflows/release.yml create mode 100644 .github/workflows/release.yml create mode 100755 scripts/ci-build-release.sh diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..e4b6ded --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +# Forgejo Actions workflow for Codeberg. It mirrors .github/workflows/release.yml +# and reuses the same scripts/ci-build-release.sh, so the actual build/package +# commands live in exactly one place. Codeberg has no Windows runners, but the +# Windows binary is cross-compiled with MinGW-w64 from the Linux job, so a single +# golang:1.22-bookworm container produces all three artifacts. +# +# Publishing needs a token that can write releases. Add a repository secret named +# RELEASE_TOKEN (a Codeberg access token with the "write:repository" scope) under +# Settings -> Actions -> Secrets. Without it the build still runs; only the +# upload step is skipped. +on: + push: + tags: + - "v*" + workflow_dispatch: + +jobs: + release: + runs-on: docker + container: + image: golang:1.22-bookworm + steps: + - name: Checkout + uses: https://code.forgejo.org/actions/checkout@v4 + + - name: Install cross toolchain + # Same package list as the repo Dockerfile / GitHub workflow: native gcc + # plus X11/GL headers, the aarch64 cross compiler with arm64 runtime + # libs, MinGW-w64 for the Windows GUI binary, and zip for packaging. + run: | + dpkg --add-architecture arm64 + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates \ + gcc \ + libc6-dev \ + gcc-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + linux-libc-dev-arm64-cross \ + gcc-mingw-w64-x86-64 \ + binutils-mingw-w64-x86-64 \ + pkg-config \ + libgl1-mesa-dev \ + xorg-dev \ + libgl1-mesa-dev:arm64 \ + libx11-dev:arm64 \ + libxcursor-dev:arm64 \ + libxrandr-dev:arm64 \ + libxinerama-dev:arm64 \ + libxi-dev:arm64 \ + libxxf86vm-dev:arm64 \ + zip + rm -rf /var/lib/apt/lists/* + + - name: Derive version + # On a tag push, strip the leading "v" so artifact names and the injected + # app version match the release tag. + id: version + run: | + if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then + echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + fi + + - name: Build and package + env: + VERSION: ${{ steps.version.outputs.value }} + run: | + chmod +x scripts/ci-build-release.sh + scripts/ci-build-release.sh + + - name: Collect release files + # forgejo-release uploads every file in a single directory, so gather the + # archives into one flat folder. + run: | + mkdir -p dist/release + cp dist/linux/*.tar.gz dist/windows/*.zip dist/release/ + + - name: Publish release + if: ${{ startsWith(github.ref, 'refs/tags/') }} + uses: https://code.forgejo.org/actions/forgejo-release@v2 + with: + direction: upload + url: https://codeberg.org + repo: ${{ github.repository }} + tag: ${{ github.ref_name }} + release-dir: dist/release + token: ${{ secrets.RELEASE_TOKEN }} + override: true diff --git a/.gitattributes b/.gitattributes index d454146..012941c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,8 @@ # Keep shell scripts LF even on Windows checkouts so bash on Linux hosts # doesn't choke on trailing CRs (e.g. "set: pipefail: invalid parameter name"). *.sh text eol=lf + +# CI workflow YAML embeds shell in `run:` blocks that Linux runners execute with +# bash, so keep these LF for the same reason as the shell scripts above. +.github/workflows/*.yml text eol=lf +.forgejo/workflows/*.yml text eol=lf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f7ce473 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,93 @@ +name: Release + +# Build the Linux (amd64/arm64) and Windows (amd64) binaries whenever a version +# tag is pushed, then attach the packaged archives to a GitHub Release. +# +# Everything runs inside golang:1.22-bookworm — the same base image as the +# repo Dockerfile — so the CGO/Fyne toolchain matches the local release builds. +# The Windows binary is cross-compiled with MinGW-w64 from the same Linux job, +# which is why no windows-latest runner is needed. +on: + push: + tags: + - "v*" + # Allow a manual run (from the Actions tab) to smoke-test the build without a + # tag. Manual runs build the artifacts but do not publish a release. + workflow_dispatch: + +permissions: + contents: write # required to create the release and upload assets + +jobs: + release: + runs-on: ubuntu-latest + container: + image: golang:1.22-bookworm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install cross toolchain + # Mirrors the package list in the repo Dockerfile: native gcc + X11/GL + # headers for amd64, the aarch64 cross compiler with arm64 runtime libs, + # and the MinGW-w64 toolchain for the Windows GUI binary. zip packages + # the Windows archive. + run: | + dpkg --add-architecture arm64 + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates \ + gcc \ + libc6-dev \ + gcc-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + linux-libc-dev-arm64-cross \ + gcc-mingw-w64-x86-64 \ + binutils-mingw-w64-x86-64 \ + pkg-config \ + libgl1-mesa-dev \ + xorg-dev \ + libgl1-mesa-dev:arm64 \ + libx11-dev:arm64 \ + libxcursor-dev:arm64 \ + libxrandr-dev:arm64 \ + libxinerama-dev:arm64 \ + libxi-dev:arm64 \ + libxxf86vm-dev:arm64 \ + zip + rm -rf /var/lib/apt/lists/* + + - name: Derive version + # For a tag push, use the tag without its leading "v" so the artifact + # names and the injected app version match the release. Otherwise fall + # back to the version in source (handled by the build script). + id: version + run: | + ref="${GITHUB_REF_NAME:-}" + if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then + echo "value=${ref#v}" >> "$GITHUB_OUTPUT" + fi + + - name: Build and package + env: + VERSION: ${{ steps.version.outputs.value }} + run: | + chmod +x scripts/ci-build-release.sh + scripts/ci-build-release.sh + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: gosentry-release + path: | + dist/linux/*.tar.gz + dist/windows/*.zip + + - name: Publish release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: | + dist/linux/*.tar.gz + dist/windows/*.zip + generate_release_notes: true diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d99bbf0..1195475 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -123,6 +123,43 @@ dist/linux/gosentry-0.9.0-linux-arm64 dist/windows/gosentry-0.9.0-windows-amd64.exe ``` +### Automated release builds (CI) + +Tagged releases are built automatically on both GitHub and Codeberg: + +- `.github/workflows/release.yml` — GitHub Actions. +- `.forgejo/workflows/release.yml` — Forgejo Actions (Codeberg). + +Both run inside `golang:1.22-bookworm` (the same base image as the +[Dockerfile](../Dockerfile)), install the cross toolchain, and call +`scripts/ci-build-release.sh`, which builds and packages all three artifacts: + +```text +dist/linux/gosentry--linux-amd64.tar.gz +dist/linux/gosentry--linux-arm64.tar.gz +dist/windows/gosentry--windows-amd64.zip +``` + +The Windows binary is cross-compiled with MinGW-w64 from the Linux job, so no +Windows runner is required. Each archive contains the executable plus `README.md` +and `CHANGELOG.md`, matching the local `package-*` scripts. + +To cut a release, bump `src/app/version.go` and push a matching `v` tag: + +```bash +git tag v0.11.5 +git push origin v0.11.5 # and to the Codeberg remote +``` + +The workflow strips the leading `v` from the tag and injects it as the version, +so the tag must match `version.go`. Pushing the tag triggers the build and +attaches the archives to a release on that forge. `workflow_dispatch` also allows +a manual, publish-free build to smoke-test the pipeline. + +Codeberg publishing needs a repository secret named `RELEASE_TOKEN` (a Codeberg +access token with the `write:repository` scope) under +**Settings → Actions → Secrets**. GitHub uses the built-in `GITHUB_TOKEN`. + ## Run From Source Windows: diff --git a/scripts/ci-build-release.sh b/scripts/ci-build-release.sh new file mode 100755 index 0000000..7ce0dfa --- /dev/null +++ b/scripts/ci-build-release.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build and package every release artifact on a Linux host that already has the +# cross toolchain installed (native gcc + X11/OpenGL headers, the aarch64 cross +# compiler, and the MinGW-w64 toolchain for the Windows GUI binary). This is the +# non-Docker counterpart to scripts/build-release-linux.sh: the CI workflows in +# .github/ and .forgejo/ install those packages directly on the runner and then +# call this script, so the exact build/package commands live in one place and do +# not drift between the two forges. +# +# The build flags mirror the other scripts intentionally: -trimpath strips local +# paths, -s -w drops symbol/debug tables to shrink the binaries, -H=windowsgui +# suppresses the console window on Windows, and -X injects the version so the +# GUI and artifact names agree. + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +cd "$repo_root" + +# VERSION can be provided by CI (for a tagged release the workflow passes the tag +# without its leading "v"). Fall back to the source of truth in version.go so the +# script also works for a plain local invocation. +version="${VERSION:-$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/app/version.go | tr -d '\r')}" +version="${version:-0.0.0-dev}" +ldflags="-s -w -X gitea.mixdep.ru/mix/gosentry/src/app.Version=${version}" + +echo "Building GoSentry ${version} release artifacts" +mkdir -p dist/linux dist/windows + +# --- Linux amd64 ----------------------------------------------------------- +echo "==> linux/amd64" +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ + go build -buildvcs=false -trimpath -ldflags "$ldflags" \ + -o "dist/linux/gosentry-${version}-linux-amd64" ./cmd/gosentry + +# --- Linux arm64 (cross compiled) ------------------------------------------ +echo "==> linux/arm64" +CC=aarch64-linux-gnu-gcc \ +CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \ +CGO_CFLAGS="--sysroot=/ -I/usr/include/aarch64-linux-gnu" \ +CGO_LDFLAGS="--sysroot=/ -L/usr/lib/aarch64-linux-gnu" \ +PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig \ + go build -buildvcs=false -trimpath -ldflags "$ldflags" \ + -o "dist/linux/gosentry-${version}-linux-arm64" ./cmd/gosentry + +# --- Windows amd64 (cross compiled with MinGW) ----------------------------- +echo "==> windows/amd64" +# windres embeds the .ico into the PE resource so Explorer/taskbar show the icon. +# The .syso is suffixed windows_amd64, so Go only links it into the Windows build +# and ignores it for the Linux targets above. +x86_64-w64-mingw32-windres -O coff \ + -o cmd/gosentry/rsrc_windows_amd64.syso packaging/windows/gosentry.rc +CC=x86_64-w64-mingw32-gcc \ +CGO_ENABLED=1 GOOS=windows GOARCH=amd64 \ + go build -buildvcs=false -trimpath -ldflags "-H=windowsgui ${ldflags}" \ + -o "dist/windows/gosentry-${version}-windows-amd64.exe" ./cmd/gosentry + +# --- Package --------------------------------------------------------------- +# Each archive holds the executable plus the top-level README and CHANGELOG, +# flattened to the archive root so a user can extract straight into any folder. +# This matches the layout produced by package-linux.sh / package-windows.bat. +package_linux() { + local arch="$1" + local binary="dist/linux/gosentry-${version}-linux-${arch}" + local tarball="dist/linux/gosentry-${version}-linux-${arch}.tar.gz" + local staging="dist/linux/_staging-${arch}" + + rm -rf "$staging" + mkdir -p "$staging" + cp "$binary" "$staging/gosentry" + cp README.md "$staging/README.md" + cp docs/CHANGELOG.md "$staging/CHANGELOG.md" + tar -czf "$tarball" -C "$staging" . + rm -rf "$staging" + echo "Packaged $tarball" +} + +package_windows() { + local binary="dist/windows/gosentry-${version}-windows-amd64.exe" + local zipfile="gosentry-${version}-windows-amd64.zip" + local staging="dist/windows/_staging-amd64" + + rm -rf "$staging" + mkdir -p "$staging" + cp "$binary" "$staging/gosentry.exe" + cp README.md "$staging/README.md" + cp docs/CHANGELOG.md "$staging/CHANGELOG.md" + # -j flattens: files land at the zip root with no staging path prefix. + ( cd "$staging" && zip -j -q "../${zipfile}" ./* ) + rm -rf "$staging" + echo "Packaged dist/windows/${zipfile}" +} + +package_linux amd64 +package_linux arm64 +package_windows + +echo "Release artifacts:" +find dist/linux dist/windows -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) -print