From 556e0a70d5bcd629939b1f955c3fd3f94b03c18f Mon Sep 17 00:00:00 2001 From: Umputun Date: Fri, 22 May 2026 13:24:32 -0500 Subject: [PATCH] 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. --- .github/workflows/release.yml | 143 ++++++++++++++++++ .gitignore | 1 + .goreleaser.yml | 69 +++++++++ CLAUDE.md | 21 +++ Dockerfile.artifacts | 64 -------- Makefile | 28 ++-- scripts/cleanup-release-assets.sh | 12 ++ scripts/prepare-release-assets.sh | 61 ++++++++ .../getting-started/installation/index.md | 4 +- 9 files changed, 318 insertions(+), 85 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yml delete mode 100644 Dockerfile.artifacts create mode 100755 scripts/cleanup-release-assets.sh create mode 100755 scripts/prepare-release-assets.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ad7452fc --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore index cb5ea5d5..dc1c4940 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ debug.test .mongo remark42 /bin/ +/dist/ /backend/var/ /backend/app/var/ /backend/app/cmd/web/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..56286cb8 --- /dev/null +++ b/.goreleaser.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index bc210ee4..f02d67a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/Dockerfile.artifacts b/Dockerfile.artifacts deleted file mode 100644 index 924d3e05..00000000 --- a/Dockerfile.artifacts +++ /dev/null @@ -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"] diff --git a/Makefile b/Makefile index dc9920dc..f3d6e524 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/scripts/cleanup-release-assets.sh b/scripts/cleanup-release-assets.sh new file mode 100755 index 00000000..2bc8090e --- /dev/null +++ b/scripts/cleanup-release-assets.sh @@ -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 diff --git a/scripts/prepare-release-assets.sh b/scripts/prepare-release-assets.sh new file mode 100755 index 00000000..b12c727c --- /dev/null +++ b/scripts/prepare-release-assets.sh @@ -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" diff --git a/site/src/docs/getting-started/installation/index.md b/site/src/docs/getting-started/installation/index.md index b0e98866..a6b6557e 100644 --- a/site/src/docs/getting-started/installation/index.md +++ b/site/src/docs/getting-started/installation/index.md @@ -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= ARCH=`. This step will produce an executable `remark42` file with everything embedded +- to build a single binary for direct execution - `make OS= ARCH=`. This requires Go 1.25, Node 16+, PNPM 8, and Perl, builds frontend assets locally, and produces an executable `remark42` file with everything embedded