Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/release-go-mirror.yml
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<YY>.<M>.<D>` (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
Expand Down
58 changes: 58 additions & 0 deletions scripts/backfill-semver-tags.sh
Original file line number Diff line number Diff line change
@@ -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