diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..be6ae55 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + env: + GOPRIVATE: git.veillax.com + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # Go fetches the other sharks modules straight from this server. This is harmless + # for public repositories and is what makes private ones work. + - name: Let Go fetch modules from this server + run: git config --global url."https://x-access-token:${{ secrets.GITHUB_TOKEN }}@git.veillax.com/".insteadOf "https://git.veillax.com/" + + - name: Formatting + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "These files need gofmt:" + echo "$unformatted" + exit 1 + fi + + - name: Vet + run: go vet ./... + + - name: Test + run: go test $(go list ./... | grep -v /e2e) + + # Catches platform-specific breakage before a release is tagged. + - name: Every release platform compiles + run: | + for t in linux/amd64 windows/amd64; do + for c in shark; do + echo "building $c for $t" + GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/$c + done + done diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..3e5a2fb --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release + +# Push a tag such as v0.2.0 to build and publish a release: +# git tag -a v0.2.0 -m "shark-expose v0.2.0" && git push origin v0.2.0 +on: + push: + tags: ['v*'] + +jobs: + release: + runs-on: ubuntu-latest + env: + GOPRIVATE: git.veillax.com + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # release notes come from the history since the previous tag + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Let Go fetch modules from this server + run: git config --global url."https://x-access-token:${{ secrets.GITHUB_TOKEN }}@git.veillax.com/".insteadOf "https://git.veillax.com/" + + - name: Test + run: | + go vet ./... + go test $(go list ./... | grep -v /e2e) + + - name: Build release archives + run: sh deploy/build.sh "${GITHUB_REF_NAME}" + + # Optional secret RELEASE_TOKEN: an access token that can write releases, for + # servers where the automatic job token can't. + - name: Publish the release + env: + TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + run: sh deploy/publish-release.sh "${GITHUB_REF_NAME}" diff --git a/deploy/build.sh b/deploy/build.sh new file mode 100644 index 0000000..6ecfbcf --- /dev/null +++ b/deploy/build.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Builds shark-expose for every release platform and packs the archives into dist/. +# +# deploy/build.sh [version] version defaults to `git describe` +# +# Output: +# dist/shark-expose___.tar.gz or .zip +# dist/SHA256SUMS +# +# Releases are published for linux/amd64 and windows/amd64 only. Set TARGETS="linux/arm64 darwin/arm64" +# (any GOOS/GOARCH pair Go supports) to build something else by hand. +set -eu +NAME=shark-expose +CMDS="shark" +cd "$(dirname "$0")/.." +VERSION=${1:-$(git describe --tags --always 2>/dev/null || echo dev)} +TARGETS=${TARGETS:-"linux/amd64 windows/amd64"} + +rm -rf dist +mkdir -p dist/stage +for t in $TARGETS; do + os=${t%/*} + arch=${t#*/} + ext="" + [ "$os" = windows ] && ext=".exe" + pkg="${NAME}_${VERSION}_${os}_${arch}" + stage="dist/stage/$pkg" + mkdir -p "$stage" + for c in $CMDS; do + echo "building $c $VERSION for $os/$arch" + CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -trimpath -ldflags "-s -w -X main.version=$VERSION" -o "$stage/$c$ext" "./cmd/$c" + done + for f in LICENSE README.md; do + [ -f "$f" ] && cp "$f" "$stage/" + done + if [ "$os" = windows ] && command -v zip >/dev/null 2>&1; then + (cd dist/stage && zip -qr "../$pkg.zip" "$pkg") + else + tar -C dist/stage -czf "dist/$pkg.tar.gz" "$pkg" + fi +done +rm -rf dist/stage +(cd dist && sha256sum $(ls | grep -E '\.(tar\.gz|zip)$') > SHA256SUMS) +echo "done: $(ls dist | grep -cE '\.(tar\.gz|zip)$') archives in dist/" diff --git a/deploy/publish-release.sh b/deploy/publish-release.sh new file mode 100644 index 0000000..6fd8e7f --- /dev/null +++ b/deploy/publish-release.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# Creates (or updates) the Gitea release for a tag and uploads what deploy/build.sh made. +# +# TOKEN= deploy/publish-release.sh v0.2.0 +# +# In Gitea Actions GITHUB_SERVER_URL and GITHUB_REPOSITORY are already set. Safe to +# re-run: an existing release is reused and assets with the same name are replaced. +set -eu +TAG=${1:?usage: publish-release.sh } +: "${TOKEN:?set TOKEN to a Gitea access token that can write releases}" +SERVER=${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-https://git.veillax.com}} +REPO=${GITHUB_REPOSITORY:-Veillax/shark-expose} +API="$SERVER/api/v1/repos/$REPO" +AUTH="Authorization: token $TOKEN" + +# a JSON helper without needing jq: any python will do +PY=python3 +python3 -c 'pass' > /dev/null 2>&1 || PY=python +json() { "$PY" -c "$1"; } + +# release notes: the commit subjects since the previous tag +prev=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true) +if [ -n "$prev" ]; then + notes=$(git log --pretty='format:- %s' "$prev..$TAG") +else + notes=$(git log --pretty='format:- %s' "$TAG") +fi +prerelease=false +case "$TAG" in *-*) prerelease=true ;; esac + +body=$(NOTES="$notes" TAG="$TAG" PRE="$prerelease" json ' +import json, os +print(json.dumps({"tag_name": os.environ["TAG"], "name": os.environ["TAG"], "body": os.environ["NOTES"], + "draft": False, "prerelease": os.environ["PRE"] == "true"}))') + +# reuse the release when a failed job is re-run +id=$(curl -fsS -H "$AUTH" "$API/releases/tags/$TAG" 2>/dev/null | json 'import sys, json; print(json.load(sys.stdin)["id"])' 2>/dev/null || true) +if [ -z "$id" ]; then + echo "creating release $TAG" + id=$(curl -fsS -X POST -H "$AUTH" -H 'Content-Type: application/json' -d "$body" "$API/releases" | json 'import sys, json; print(json.load(sys.stdin)["id"])') +else + echo "release $TAG already exists (id $id): replacing its files" +fi + +# the ids of the release's assets with a given name (a failed upload can leave a partial one behind) +asset_ids() { + curl -fsS -H "$AUTH" "$API/releases/$id/assets" | "$PY" -c 'import sys, json +for a in json.load(sys.stdin): + if a["name"] == sys.argv[1]: + print(a["id"])' "$1" +} + +for f in dist/*.tar.gz dist/*.zip dist/SHA256SUMS; do + [ -f "$f" ] || continue + name=$(basename "$f") + n=1 + while :; do + for aid in $(asset_ids "$name"); do + curl -fsS -X DELETE -H "$AUTH" "$API/releases/$id/assets/$aid" > /dev/null + done + echo "uploading $name (attempt $n)" + if curl -fsS --max-time 1200 -X POST -H "$AUTH" -F "attachment=@$f" "$API/releases/$id/assets?name=$name" > /dev/null; then + break + fi + # large uploads can hit a proxy timeout (HTTP 502); try again + [ "$n" -lt 5 ] || { echo "giving up on $name after $n attempts" >&2; exit 1; } + n=$((n + 1)) + sleep $((n * 10)) + done +done +echo "published $SERVER/$REPO/releases/tag/$TAG"