diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml deleted file mode 100644 index 66116b7..0000000 --- a/.github/workflows/goreleaser.yaml +++ /dev/null @@ -1,112 +0,0 @@ -name: goreleaser - -on: - push: - tags: - - '*' - workflow_dispatch: - inputs: - frontend_branch: - description: 'Frontend branch to use (leave empty for latest stable release)' - required: false - default: '' - -jobs: - goreleaser: - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - ref: ${{ github.ref }} - - - name: Derive release suffix from tag (if it exists) - run: | - # Strip the 'refs/tags/' prefix - TAG_NAME=${GITHUB_REF#refs/tags/} - - # Extract suffix from tag name after the last '-' (e.g., 'dencun' from 'v1.0.0-dencun') - RELEASE_SUFFIX=${TAG_NAME##*-} - - # Check if the suffix is still a version pattern (e.g., 'v0.0.44'), in which case there's no suffix - if [[ $RELEASE_SUFFIX =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - RELEASE_SUFFIX="" - fi - - echo "RELEASE_SUFFIX=$RELEASE_SUFFIX" >> $GITHUB_ENV - - echo "Release suffix: $RELEASE_SUFFIX" - - - name: Set up Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version: '1.25.1' - - - name: Run apt-get update - run: sudo apt-get update - - - name: Install cross-compiler for linux/arm64 - run: sudo apt-get -y install gcc-aarch64-linux-gnu - - - name: Install make - run: sudo apt-get -y install make - - - name: Set frontend branch - run: | - # Use workflow input if provided, otherwise use latest stable release - if [ "${{ github.event.inputs.frontend_branch }}" != "" ]; then - echo "FRONTEND_BRANCH=${{ github.event.inputs.frontend_branch }}" >> $GITHUB_ENV - fi - # If FRONTEND_BRANCH is empty, Makefile will use latest stable release - - - name: Download and setup frontend - run: | - echo "Using frontend branch: $FRONTEND_BRANCH" - FRONTEND_BRANCH=$FRONTEND_BRANCH make setup-frontend - - - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - - - name: Set up Docker Context for Buildx - shell: bash - id: buildx-context - run: | - docker context create builders - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - with: - endpoint: builders - - - name: Login to DockerHub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Update GoReleaser config - run: | - cp .goreleaser.yaml .goreleaser.yaml.new - - # If we have a RELEASE_SUFFIX, update the goreleaser config to not set - # the release as the latest - if [[ -n "$RELEASE_SUFFIX" ]]; then - echo "release:" >> .goreleaser.yaml.new - echo " prerelease: true" >> .goreleaser.yaml.new - echo " make_latest: false" >> .goreleaser.yaml.new - fi - - - name: Run GoReleaser in Docker - run: | - docker run --rm \ - -v ${{ github.workspace }}:/workspace \ - -w /workspace \ - -e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ - -e DOCKER_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} \ - -e DOCKER_PASSWORD=${{ secrets.DOCKERHUB_TOKEN }} \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e RELEASE_SUFFIX=${{ env.RELEASE_SUFFIX }} \ - goreleaser/goreleaser-cross:v1.26.2 release --clean --config .goreleaser.yaml.new diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..9071ef2 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,156 @@ +name: release + +# Builds and publishes a release with an exact frontend release embedded. +# +# Triggered three ways: +# - repository_dispatch from ethpandaops/lab when it publishes a stable +# release: bumps the patch version, tags, and releases automatically. +# - workflow_dispatch: same auto-tagging, with an optional frontend_tag +# override (alpha frontend tags produce a suffixed prerelease). +# - tag push: the manual flow, embedding the latest stable frontend. + +on: + push: + tags: + - '*' + repository_dispatch: + types: [frontend-release] + workflow_dispatch: + inputs: + frontend_tag: + description: 'Frontend release tag to embed (empty = latest stable lab release)' + required: false + default: '' + +# Serialized so concurrent runs cannot compute the same next tag. +concurrency: + group: release + +jobs: + release: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.ref }} + + - name: Resolve frontend release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + FRONTEND_TAG="" + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + FRONTEND_TAG="${{ github.event.client_payload.frontend_tag }}" + elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + FRONTEND_TAG="${{ inputs.frontend_tag }}" + fi + + if [ -z "$FRONTEND_TAG" ]; then + FRONTEND_TAG=$(gh api repos/ethpandaops/lab/releases/latest --jq .tag_name) + fi + + if [ -z "$FRONTEND_TAG" ]; then + echo "Could not resolve a frontend release tag" >&2 + exit 1 + fi + + echo "Frontend release: $FRONTEND_TAG" + echo "FRONTEND_TAG=$FRONTEND_TAG" >> $GITHUB_ENV + + - name: Resolve release tag + run: | + if [ "${{ github.event_name }}" = "push" ]; then + TAG_NAME=${GITHUB_REF#refs/tags/} + + # Anything after the semver is the suffix (e.g. 'fusaka' from + # 'v1.3.45-fusaka'); a plain semver tag has none. + RELEASE_SUFFIX=$(echo "$TAG_NAME" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+-?//') + else + # Alpha frontend tags ('fusaka-v0.0.3') carry their name over as + # the release suffix; stable frontend tags ('v1.0.70') have none. + RELEASE_SUFFIX="" + if [[ ! $FRONTEND_TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + RELEASE_SUFFIX=$(echo "$FRONTEND_TAG" | sed -E 's/-v[0-9]+\.[0-9]+\.[0-9]+$//') + fi + + LATEST_TAG=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1) + if [ -z "$LATEST_TAG" ]; then + TAG_NAME="v0.0.1" + else + VERSION="${LATEST_TAG#v}" + IFS='.' read -r major minor patch <<< "$VERSION" + TAG_NAME="v${major}.${minor}.$((patch + 1))" + fi + + if [ -n "$RELEASE_SUFFIX" ]; then + TAG_NAME="${TAG_NAME}-${RELEASE_SUFFIX}" + fi + fi + + echo "Release tag: $TAG_NAME (suffix: ${RELEASE_SUFFIX:-none})" + echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV + echo "RELEASE_SUFFIX=$RELEASE_SUFFIX" >> $GITHUB_ENV + + # Tags pushed with GITHUB_TOKEN do not trigger the tag-push path of + # this workflow, so the dispatch paths never double-build. + - name: Create and push tag + if: github.event_name != 'push' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "$TAG_NAME" + git push origin "$TAG_NAME" + + - name: Set up Go + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version: '1.25.1' + + - name: Run apt-get update + run: sudo apt-get update + + - name: Install cross-compiler for linux/arm64 + run: sudo apt-get -y install gcc-aarch64-linux-gnu + + - name: Install make + run: sudo apt-get -y install make + + - name: Download and setup frontend + run: FRONTEND_TAG=$FRONTEND_TAG make setup-frontend + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + - name: Set up Docker Context for Buildx + shell: bash + id: buildx-context + run: | + docker context create builders + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + with: + endpoint: builders + + - name: Login to DockerHub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Run GoReleaser in Docker + run: | + docker run --rm \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace \ + -e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ + -e DOCKER_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} \ + -e DOCKER_PASSWORD=${{ secrets.DOCKERHUB_TOKEN }} \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e RELEASE_SUFFIX=${{ env.RELEASE_SUFFIX }} \ + -e FRONTEND_TAG=${{ env.FRONTEND_TAG }} \ + goreleaser/goreleaser-cross:v1.26.2 release --clean diff --git a/.gitignore b/.gitignore index 118623e..09a9c86 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ dist/ *.dll *.so *.dylib -.goreleaser.yaml.new # Test coverage *.out diff --git a/.goreleaser.yaml b/.goreleaser.yaml index bd4e203..a21aebe 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -20,6 +20,7 @@ builds: - -X github.com/ethpandaops/lab-backend/internal/version.Version={{.Tag}} - -X github.com/ethpandaops/lab-backend/internal/version.GitCommit={{.FullCommit}} - -X github.com/ethpandaops/lab-backend/internal/version.BuildDate={{.Date}} + - -X github.com/ethpandaops/lab-backend/internal/version.FrontendVersion={{ envOrDefault "FRONTEND_TAG" "" }} mod_timestamp: "{{ .CommitTimestamp }}" - id: linux-arm64 @@ -36,7 +37,14 @@ builds: - -X github.com/ethpandaops/lab-backend/internal/version.Version={{.Tag}} - -X github.com/ethpandaops/lab-backend/internal/version.GitCommit={{.FullCommit}} - -X github.com/ethpandaops/lab-backend/internal/version.BuildDate={{.Date}} + - -X github.com/ethpandaops/lab-backend/internal/version.FrontendVersion={{ envOrDefault "FRONTEND_TAG" "" }} mod_timestamp: "{{ .CommitTimestamp }}" +release: + prerelease: auto + make_latest: '{{ not .Prerelease }}' + header: | + Embedded frontend: [ethpandaops/lab {{ envOrDefault "FRONTEND_TAG" "unknown" }}](https://github.com/ethpandaops/lab/releases/tag/{{ envOrDefault "FRONTEND_TAG" "" }}) + checksum: name_template: 'checksums.txt' snapshot: @@ -71,6 +79,8 @@ dockers: - "--build-arg=VERSION={{.Tag}}" - "--build-arg=GIT_COMMIT={{.FullCommit}}" - "--build-arg=BUILD_DATE={{.Date}}" + - '--build-arg=FRONTEND_VERSION={{ envOrDefault "FRONTEND_TAG" "" }}' + - '--label=io.ethpandaops.frontend.version={{ envOrDefault "FRONTEND_TAG" "" }}' - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" @@ -97,6 +107,8 @@ dockers: - "--build-arg=VERSION={{.Tag}}" - "--build-arg=GIT_COMMIT={{.FullCommit}}" - "--build-arg=BUILD_DATE={{.Date}}" + - '--build-arg=FRONTEND_VERSION={{ envOrDefault "FRONTEND_TAG" "" }}' + - '--label=io.ethpandaops.frontend.version={{ envOrDefault "FRONTEND_TAG" "" }}' - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" diff --git a/Dockerfile b/Dockerfile index aab7aa2..ef2b6f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,13 +11,15 @@ COPY . . ARG VERSION=dev ARG GIT_COMMIT=dev ARG BUILD_DATE=unknown +ARG FRONTEND_VERSION= # Build the binary directly (frontend already included via goreleaser extra_files) RUN mkdir -p bin && \ go build -ldflags="-w -s \ -X github.com/ethpandaops/lab-backend/internal/version.Version=${VERSION} \ -X github.com/ethpandaops/lab-backend/internal/version.GitCommit=${GIT_COMMIT} \ - -X github.com/ethpandaops/lab-backend/internal/version.BuildDate=${BUILD_DATE}" \ + -X github.com/ethpandaops/lab-backend/internal/version.BuildDate=${BUILD_DATE} \ + -X github.com/ethpandaops/lab-backend/internal/version.FrontendVersion=${FRONTEND_VERSION}" \ -o bin/lab-backend ./cmd/server # Runtime stage diff --git a/Makefile b/Makefile index 9b8b0f1..c32fc9e 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ all: build # Frontend configuration FRONTEND_SOURCE ?= +FRONTEND_TAG ?= FRONTEND_BRANCH ?= FRONTEND_TARGET ?= web/frontend FRONTEND_VERSION_FILE ?= .tmp/frontend-version.txt @@ -57,7 +58,10 @@ setup-frontend: echo "dev" > $(FRONTEND_VERSION_FILE); \ printf "$(GREEN)✓ Copied $(FRONTEND_SOURCE)/dist -> $(FRONTEND_TARGET)$(RESET)\n"; \ else \ - if [ -n "$(FRONTEND_BRANCH)" ]; then \ + if [ -n "$(FRONTEND_TAG)" ]; then \ + printf "$(YELLOW)Frontend tag: $(FRONTEND_TAG)$(RESET)\n"; \ + RELEASE_TAG="$(FRONTEND_TAG)"; \ + elif [ -n "$(FRONTEND_BRANCH)" ]; then \ printf "$(YELLOW)Frontend branch: $(FRONTEND_BRANCH)$(RESET)\n"; \ RELEASE_TAG=$$(curl -s "https://api.github.com/repos/$(GITHUB_REPO)/releases" | \ grep -o '"tag_name": *"[^"]*"' | \ diff --git a/README.md b/README.md index 620655f..8ed89a8 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,14 @@ FRONTEND_BRANCH=develop make run **Environment Variables:** - `FRONTEND_SOURCE` - Path to local frontend source (uses `dist/` directory) +- `FRONTEND_TAG` - Download an exact frontend release (e.g., `v1.0.70`) - `FRONTEND_BRANCH` - Download from specific branch's latest release (e.g., `develop`) - `GITHUB_REPO` - GitHub repository for frontend releases (default: `ethpandaops/lab`) +## Releasing + +See [RELEASING.md](RELEASING.md). Stable releases are cut automatically when [lab](https://github.com/ethpandaops/lab) publishes a release. + ## Configuration Copy `config.example.yaml` to `config.yaml` and configure: diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..c1c6828 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,12 @@ +# Releasing + +Releases are normally cut automatically: when [lab](https://github.com/ethpandaops/lab) publishes a stable release, it sends a `frontend-release` dispatch to this repo. The release workflow then bumps the patch version, tags it, and runs goreleaser with that exact frontend release pinned. + +Manual options: + +- **Run the release workflow** (`gh workflow run release.yaml`): cuts the next patch release. Leave `frontend_tag` empty to embed the latest stable lab release, or set it to any lab release tag — including alpha tags like `fusaka-v0.0.3`, which produce a suffixed backend tag (`v1.3.45-fusaka`) published as a prerelease and tagged `fusaka-latest` on Docker Hub. +- **Push a tag** (`git tag v1.3.45 && git push origin v1.3.45`): the old flow still works and embeds the latest stable lab release. + +Every release records the embedded frontend version in its release notes, in the `io.ethpandaops.frontend.version` Docker label, and in the binary itself (`frontend_version` in the version endpoint output). + +Deploying the image is a separate manual bump of `image.tag` in the [platform](https://github.com/ethpandaops/platform) lab application values. diff --git a/internal/version/version.go b/internal/version/version.go index 7aa43bc..b739875 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -12,6 +12,9 @@ var ( Version = "dev" GitCommit = "unknown" BuildDate = "unknown" + // FrontendVersion is the embedded frontend release tag. Set via ldflags + // for release builds; dev builds fall back to .tmp/frontend-version.txt. + FrontendVersion = "" ) // Info contains version information. @@ -32,10 +35,15 @@ func Get() Info { } // GetWithFrontend returns version information including frontend version. -// It reads the frontend version from .tmp/frontend-version.txt if it exists. +// It prefers the build-time FrontendVersion and falls back to reading +// .tmp/frontend-version.txt for dev builds. func GetWithFrontend() Info { info := Get() - info.FrontendVersion = readFrontendVersion() + + info.FrontendVersion = FrontendVersion + if info.FrontendVersion == "" { + info.FrontendVersion = readFrontendVersion() + } return info } diff --git a/internal/version/version_test.go b/internal/version/version_test.go index 1c72a00..6089558 100644 --- a/internal/version/version_test.go +++ b/internal/version/version_test.go @@ -52,6 +52,23 @@ func TestGetWithFrontend(t *testing.T) { require.NoError(t, os.RemoveAll(".tmp")) }) + t.Run("prefers build-time frontend version over file", func(t *testing.T) { + original := FrontendVersion + FrontendVersion = "v9.9.9" + + defer func() { FrontendVersion = original }() + + require.NoError(t, os.MkdirAll(".tmp", 0o755)) + require.NoError(t, os.WriteFile(".tmp/frontend-version.txt", []byte("frontend-v2.5.0-test"), 0o644)) + + info := GetWithFrontend() + + assert.Equal(t, "v9.9.9", info.FrontendVersion) + + // Cleanup + require.NoError(t, os.RemoveAll(".tmp")) + }) + t.Run("handles missing frontend version file", func(t *testing.T) { // Ensure .tmp directory doesn't exist os.RemoveAll(".tmp")