name: release on: push: tags: - 'v*' workflow_dispatch: inputs: tag: description: 'Release tag (e.g. v1.2.3). If empty, uses current ref name.' required: false type: string build_targets: description: 'Space-separated GOOS/GOARCH targets. Leave empty to use Makefile defaults.' required: false type: string channel_variants: description: 'Space-separated package variants. Leave empty to use full none and all single-channel variants.' required: false type: string permissions: contents: write jobs: prepare-release: runs-on: ubuntu-latest outputs: raw_tag: ${{ steps.prepare.outputs.raw_tag }} version: ${{ steps.prepare.outputs.version }} targets_json: ${{ steps.prepare.outputs.targets_json }} channel_variants: ${{ steps.prepare.outputs.channel_variants }} steps: - name: Checkout uses: actions/checkout@v4 - name: Resolve release inputs id: prepare shell: bash run: | set -euo pipefail if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then raw_tag="${{ inputs.tag }}" elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then raw_tag="${GITHUB_REF_NAME}" else raw_tag="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" fi build_targets="${{ inputs.build_targets || '' }}" channel_variants="${{ inputs.channel_variants || '' }}" if [ -z "$build_targets" ]; then build_targets="linux/amd64 linux/arm64 linux/riscv64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64" fi if [ -z "$channel_variants" ]; then channel_variants="full none telegram discord feishu maixcam qq dingtalk whatsapp" fi version="${raw_tag#v}" targets_json="$(python3 -c 'import json, sys; print(json.dumps(sys.argv[1].split()))' "$build_targets")" echo "raw_tag=$raw_tag" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT" echo "targets_json=$targets_json" >> "$GITHUB_OUTPUT" echo "channel_variants=$channel_variants" >> "$GITHUB_OUTPUT" echo "Build version: $version (from $raw_tag)" echo "Targets: $build_targets" echo "Variants: $channel_variants" build-webui: needs: prepare-release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Ensure Node.js 20 shell: bash run: | set -euo pipefail if command -v node >/dev/null 2>&1; then node_major="$(node -p 'process.versions.node.split(".")[0]')" else node_major="" fi if [ "$node_major" != "20" ]; then arch="$(dpkg --print-architecture)" case "$arch" in amd64) node_arch="x64" ;; arm64) node_arch="arm64" ;; *) echo "Unsupported runner architecture for Node install: $arch" >&2 exit 1 ;; esac curl -fsSL "https://nodejs.org/dist/v20.19.5/node-v20.19.5-linux-${node_arch}.tar.xz" -o /tmp/node.tar.xz sudo rm -rf /usr/local/lib/nodejs sudo mkdir -p /usr/local/lib/nodejs sudo tar -xJf /tmp/node.tar.xz -C /usr/local/lib/nodejs echo "/usr/local/lib/nodejs/node-v20.19.5-linux-${node_arch}/bin" >> "$GITHUB_PATH" fi node --version npm --version - name: Install WebUI dependencies shell: bash run: | set -euo pipefail cd webui if [ -f package-lock.json ]; then npm ci else npm install fi - name: Build WebUI shell: bash run: | set -euo pipefail make build-webui - name: Upload WebUI dist uses: actions/upload-artifact@v4 with: name: webui-dist path: webui/dist if-no-files-found: error prepare-go-cache: needs: prepare-release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install Go shell: bash run: | set -euo pipefail go_version="$(awk '/^go / {print $2; exit}' go.mod)" arch="$(dpkg --print-architecture)" case "$arch" in amd64) go_arch="amd64" ;; arm64) go_arch="arm64" ;; *) echo "Unsupported runner architecture for Go install: $arch" >&2 exit 1 ;; esac curl -fsSL "https://go.dev/dl/go${go_version}.linux-${go_arch}.tar.gz" -o /tmp/go.tgz sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf /tmp/go.tgz echo "/usr/local/go/bin" >> "$GITHUB_PATH" /usr/local/go/bin/go version - name: Warm Go module cache shell: bash run: | set -euo pipefail mkdir -p /tmp/go-cache go mod download GOMODCACHE="$(go env GOMODCACHE)" tar -C "$GOMODCACHE" -czf /tmp/go-cache/gomodcache.tar.gz . - name: Upload Go module cache uses: actions/upload-artifact@v4 with: name: go-mod-cache path: /tmp/go-cache/gomodcache.tar.gz if-no-files-found: error build-and-package: needs: - prepare-release - build-webui - prepare-go-cache runs-on: ubuntu-latest strategy: fail-fast: false matrix: target: ${{ fromJson(needs.prepare-release.outputs.targets_json) }} steps: - name: Checkout uses: actions/checkout@v4 - name: Install Go shell: bash run: | set -euo pipefail go_version="$(awk '/^go / {print $2; exit}' go.mod)" arch="$(dpkg --print-architecture)" case "$arch" in amd64) go_arch="amd64" ;; arm64) go_arch="arm64" ;; *) echo "Unsupported runner architecture for Go install: $arch" >&2 exit 1 ;; esac curl -fsSL "https://go.dev/dl/go${go_version}.linux-${go_arch}.tar.gz" -o /tmp/go.tgz sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf /tmp/go.tgz echo "/usr/local/go/bin" >> "$GITHUB_PATH" /usr/local/go/bin/go version - name: Download WebUI dist uses: actions/download-artifact@v4 with: name: webui-dist path: webui/dist - name: Download Go module cache uses: actions/download-artifact@v4 with: name: go-mod-cache path: /tmp/go-cache - name: Restore Go module cache shell: bash run: | set -euo pipefail GOMODCACHE="$(go env GOMODCACHE)" mkdir -p "$GOMODCACHE" tar -C "$GOMODCACHE" -xzf /tmp/go-cache/gomodcache.tar.gz - name: Install packaging tools run: | sudo apt-get update sudo apt-get install -y zip - name: Resolve matrix target id: target shell: bash run: | set -euo pipefail target="${{ matrix.target }}" goos="${target%/*}" goarch="${target#*/}" echo "goos=$goos" >> "$GITHUB_OUTPUT" echo "goarch=$goarch" >> "$GITHUB_OUTPUT" echo "artifact_name=release-$goos-$goarch" >> "$GITHUB_OUTPUT" - name: Build and package matrix artifacts shell: bash run: | set -euo pipefail make clean make package-all \ VERSION="${{ needs.prepare-release.outputs.version }}" \ BUILD_TARGETS="${{ matrix.target }}" \ CHANNEL_PACKAGE_VARIANTS="${{ needs.prepare-release.outputs.channel_variants }}" \ SKIP_WEBUI_BUILD=1 rm -f build/checksums.txt - name: Upload matrix artifacts uses: actions/upload-artifact@v4 with: name: ${{ steps.target.outputs.artifact_name }} path: | build/*.tar.gz build/*.zip if-no-files-found: error publish-release: needs: - prepare-release - build-and-package runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' steps: - name: Download packaged artifacts uses: actions/download-artifact@v4 with: pattern: release-* merge-multiple: true path: build - name: List downloaded artifacts run: find build -maxdepth 2 -type f | sort - name: Generate checksums shell: bash run: | set -euo pipefail cd build if command -v sha256sum >/dev/null 2>&1; then sha256sum *.tar.gz *.zip > checksums.txt else shasum -a 256 *.tar.gz *.zip > checksums.txt fi cat checksums.txt - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.prepare-release.outputs.raw_tag }} name: ${{ needs.prepare-release.outputs.raw_tag }} generate_release_notes: true files: | build/*.tar.gz build/*.zip build/checksums.txt