chore(release): build binary artifacts with GoReleaser (#2070)

replace the Docker artifact build with GoReleaser config and a tag release workflow. Keep local artifact builds snapshot-only and clean generated frontend embed files after release runs.
This commit is contained in:
Umputun
2026-05-22 13:24:32 -05:00
committed by GitHub
parent 0e20861419
commit 556e0a70d5
9 changed files with 318 additions and 85 deletions
+143
View File
@@ -0,0 +1,143 @@
name: release
on:
push:
tags:
- "v*"
pull_request:
paths:
- ".github/workflows/release.yml"
- ".goreleaser.yml"
- "Makefile"
- "scripts/**"
- "backend/**"
- "frontend/**"
- "README.md"
- "LICENSE"
- "CLAUDE.md"
- "site/src/docs/getting-started/installation/index.md"
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: install go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: backend/go.sum
- name: install pnpm
uses: pnpm/action-setup@v6.0.4
with:
version: 8
run_install: false
- name: install node
uses: actions/setup-node@v6
with:
node-version: 16
cache: "pnpm"
cache-dependency-path: frontend/pnpm-lock.yaml
- name: test and build backend
run: |
go test -race -timeout=120s ./...
go build -race ./...
working-directory: backend/app
env:
TZ: "America/Chicago"
- name: test examples
run: |
go test -race ./...
go build -race ./...
working-directory: backend/_example/memory_store
env:
TZ: "America/Chicago"
- name: install frontend dependencies
run: pnpm install --frozen-lockfile
working-directory: frontend
env:
CI: "true"
- name: check frontend
run: |
pnpm lint
pnpm type-check
pnpm test -- --runInBand
working-directory: frontend/apps/remark42
env:
CI: "true"
- name: check goreleaser snapshot
if: github.event_name == 'pull_request'
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --snapshot --clean --skip=publish
env:
SKIP_PNPM_INSTALL: "true"
- name: clean generated release assets
if: always()
run: ./scripts/cleanup-release-assets.sh
release:
if: github.event_name == 'push'
needs: validate
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: install go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: backend/go.sum
- name: install pnpm
uses: pnpm/action-setup@v6.0.4
with:
version: 8
run_install: false
- name: install node
uses: actions/setup-node@v6
with:
node-version: 16
cache: "pnpm"
cache-dependency-path: frontend/pnpm-lock.yaml
- name: install frontend dependencies
run: pnpm install --frozen-lockfile
working-directory: frontend
env:
CI: "true"
- name: run goreleaser
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SKIP_PNPM_INSTALL: "true"
- name: clean generated release assets
if: always()
run: ./scripts/cleanup-release-assets.sh
+1
View File
@@ -15,6 +15,7 @@ debug.test
.mongo
remark42
/bin/
/dist/
/backend/var/
/backend/app/var/
/backend/app/cmd/web/
+69
View File
@@ -0,0 +1,69 @@
version: 2
project_name: remark42
git:
ignore_tags:
- backend/*
before:
hooks:
- ./scripts/prepare-release-assets.sh
builds:
- id: remark42
dir: backend
main: ./app
binary: "remark42.{{ .Os }}-{{ .Arch }}"
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- freebsd
- windows
goarch:
- amd64
- arm64
- "386"
- arm
goarm:
- "7"
ignore:
- goos: darwin
goarch: "386"
- goos: darwin
goarch: arm
- goos: freebsd
goarch: arm64
- goos: freebsd
goarch: "386"
- goos: freebsd
goarch: arm
- goos: windows
goarch: arm64
- goos: windows
goarch: "386"
- goos: windows
goarch: arm
ldflags:
- -s -w -X main.revision={{ .Tag }}-{{ .ShortCommit }}-{{ trimsuffix (replace (replace .CommitDate "-" "") ":" "") "Z" }}
archives:
- id: remark42
ids:
- remark42
name_template: "{{ .ProjectName }}.{{ .Os }}-{{ .Arch }}"
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
files:
- LICENSE
- README.md
release:
name_template: "Version {{ .Version }}"
mode: keep-existing
+21
View File
@@ -20,6 +20,27 @@
- **Dependency Updates**:
- When updating Go modules in `backend/`, also run `go mod tidy` (and `go mod vendor`) in `backend/_example/memory_store` to keep indirect deps in sync. The example module replaces `github.com/umputun/remark42/backend` with `../../` so stale indirect deps there will break the example build.
## Release Procedure
Remark42 uses two tags for each release:
- `vX.Y.Z` - product release tag used by GitHub releases, GoReleaser binary artifacts, and Docker image publishing.
- `backend/vX.Y.Z` - nested Go module tag for `github.com/umputun/remark42/backend`.
Release flow:
1. Create the GitHub release for `vX.Y.Z` with title `Version X.Y.Z`. The GitHub release must exist before the `vX.Y.Z` tag reaches the remote; `gh release create vX.Y.Z` satisfies this because it creates and pushes the tag.
2. The `vX.Y.Z` tag triggers GoReleaser, which builds and uploads binary artifacts to the existing release.
3. Create and push the matching backend module tag pointing at the same commit:
```bash
git fetch origin --tags
git tag backend/vX.Y.Z vX.Y.Z
git push origin backend/vX.Y.Z
```
GoReleaser must ignore `backend/*` tags in `.goreleaser.yml` so release notes and current-tag detection use only product tags. Docker image publishing stays separate and is handled by the existing Docker workflow.
For local artifact runs, install GoReleaser, Go 1.25, Node 16+, PNPM 8, and Perl, then use `make release`. The target runs a snapshot/no-publish GoReleaser build, leaves local artifacts and metadata in `dist/`, and cleans generated frontend embed files after GoReleaser exits. Do not run raw `goreleaser release` for local artifacts unless you also run `./scripts/cleanup-release-assets.sh` afterward.
## Code Style
- **Backend**: Formatting with golangci-lint, strict error handling
- **Frontend**: TypeScript with ESLint, Stylelint and Prettier
-64
View File
@@ -1,64 +0,0 @@
FROM node:16-alpine AS frontend-deps
ENV CI=true
WORKDIR /srv/frontend
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml ./frontend/pnpm-workspace.yaml /srv/frontend/
COPY ./frontend/apps/remark42/package.json /srv/frontend/apps/remark42/package.json
RUN apk add --no-cache --update git && npm i -g pnpm@8
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store/v3 pnpm i
FROM frontend-deps AS build-frontend
ENV NODE_ENV=production
ENV CI=true
WORKDIR /srv/frontend/apps/remark42/
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
RUN pnpm build
FROM umputun/baseimage:buildgo-v1.17.0 AS build-backend
ARG GITHUB_TOKEN
ARG GITHUB_REF
ARG GITHUB_SHA
WORKDIR /build/backend
ADD backend /build/backend
ADD README.md /build/
ADD LICENSE /build/
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /build/backend/app/cmd/web/
RUN find /build/backend/app/cmd/web/ -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;
RUN \
version=$("/script/version.sh") && echo "version=${version}" && \
GOOS=linux GOARCH=amd64 go build -o remark42.linux-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=linux GOARCH=386 go build -o remark42.linux-386 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=linux GOARCH=arm go build -o remark42.linux-arm -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=linux GOARCH=arm64 go build -o remark42.linux-arm64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=windows GOARCH=amd64 go build -o remark42.windows-amd64.exe -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=darwin GOARCH=amd64 go build -o remark42.darwin-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=darwin GOARCH=arm64 go build -o remark42.darwin-arm64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=freebsd GOARCH=amd64 go build -o remark42.freebsd-amd64 -ldflags "-X main.revision=${version} -s -w" ./app
RUN \
apk add --no-cache --update zip && \
cp ../LICENSE ./LICENSE && cp ../README.md ./README.md && \
tar cvzf remark42.linux-amd64.tar.gz remark42.linux-amd64 LICENSE README.md && \
tar cvzf remark42.linux-386.tar.gz remark42.linux-386 LICENSE README.md && \
tar cvzf remark42.linux-arm.tar.gz remark42.linux-arm LICENSE README.md && \
tar cvzf remark42.linux-arm64.tar.gz remark42.linux-arm64 LICENSE README.md && \
tar cvzf remark42.darwin-amd64.tar.gz remark42.darwin-amd64 LICENSE README.md && \
tar cvzf remark42.darwin-arm64.tar.gz remark42.darwin-arm64 LICENSE README.md && \
tar cvzf remark42.freebsd-amd64.tar.gz remark42.freebsd-amd64 LICENSE README.md && \
zip remark42.windows-amd64.zip remark42.windows-amd64.exe LICENSE README.md
FROM alpine
COPY --from=build-backend /build/backend/remark42.* /artifacts/
RUN ls -la /artifacts/*
CMD ["sleep", "100"]
+9 -19
View File
@@ -2,13 +2,13 @@ OS=linux
ARCH=amd64
GITHUB_REF=$(shell git rev-parse --symbolic-full-name HEAD)
GITHUB_SHA=$(shell git rev-parse --short HEAD)
CLEANUP_RELEASE_ASSETS=$(CURDIR)/scripts/cleanup-release-assets.sh
bin:
docker build -f Dockerfile.artifacts -t remark42.bin .
- @docker rm -f remark42.bin 2>/dev/null || exit 0
docker run -d --name=remark42.bin remark42.bin
docker cp remark42.bin:/artifacts/remark42.$(OS)-$(ARCH) remark42
docker rm -f remark42.bin
@set -e; \
./scripts/prepare-release-assets.sh; \
trap '$(CLEANUP_RELEASE_ASSETS)' EXIT; \
cd backend && CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -o ../remark42 -ldflags "-X main.revision=$(GITHUB_REF)-$(GITHUB_SHA) -s -w" ./app
docker:
DOCKER_BUILDKIT=1 docker build -t umputun/remark42 -t ghcr.io/umputun/remark42 --build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) \
@@ -21,19 +21,9 @@ dockerx:
-t ghcr.io/umputun/remark42:master -t umputun/remark42:master .
release:
docker build -f Dockerfile.artifacts --no-cache --pull --build-arg CI=true \
--build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) -t remark42.bin .
- @docker rm -f remark42.bin 2>/dev/null || exit 0
- @mkdir -p bin
docker run -d --name=remark42.bin remark42.bin
docker cp remark42.bin:/artifacts/remark42.linux-amd64.tar.gz bin/remark42.linux-amd64.tar.gz
docker cp remark42.bin:/artifacts/remark42.linux-386.tar.gz bin/remark42.linux-386.tar.gz
docker cp remark42.bin:/artifacts/remark42.linux-arm64.tar.gz bin/remark42.linux-arm64.tar.gz
docker cp remark42.bin:/artifacts/remark42.darwin-amd64.tar.gz bin/remark42.darwin-amd64.tar.gz
docker cp remark42.bin:/artifacts/remark42.darwin-arm64.tar.gz bin/remark42.darwin-arm64.tar.gz
docker cp remark42.bin:/artifacts/remark42.freebsd-amd64.tar.gz bin/remark42.freebsd-amd64.tar.gz
docker cp remark42.bin:/artifacts/remark42.windows-amd64.zip bin/remark42.windows-amd64.zip
docker rm -f remark42.bin
@set -e; \
trap '$(CLEANUP_RELEASE_ASSETS)' EXIT; \
goreleaser release --snapshot --clean --skip=publish
race_test:
cd backend/app && go test -race -timeout=60s -count 1 ./...
@@ -52,4 +42,4 @@ rundev:
e2e:
docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
.PHONY: bin backend
.PHONY: bin docker dockerx release race_test backend frontend rundev e2e
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
PREPARED_MARKER="$ROOT/backend/app/cmd/web/.release-assets-prepared"
if [[ ! -e "$PREPARED_MARKER" ]]; then
exit 0
fi
git -C "$ROOT" checkout -- backend/app/cmd/web
git -C "$ROOT" clean -fdX backend/app/cmd/web frontend/apps/remark42/public >/dev/null
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
FRONTEND_DIR="$ROOT/frontend"
APP_DIR="$FRONTEND_DIR/apps/remark42"
PUBLIC_DIR="$APP_DIR/public"
EMBED_DIR="$ROOT/backend/app/cmd/web"
PREPARED_MARKER="$EMBED_DIR/.release-assets-prepared"
for cmd in git pnpm perl; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "error: $cmd is required to build release assets" >&2
exit 1
fi
done
if [[ -n $(git -C "$ROOT" status --porcelain -- backend/app/cmd/web) ]]; then
echo "error: backend/app/cmd/web has uncommitted changes" >&2
exit 1
fi
cleanup_on_error() {
git -C "$ROOT" checkout -- backend/app/cmd/web
git -C "$ROOT" clean -fdX backend/app/cmd/web frontend/apps/remark42/public >/dev/null
}
cleanup_on_exit() {
rc=$?
if [[ "$rc" -ne 0 ]]; then
cleanup_on_error
fi
}
trap cleanup_on_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
rm -rf "$PUBLIC_DIR" "$EMBED_DIR"
mkdir -p "$EMBED_DIR"
(
cd "$FRONTEND_DIR"
if [[ "${SKIP_PNPM_INSTALL:-}" != "true" ]]; then
CI=true pnpm install --frozen-lockfile
fi
cd "$APP_DIR"
CI=true pnpm build
)
cp -R "$PUBLIC_DIR"/. "$EMBED_DIR"/
find "$EMBED_DIR" -type f \( -name '*.html' -o -name '*.js' -o -name '*.mjs' \) \
-exec perl -pi -e 's|\{\% REMARK_URL \%\}|http://127.0.0.1:8080|g' {} +
if grep -R "{% REMARK_URL %}" "$EMBED_DIR" >/dev/null; then
echo "error: unreplaced REMARK_URL placeholder in $EMBED_DIR" >&2
exit 1
fi
touch "$PREPARED_MARKER"
@@ -29,7 +29,7 @@ _This is the recommended way to run Remark42_
- download [archive for the stable release](https://github.com/umputun/remark42/releases)
- unpack with `gunzip` (Linux, macOS) or with `zip` (Windows)
- run as `remark42.{os}-{arch} server {parameters...}`, i.e., `remark42.linux-amd64 server --secret=12345 --url=http://127.0.0.1:8080`
- alternatively compile from the sources - `make OS=[linux|darwin|windows] ARCH=[amd64,386,arm64,arm]`
- alternatively compile from the sources - `make OS=[linux|darwin|windows] ARCH=[amd64,386,arm64,arm]`. Source binary builds require Go 1.25, Node 16+, PNPM 8, and Perl because the frontend assets are built and embedded locally.
#### Installation as a systemd Service
@@ -129,4 +129,4 @@ To verify if Remark42 has been properly installed, check a demo page at `${REMAR
### Build from the source
- to build Docker container - `make docker`. This command will produce container `ghcr.io/umputun/remark42`
- to build a single binary for direct execution - `make OS=<linux|windows|darwin> ARCH=<amd64|386>`. This step will produce an executable `remark42` file with everything embedded
- to build a single binary for direct execution - `make OS=<linux|windows|darwin> ARCH=<amd64|386>`. This requires Go 1.25, Node 16+, PNPM 8, and Perl, builds frontend assets locally, and produces an executable `remark42` file with everything embedded