diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..f2ccfc0 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Test + run: npm test + + # The app is static files, so a broken manifest or service worker only shows up on a phone. + - name: Manifest and scripts parse + run: | + node -e "JSON.parse(require('fs').readFileSync('public/manifest.webmanifest','utf8'))" + for f in public/*.js; do node --check "$f"; done diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..2d312ac --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +# Push a tag such as v0.2.0 to publish public/ as a release archive that can be +# unpacked into SANDBAR_APP_DIR or hosted on any static site: +# git tag -a v0.2.0 -m "remora v0.2.0" && git push origin v0.2.0 +on: + push: + tags: ['v*'] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # release notes come from the history since the previous tag + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Test + run: npm test + + - name: Build release archive + run: | + mkdir -p dist + tar -C public -czf "dist/remora_${GITHUB_REF_NAME}.tar.gz" . + (cd public && zip -qr "../dist/remora_${GITHUB_REF_NAME}.zip" .) + (cd dist && sha256sum remora_* > SHA256SUMS) + + # 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/.gitignore b/.gitignore index 713d500..a884d38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ .env +dist/ diff --git a/deploy/publish-release.sh b/deploy/publish-release.sh new file mode 100644 index 0000000..4421cd0 --- /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 the release job packed. +# +# 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/remora} +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"