diff --git a/.github/workflows/release-go-mirror.yml b/.github/workflows/release-go-mirror.yml new file mode 100644 index 00000000..d45255c5 --- /dev/null +++ b/.github/workflows/release-go-mirror.yml @@ -0,0 +1,71 @@ +name: Go module mirror tag + +# Background +# +# libXray uses CalVer tags vYY.M.D (e.g. v26.3.27 = 2026-03-27). Per the Go +# modules spec, modules with major version >= 2 must encode the major in their +# import path (github.com/xtls/libxray/vN). Bumping the import path every year +# would force every Go consumer to rewrite imports each January, so this +# repository keeps its module path bare and instead publishes a SemVer-shaped +# mirror tag on the same commit: +# +# CalVer v26.3.27 → Go-import v1.260327.0 +# +# Pattern matches xray-core's own scheme (v1.YYMMDD.N) and keeps Go consumers +# on a stable major. The CalVer tag remains the human-readable canonical +# release; the v1.* tag is purely for `go get`. +# +# This workflow listens for CalVer pushes and creates the matching v1.* tag. + +on: + push: + tags: + - 'v[0-9]*.[0-9]*.[0-9]*' + +jobs: + mirror: + name: Mirror CalVer tag to v1.YYMMDD.N + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout (full history + tags) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Compute and push mirror tag + env: + GH_REF: ${{ github.ref }} + run: | + set -euo pipefail + CAL="${GH_REF#refs/tags/}" + # Skip if this push is itself a v1.* tag (avoid recursion). + case "$CAL" in + v1.*) echo "skip: $CAL is already a Go-import tag"; exit 0 ;; + esac + + # Parse vYY.M.D. Reject anything that is not three numeric parts + # in plausible CalVer ranges (YY >= 20, M 1-12, D 1-31). + IFS=. read -r Y M D <<<"${CAL#v}" + if ! [[ "$Y" =~ ^[0-9]+$ && "$M" =~ ^[0-9]+$ && "$D" =~ ^[0-9]+$ ]]; then + echo "skip: $CAL is not numeric vYY.M.D"; exit 0 + fi + if [ "$Y" -lt 20 ] || [ "$M" -lt 1 ] || [ "$M" -gt 12 ] || [ "$D" -lt 1 ] || [ "$D" -gt 31 ]; then + echo "skip: $CAL is outside CalVer range (legacy semver tag?)"; exit 0 + fi + + MMDD=$(printf '%02d%02d' "$M" "$D") + BASE="v1.${Y}${MMDD}" + PATCH=0 + while git rev-parse -q --verify "refs/tags/${BASE}.${PATCH}" >/dev/null; do + PATCH=$((PATCH+1)) + done + SEMVER="${BASE}.${PATCH}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$SEMVER" "$CAL" -m "Go-modules mirror of $CAL" + git push origin "$SEMVER" + echo "mapped $CAL → $SEMVER" diff --git a/README.md b/README.md index 2039f847..102cb5c3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,28 @@ This is a wrapper around [Xray-core](https://github.com/XTLS/Xray-core) to impro 2. This repository does not guarantee API stability, you need to adapt it yourself. 3. This repository is only compatible with the latest release of Xray-core. +# Versioning + +Releases use CalVer in the form `v..` (e.g. `v26.3.27` = 2026-03-27). +Because Go modules require any module with major version `>= 2` to encode the +major in its import path, every CalVer release is mirrored onto a Go-friendly +SemVer tag on the same commit: + +| CalVer tag | Go-import tag | +|------------|---------------| +| `v26.3.27` | `v1.260327.0` | + +Go consumers should pin against the SemVer mirror: + +```shell +go get github.com/xtls/libxray@v1.260327.0 +``` + +The mirror tag is created automatically by +[`.github/workflows/release-go-mirror.yml`](./.github/workflows/release-go-mirror.yml) +on every CalVer push. Existing CalVer tags can be backfilled with +[`scripts/backfill-semver-tags.sh`](./scripts/backfill-semver-tags.sh). + # Features ## build diff --git a/scripts/backfill-semver-tags.sh b/scripts/backfill-semver-tags.sh new file mode 100755 index 00000000..a6ddd091 --- /dev/null +++ b/scripts/backfill-semver-tags.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Backfill Go-modules mirror tags (v1.YYMMDD.N) for every historical CalVer +# tag (vYY.M.D, YY >= 20). Run once after release-go-mirror.yml lands; from +# then on the workflow handles new tags automatically. +# +# Usage: +# ./scripts/backfill-semver-tags.sh # local — creates tags only +# ./scripts/backfill-semver-tags.sh --push # also push them to origin +# +# Idempotent: skips any v1.YYMMDD.N that already exists. + +set -euo pipefail + +PUSH=false +if [ "${1:-}" = "--push" ]; then + PUSH=true +fi + +git fetch --tags --quiet + +mapped=0 +skipped=0 + +while IFS= read -r CAL; do + [ -z "$CAL" ] && continue + case "$CAL" in + v1.*) skipped=$((skipped+1)); continue ;; + esac + + IFS=. read -r Y M D <<<"${CAL#v}" + if ! [[ "$Y" =~ ^[0-9]+$ && "$M" =~ ^[0-9]+$ && "$D" =~ ^[0-9]+$ ]]; then + skipped=$((skipped+1)); continue + fi + if [ "$Y" -lt 20 ] || [ "$M" -lt 1 ] || [ "$M" -gt 12 ] || [ "$D" -lt 1 ] || [ "$D" -gt 31 ]; then + skipped=$((skipped+1)); continue + fi + + MMDD=$(printf '%02d%02d' "$M" "$D") + BASE="v1.${Y}${MMDD}" + PATCH=0 + while git rev-parse -q --verify "refs/tags/${BASE}.${PATCH}" >/dev/null; do + PATCH=$((PATCH+1)) + done + SEMVER="${BASE}.${PATCH}" + + COMMIT=$(git rev-list -n1 "$CAL") + git tag -a "$SEMVER" "$COMMIT" -m "Go-modules mirror of $CAL" + echo "mapped $CAL → $SEMVER ($COMMIT)" + mapped=$((mapped+1)) +done < <(git tag --list 'v*.*.*' | sort -V) + +echo +echo "summary: mapped=$mapped skipped=$skipped" + +if $PUSH && [ "$mapped" -gt 0 ]; then + echo "pushing v1.* tags to origin..." + git push origin --tags +fi