diff --git a/.gitea/workflows/build-android.yml b/.gitea/workflows/build-android.yml index ca1157b..16130ee 100644 --- a/.gitea/workflows/build-android.yml +++ b/.gitea/workflows/build-android.yml @@ -159,7 +159,7 @@ jobs: path: ${{ steps.apk.outputs.path }} retention-days: 90 - - name: Attach APK to Gitea release + - name: Attach APK to Gitea release (upsert) if: ${{ steps.label.outputs.is_release == 'true' }} env: GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }} @@ -170,25 +170,61 @@ jobs: APK_PATH="${{ steps.apk.outputs.path }}" APK_NAME="${{ steps.apk.outputs.name }}" - # Fetch release by tag (created by release.yml `create-release` job) - RELEASE_ID=$(curl -s "$BASE_URL/releases/tags/$TAG" \ - -H "Authorization: token $GITEA_TOKEN" \ - | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))") - if [ -z "$RELEASE_ID" ]; then - echo "::warning::No release found for tag $TAG — skipping asset upload" - exit 0 + if [ -z "${GITEA_TOKEN:-}" ]; then + echo "::error::DEPLOY_TOKEN secret not configured — cannot attach APK" + exit 1 fi - # Replace existing asset if present (re-run safety) - EXISTING_ID=$(curl -s "$BASE_URL/releases/$RELEASE_ID/assets" \ + # Upsert: look up release by tag. If it exists, reuse it; if 404, + # create one. Makes the Android workflow self-sufficient — no + # ordering dependency on release.yml's create-release job. + HTTP=$(curl -s -o /tmp/release.json -w "%{http_code}" \ + "$BASE_URL/releases/tags/$TAG" \ + -H "Authorization: token $GITEA_TOKEN") + case "$HTTP" in + 200) + RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['id'])") + echo "Found existing release id=$RELEASE_ID" + ;; + 404) + echo "No release for tag $TAG — creating one" + IS_PRE="false" + if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then + IS_PRE="true" + fi + CREATE_HTTP=$(curl -s -o /tmp/created.json -w "%{http_code}" \ + -X POST "$BASE_URL/releases" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"LedGrab $TAG\",\"draft\":false,\"prerelease\":$IS_PRE}") + if [ "$CREATE_HTTP" != "201" ] && [ "$CREATE_HTTP" != "200" ]; then + echo "::error::Failed to create release (HTTP $CREATE_HTTP)" + cat /tmp/created.json + exit 1 + fi + RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/created.json'))['id'])") + echo "Created release id=$RELEASE_ID" + ;; + *) + echo "::error::Unexpected HTTP $HTTP when looking up release for tag $TAG" + cat /tmp/release.json + exit 1 + ;; + esac + + # Replace existing asset if present (re-run safety). + EXISTING_ID=$(curl -fsS "$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']=='$APK_NAME'),''))" 2>/dev/null) + | python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$APK_NAME'),''))") if [ -n "$EXISTING_ID" ]; then - curl -s -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ID" \ + curl -fsS -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ID" \ -H "Authorization: token $GITEA_TOKEN" + echo "Replaced existing asset: $APK_NAME" fi - curl -s -X POST \ + # -f: exit non-zero on 4xx/5xx so a broken token fails the job + # loudly instead of the previous silent "Uploaded" lie. + curl -fsS -X POST \ "$BASE_URL/releases/$RELEASE_ID/assets?name=$APK_NAME" \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/octet-stream" \ diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 87e9ab4..8419d15 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -4,10 +4,21 @@ 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 }} @@ -112,6 +123,9 @@ jobs: # ── 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 @@ -149,6 +163,8 @@ jobs: 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: | @@ -184,6 +200,7 @@ jobs: # ── 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 @@ -219,6 +236,7 @@ jobs: retention-days: 90 - name: Attach tarball to release + if: github.event_name == 'push' env: GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }} run: | @@ -247,6 +265,7 @@ jobs: # ── 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 @@ -270,6 +289,9 @@ jobs: - 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 \ @@ -277,7 +299,10 @@ jobs: -u "${{ gitea.actor }}" --password-stdin - name: Build Docker image - if: steps.docker-login.outcome == 'success' + # 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 }}" @@ -296,7 +321,7 @@ jobs: fi - name: Push Docker image - if: steps.docker-login.outcome == 'success' + if: github.event_name == 'push' && steps.docker-login.outcome == 'success' run: | TAG="${{ gitea.ref_name }}" REGISTRY="${{ steps.meta.outputs.registry }}"