03d2e6b1f2
Lint & Test / test (push) Successful in 2m4s
The in-app update service (`ledgrab.core.update.update_service`) refuses to install any downloaded artifact that has no published sha256 — either as a sibling `<asset>.sha256` asset on the Gitea release, or embedded in the release body. The release workflow uploaded the ZIP, setup.exe, and Linux tarball but never published checksums, so every auto-update 500'd with "Update checksum unavailable; install aborted". Generate sha256sum sidecars for the Windows ZIP, Windows setup.exe, and Linux tar.gz and upload them next to the primary asset on each tagged release. Existing v0.4.x releases stay broken — ship v0.4.2 (or manually upload sidecars to v0.4.1) to unblock in-app updates.
353 lines
14 KiB
YAML
353 lines
14 KiB
YAML
name: Build Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
# Manual dispatch builds Windows/Linux/Docker artifacts without creating
|
|
# a Gitea release — for validating build scripts between real releases.
|
|
# Attach/push steps are gated on github.event_name == 'push'.
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Version label for dispatch builds (artifacts only, no release)'
|
|
required: false
|
|
default: 'dev'
|
|
|
|
jobs:
|
|
# ── Create the release first (shared by all build jobs) ────
|
|
create-release:
|
|
# Skipped on workflow_dispatch — dispatch is for build validation only.
|
|
if: github.event_name == 'push'
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
release_id: ${{ steps.create.outputs.release_id }}
|
|
steps:
|
|
- name: Fetch RELEASE_NOTES.md only
|
|
uses: actions/checkout@v4
|
|
with:
|
|
sparse-checkout: RELEASE_NOTES.md
|
|
sparse-checkout-cone-mode: false
|
|
|
|
- name: Create Gitea release
|
|
id: create
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
|
run: |
|
|
TAG="${{ gitea.ref_name }}"
|
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
|
|
|
IS_PRE="false"
|
|
if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
|
|
IS_PRE="true"
|
|
fi
|
|
|
|
# Build registry path for Docker instructions
|
|
SERVER_HOST=$(echo "${{ gitea.server_url }}" | sed -E 's|https?://||')
|
|
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
|
DOCKER_IMAGE="${SERVER_HOST}/${REPO}"
|
|
|
|
if [ -f RELEASE_NOTES.md ]; then
|
|
export RELEASE_NOTES=$(cat RELEASE_NOTES.md)
|
|
echo "Found RELEASE_NOTES.md"
|
|
else
|
|
export RELEASE_NOTES=""
|
|
echo "No RELEASE_NOTES.md found"
|
|
fi
|
|
|
|
# Build release body via Python to avoid YAML escaping issues
|
|
BODY_JSON=$(python3 -c "
|
|
import json, sys, os, textwrap
|
|
|
|
tag = '$TAG'
|
|
image = '$DOCKER_IMAGE'
|
|
release_notes = os.environ.get('RELEASE_NOTES', '')
|
|
|
|
sections = []
|
|
|
|
if release_notes.strip():
|
|
sections.append(release_notes.strip())
|
|
|
|
sections.append(textwrap.dedent(f'''
|
|
## Downloads
|
|
|
|
| Platform | File | Description |
|
|
|----------|------|-------------|
|
|
| Windows (installer) | \`LedGrab-{tag}-setup.exe\` | Install with Start Menu shortcut, optional autostart, uninstaller |
|
|
| Windows (portable) | \`LedGrab-{tag}-win-x64.zip\` | Unzip anywhere, run LedGrab.bat |
|
|
| Linux | \`LedGrab-{tag}-linux-x64.tar.gz\` | Extract, run ./run.sh |
|
|
| Android | \`LedGrab-{tag}-android-release.apk\` | Sideload on Android 7.0+ (API 24+) — TV boxes, Fire TV, phones, tablets. arm64-v8a / x86_64 / x86 |
|
|
| Docker | See below | docker pull + docker run |
|
|
|
|
After starting, open **http://localhost:8080** in your browser.
|
|
|
|
### Docker
|
|
|
|
\`\`\`bash
|
|
docker pull {image}:{tag}
|
|
docker run -d --name ledgrab -p 8080:8080 -v ledgrab-data:/app/data {image}:{tag}
|
|
\`\`\`
|
|
|
|
### First-time setup
|
|
|
|
1. Change the default API key in `config/default_config.yaml`.
|
|
2. Open http://localhost:8080 and add your LED devices.
|
|
3. See `INSTALLATION.md` for detailed configuration.
|
|
''').strip())
|
|
|
|
print(json.dumps('\n\n'.join(sections)))
|
|
")
|
|
|
|
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"tag_name\": \"$TAG\",
|
|
\"name\": \"LedGrab $TAG\",
|
|
\"body\": $BODY_JSON,
|
|
\"draft\": false,
|
|
\"prerelease\": $IS_PRE
|
|
}")
|
|
|
|
# Fallback: if release already exists for this tag, fetch it instead
|
|
RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null)
|
|
if [ -z "$RELEASE_ID" ]; then
|
|
echo "::warning::Release already exists for tag $TAG — reusing existing release"
|
|
RELEASE=$(curl -s "$BASE_URL/releases/tags/$TAG" \
|
|
-H "Authorization: token $GITEA_TOKEN")
|
|
RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
fi
|
|
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
|
echo "Release ID: $RELEASE_ID"
|
|
|
|
# ── Windows portable ZIP (cross-built from Linux) ─────────
|
|
build-windows:
|
|
needs: create-release
|
|
# `!cancelled()` lets this job run even when create-release was skipped
|
|
# (dispatch) or failed. The attach step itself is still push-gated.
|
|
if: ${{ !cancelled() && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Install system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends zip libportaudio2 nsis msitools
|
|
|
|
- name: Cross-build Windows distribution
|
|
run: |
|
|
chmod +x build/build-dist-windows.sh
|
|
./build/build-dist-windows.sh "${{ gitea.ref_name }}"
|
|
|
|
- name: Upload build artifacts
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: LedGrab-${{ gitea.ref_name }}-win-x64
|
|
path: |
|
|
build/LedGrab-*.zip
|
|
build/LedGrab-*-setup.exe
|
|
retention-days: 90
|
|
|
|
- name: Attach assets to release
|
|
# Push (tag) only — dispatch runs produce artifacts but no release.
|
|
if: github.event_name == 'push'
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
|
run: |
|
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
|
|
|
# Upload helper — deletes existing asset with same name to prevent duplicates on re-run
|
|
upload_asset() {
|
|
local FILE="$1"
|
|
local NAME=$(basename "$FILE")
|
|
EXISTING_ID=$(curl -s "$BASE_URL/releases/$RELEASE_ID/assets" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
| python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$NAME'),''))" 2>/dev/null)
|
|
if [ -n "$EXISTING_ID" ]; then
|
|
curl -s -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ID" \
|
|
-H "Authorization: token $GITEA_TOKEN"
|
|
echo "Replaced existing asset: $NAME"
|
|
fi
|
|
curl -s -X POST \
|
|
"$BASE_URL/releases/$RELEASE_ID/assets?name=$NAME" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary "@$FILE"
|
|
echo "Uploaded: $NAME"
|
|
}
|
|
|
|
# Publish an asset plus its .sha256 sidecar. The in-app update
|
|
# service refuses to install without a published checksum, so
|
|
# every artifact needs its hash uploaded alongside.
|
|
upload_with_sha256() {
|
|
local FILE="$1"
|
|
upload_asset "$FILE"
|
|
(cd "$(dirname "$FILE")" && sha256sum "$(basename "$FILE")" > "$(basename "$FILE").sha256")
|
|
upload_asset "$FILE.sha256"
|
|
}
|
|
|
|
ZIP_FILE=$(ls build/LedGrab-*.zip | head -1)
|
|
[ -f "$ZIP_FILE" ] && upload_with_sha256 "$ZIP_FILE"
|
|
|
|
SETUP_FILE=$(ls build/LedGrab-*-setup.exe 2>/dev/null | head -1)
|
|
[ -f "$SETUP_FILE" ] && upload_with_sha256 "$SETUP_FILE"
|
|
|
|
# ── Linux tarball ──────────────────────────────────────────
|
|
build-linux:
|
|
needs: create-release
|
|
if: ${{ !cancelled() && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Install system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends libportaudio2
|
|
|
|
- name: Build Linux distribution
|
|
run: |
|
|
chmod +x build/build-dist.sh
|
|
./build/build-dist.sh "${{ gitea.ref_name }}"
|
|
|
|
- name: Upload build artifact
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: LedGrab-${{ gitea.ref_name }}-linux-x64
|
|
path: build/LedGrab-*.tar.gz
|
|
retention-days: 90
|
|
|
|
- name: Attach tarball to release
|
|
if: github.event_name == 'push'
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
|
run: |
|
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
|
|
|
upload_asset() {
|
|
local FILE="$1"
|
|
local NAME
|
|
NAME=$(basename "$FILE")
|
|
EXISTING_ID=$(curl -s "$BASE_URL/releases/$RELEASE_ID/assets" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
| python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$NAME'),''))" 2>/dev/null)
|
|
if [ -n "$EXISTING_ID" ]; then
|
|
curl -s -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ID" \
|
|
-H "Authorization: token $GITEA_TOKEN"
|
|
echo "Replaced existing asset: $NAME"
|
|
fi
|
|
curl -s -X POST \
|
|
"$BASE_URL/releases/$RELEASE_ID/assets?name=$NAME" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary "@$FILE"
|
|
echo "Uploaded: $NAME"
|
|
}
|
|
|
|
TAR_FILE=$(ls build/LedGrab-*.tar.gz | head -1)
|
|
if [ -f "$TAR_FILE" ]; then
|
|
upload_asset "$TAR_FILE"
|
|
(cd "$(dirname "$TAR_FILE")" && sha256sum "$(basename "$TAR_FILE")" > "$(basename "$TAR_FILE").sha256")
|
|
upload_asset "$TAR_FILE.sha256"
|
|
fi
|
|
|
|
# ── Docker image ───────────────────────────────────────────
|
|
build-docker:
|
|
needs: create-release
|
|
if: ${{ !cancelled() && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Extract version metadata
|
|
id: meta
|
|
run: |
|
|
TAG="${{ gitea.ref_name }}"
|
|
VERSION="${TAG#v}"
|
|
# Strip protocol and lowercase for Docker registry path
|
|
SERVER_HOST=$(echo "${{ gitea.server_url }}" | sed -E 's|https?://||')
|
|
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
|
REGISTRY="${SERVER_HOST}/${REPO}"
|
|
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"
|
|
echo "server_host=$SERVER_HOST" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Login to Gitea Container Registry
|
|
id: docker-login
|
|
# Dispatch runs don't need registry credentials — the build step
|
|
# verifies the Dockerfile locally and push is skipped.
|
|
if: github.event_name == 'push'
|
|
continue-on-error: true
|
|
run: |
|
|
echo "${{ secrets.DEPLOY_TOKEN }}" | docker login \
|
|
"${{ steps.meta.outputs.server_host }}" \
|
|
-u "${{ gitea.actor }}" --password-stdin
|
|
|
|
- name: Build Docker image
|
|
# Always build — dispatch uses this to validate the Dockerfile.
|
|
# On push, still gate on successful login so we don't build a
|
|
# tagged image that can't be pushed.
|
|
if: github.event_name != 'push' || steps.docker-login.outcome == 'success'
|
|
run: |
|
|
TAG="${{ gitea.ref_name }}"
|
|
REGISTRY="${{ steps.meta.outputs.registry }}"
|
|
|
|
docker build \
|
|
--build-arg APP_VERSION="${{ steps.meta.outputs.version }}" \
|
|
--label "org.opencontainers.image.version=${{ steps.meta.outputs.version }}" \
|
|
--label "org.opencontainers.image.revision=${{ gitea.sha }}" \
|
|
-t "$REGISTRY:$TAG" \
|
|
-t "$REGISTRY:${{ steps.meta.outputs.version }}" \
|
|
./server
|
|
|
|
# Tag as latest only for stable releases
|
|
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
|
|
docker tag "$REGISTRY:$TAG" "$REGISTRY:latest"
|
|
fi
|
|
|
|
- name: Push Docker image
|
|
if: github.event_name == 'push' && steps.docker-login.outcome == 'success'
|
|
run: |
|
|
TAG="${{ gitea.ref_name }}"
|
|
REGISTRY="${{ steps.meta.outputs.registry }}"
|
|
|
|
docker push "$REGISTRY:$TAG"
|
|
docker push "$REGISTRY:${{ steps.meta.outputs.version }}"
|
|
|
|
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
|
|
docker push "$REGISTRY:latest"
|
|
fi
|