Skip to content

chore: bump egress version to v2.3.12 (#409) #33

chore: bump egress version to v2.3.12 (#409)

chore: bump egress version to v2.3.12 (#409) #33

name: Release egress binary
# Every merge to main that changes egress-relevant code auto-bumps the
# semver patch in common/version.go, tags the commit, and publishes a
# GitHub Release with linux/amd64 + linux/arm64 egress binaries and
# their sha256 sums. The lantern-cloud egress provisioner (cmd/api/vps/
# cloudinit_egress.go) then fetches these at VM boot.
#
# Path filter: we deliberately DON'T trigger on freddie-, clientcore-, UI-,
# netstate-, or docs-only changes. Those have no effect on the egress binary.
#
# Author-driven major/minor bumps still work: if common/version.go already
# differs from the latest release tag, the workflow respects that version
# instead of incrementing the patch. So a PR that wants v2.4.0 just edits
# version.go; no flags needed.
on:
push:
branches: [main]
paths:
- 'egress/**'
- 'common/**'
- 'go.mod'
- 'go.sum'
- '.github/workflows/release-egress.yml'
# Manual trigger. The push trigger above is the normal path, but it leaves no
# way to retry: on 2026-08-03 it silently did not fire on a merge that touched
# this very file, and with no dispatch there was no way to re-run it or to cut a
# release on demand. It also means the bump-PR path added in #377 cannot be
# exercised without manufacturing a qualifying merge.
#
# Dispatch runs ignore the path filter, so this always runs the version job. It
# is safe to run repeatedly: if version.go already matches the latest release it
# opens (or refreshes) the bump PR and releases nothing, and if version.go is
# ahead it publishes that version. Neither path force-pushes to main.
workflow_dispatch:
# Serialize so two back-to-back merges can't race on the version bump.
# cancel-in-progress: false — we want every merge to produce a release,
# not to let a later merge skip the earlier one's artifacts.
concurrency:
group: release-egress
cancel-in-progress: false
permissions:
contents: write # create tags, push the bump branch, publish releases
pull-requests: write # open the version-bump PR (main is protected)
jobs:
version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.pick.outputs.version }}
bumped: ${{ steps.pick.outputs.bumped }}
release: ${{ steps.pick.outputs.release }}
target_sha: ${{ steps.pick.outputs.target_sha }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Use a token that can push back to main. GITHUB_TOKEN works
# because permissions.contents=write above.
token: ${{ secrets.GITHUB_TOKEN }}
- name: Determine next version
id: pick
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
current=$(grep -oP '(?<=Version = ")v[^"]+' common/version.go)
if [ -z "$current" ]; then
echo "ERROR: could not parse Version from common/version.go"
exit 1
fi
# Validate the parsed value is strict semver before we use it
# anywhere shell-interpolated. Belt-and-suspenders: version.go is
# editable by PR authors, so we never trust its content verbatim.
if ! printf '%s' "$current" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: Version \"$current\" is not strict semver vMAJOR.MINOR.PATCH"
exit 1
fi
# Latest NON-draft NON-prerelease release only. We never want a
# stale/WIP release to masquerade as the current shipping
# version and derail our bump math.
last_tag=$(gh release list \
--exclude-drafts --exclude-pre-releases --limit 1 \
--json tagName --jq '.[0].tagName // ""')
# Validate last_tag the same way we validated current. Reject
# anything weird rather than silently picking a bad baseline.
if [ -n "$last_tag" ] && \
! printf '%s' "$last_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: latest release tag \"$last_tag\" is not strict semver — refusing to continue"
exit 1
fi
bumped=false
if [ -z "$last_tag" ]; then
# First-ever release: ship current as-is, no bump.
echo "no prior release found; releasing current version $current as the initial release"
next="$current"
elif [ "$current" != "$last_tag" ]; then
# PR author manually bumped version.go. Respect their value.
echo "manual bump detected: $last_tag -> $current"
next="$current"
else
# Auto patch-bump. current looks like "v2.3.0".
IFS='.' read -r maj min patch <<< "${current#v}"
next="v${maj}.${min}.$((patch + 1))"
echo "auto patch-bump: $current -> $next"
sed -i "s|const Version = \"[^\"]*\"|const Version = \"$next\"|" common/version.go
grep -n "Version = " common/version.go
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add common/version.go
# main is a protected branch ("Changes must be made through a pull
# request"), so pushing the bump directly fails with GH006 and takes
# the whole release down with it. That is exactly what happened on
# every egress-relevant merge until this change: the workflow bumped
# v2.3.3 -> v2.3.4, got rejected, and no release was ever published.
#
# So open a PR instead. Deliberately NO [skip ci] here: merging this
# PR must re-trigger the workflow, and on that run version.go will
# differ from the latest release tag, so the "manual bump detected"
# branch above takes over and publishes without needing to push
# anything. One human click per release, and the tag always points at
# a commit that is genuinely on main.
branch="chore/egress-release-$next"
git commit -m "chore: bump egress version to $next"
# --force so a re-run of a failed release refreshes the branch rather
# than dying on "already exists".
git push --force origin "HEAD:refs/heads/$branch"
# Reuse an open PR if one is already out for this version; gh pr
# create would otherwise fail the job on a re-run.
if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[0].number // ""')" ]; then
gh pr create \
--base main --head "$branch" \
--title "chore: bump egress version to $next" \
--body "Automated egress version bump opened by release-egress.yml. main is protected, so the workflow cannot push this commit directly. Merging this PR re-triggers the release workflow, which will then see version.go ahead of the latest release tag and publish $next."
fi
echo "opened/updated bump PR on $branch; release will publish when it merges"
bumped=true
fi
# Pin downstream jobs to the exact commit we just operated on —
# either the trigger SHA (no bump) or the bump commit SHA (bump).
# Otherwise checking out `ref: main` in build/release would race
# any commit that lands on main after us. concurrency serializes
# OUR workflow runs, but doesn't block direct pushes from humans.
target_sha=$(git rev-parse HEAD)
# release=false when this run only opened a bump PR: version.go is not
# on main yet, so tagging target_sha would tag a commit off-main and
# publish a binary whose common.Version disagrees with the tag.
if [ "$bumped" = "true" ]; then release=false; else release=true; fi
echo "version=$next" >> "$GITHUB_OUTPUT"
echo "bumped=$bumped" >> "$GITHUB_OUTPUT"
echo "release=$release" >> "$GITHUB_OUTPUT"
echo "target_sha=$target_sha" >> "$GITHUB_OUTPUT"
build:
needs: version
# Skipped on runs that only opened a bump PR; the merge of that PR triggers
# the run that actually builds and releases.
if: needs.version.outputs.release == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
goos: [linux]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v4
with:
# Pin to the exact SHA the version job operated on. Using
# `ref: main` would pick up any commits landed after our
# version step finished, silently building a different tree
# than we just tagged.
ref: ${{ needs.version.outputs.target_sha }}
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build egress
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: '0'
run: |
set -euo pipefail
out="egress-${GOOS}-${GOARCH}"
# -s -w strip debug/symbol tables; the binary is self-contained
# (CGO_ENABLED=0 → static, portable across Ubuntu/Debian/CentOS).
go build -trimpath -ldflags="-s -w" -o "$out" ./egress/cmd/sing-box/
sha256sum "$out" > "${out}.sha256"
ls -lh "$out" "${out}.sha256"
- uses: actions/upload-artifact@v4
with:
name: egress-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
egress-${{ matrix.goos }}-${{ matrix.goarch }}
egress-${{ matrix.goos }}-${{ matrix.goarch }}.sha256
retention-days: 7
if-no-files-found: error
release:
needs: [version, build]
if: needs.version.outputs.release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Same pinning rationale as build: check out the exact SHA
# we're about to release, not wherever main happens to be now.
ref: ${{ needs.version.outputs.target_sha }}
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Pull the version output into an env var rather than
# interpolating ${{ ... }} directly into a run: command. The
# version string goes through shell, and even though we validated
# it in the `version` job, routing via env is the safer pattern.
VERSION: ${{ needs.version.outputs.version }}
TARGET_SHA: ${{ needs.version.outputs.target_sha }}
run: |
set -euo pipefail
ls -lh dist/
# --generate-notes fills the body with the commit list since the
# previous release tag. --target is the SHA the version job
# decided on — which is what build just compiled from.
gh release create "$VERSION" \
--title "$VERSION" \
--generate-notes \
--target "$TARGET_SHA" \
dist/egress-linux-amd64 \
dist/egress-linux-amd64.sha256 \
dist/egress-linux-arm64 \
dist/egress-linux-arm64.sha256