diff --git a/.gitea/workflows/windows-release.yml b/.gitea/workflows/windows-release.yml new file mode 100644 index 00000000..e3fa82cb --- /dev/null +++ b/.gitea/workflows/windows-release.yml @@ -0,0 +1,87 @@ +name: Mirror release tags to GitHub build bridge + +# Gitea is the source of truth for releases, but this instance has no Windows +# runner (only ubuntu-latest on the homeserver), and Flutter Windows builds +# require a Windows host. So when a tag is pushed here, this workflow mirrors it +# to the public GitHub fork yorickr/plezy, whose only active workflow +# (.github/workflows/windows-gitea-release.yml) builds the Windows release on +# GitHub-hosted runners and publishes the artifacts back to this repo's Gitea +# release for the same tag. +# +# The write path uses an SSH deploy key (secret RELEASE_PUSH_KEY) that only has +# access to the fork. ssh.github.com:443 is used because the homeserver +# demonstrably reaches GitHub over 443, and it avoids depending on outbound 22. + +on: + push: + tags: + - "*" + workflow_dispatch: + inputs: + tag: + description: "Tag to mirror to the GitHub build bridge (e.g. 2.13.0)" + required: true + type: string + +jobs: + mirror-tag: + name: Mirror tag to GitHub + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + - name: Mirror tag to GitHub fork + shell: bash + env: + RELEASE_PUSH_KEY: ${{ secrets.RELEASE_PUSH_KEY }} + run: | + set -euo pipefail + + if [[ "${{ github.event_name }}" == "push" ]]; then + TAG="${GITHUB_REF_NAME}" + else + TAG="${{ inputs.tag }}" + fi + + if [[ -z "${TAG}" ]]; then + echo "No tag to mirror" >&2 + exit 1 + fi + if [[ -z "${RELEASE_PUSH_KEY}" ]]; then + echo "Missing RELEASE_PUSH_KEY secret - refusing to mirror" >&2 + exit 1 + fi + + # The tag must exist in this repository. + git rev-parse --verify "refs/tags/${TAG}" >/dev/null || { + echo "Tag ${TAG} not found in this repository" >&2 + exit 1 + } + + REMOTE="git@ssh.github.com:yorickr/plezy.git" + + # Already mirrored? Nothing to do (previous run may have succeeded, + # or the tag was pushed to GitHub directly). + if git ls-remote "${REMOTE}" "refs/tags/${TAG}" 2>/dev/null | grep -q .; then + echo "Tag ${TAG} already exists on the GitHub build bridge - nothing to do" + exit 0 + fi + + mkdir -p "${HOME}/.ssh" + chmod 700 "${HOME}/.ssh" + printf '%s\n' "${RELEASE_PUSH_KEY}" > "${HOME}/.ssh/id_ed25519" + chmod 600 "${HOME}/.ssh/id_ed25519" + # GitHub's published ed25519 host key (ssh.github.com shares it). + printf '%s\n' \ + 'github.com,ssh.github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl' \ + > "${HOME}/.ssh/known_hosts" + + export GIT_SSH_COMMAND="ssh -p 443 -i '${HOME}/.ssh/id_ed25519' -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile='${HOME}/.ssh/known_hosts'" + + git push "${REMOTE}" "refs/tags/${TAG}:refs/tags/${TAG}" + echo "Mirrored tag ${TAG} to the GitHub build bridge" diff --git a/.github/workflows/windows-gitea-release.yml b/.github/workflows/windows-gitea-release.yml new file mode 100644 index 00000000..480196e3 --- /dev/null +++ b/.github/workflows/windows-gitea-release.yml @@ -0,0 +1,203 @@ +name: Publish Windows Release to Gitea + +# Builds the Windows release on GitHub-hosted runners and publishes it to the +# release of the matching tag in the Gitea repository. It is triggered by tags +# mirrored from Gitea by .gitea/workflows/windows-release.yml (or by any tag +# pushed to this fork directly). +# +# This workflow is skipped everywhere except GitHub and except the +# yorickr/plezy fork, which owns the GITEA_TOKEN secret. It never creates a +# GitHub release; the Gitea release is the artifact channel. + +on: + push: + tags: + - "*" + +env: + # Only place this workflow names the SDK; .github/actions/setup-flutter-git pins the same release. + FLUTTER_VERSION: "3.44.0" + # Same defines as build.yml, except Sentry is always enabled: these builds are + # the primary channel from the Gitea repo, and the DSN is compiled in anyway. + RELEASE_DART_DEFINES: >- + --dart-define=ENABLE_UPDATE_CHECK=true + --dart-define=ENABLE_SENTRY=true + --dart-define=GIT_COMMIT=${{ github.sha }} + --dart-define=SENTRY_ENVIRONMENT=gitea-release + --dart-define=ENABLE_DONATIONS=true + +jobs: + build-windows: + name: Build Windows x64 and publish to Gitea + # Never queue on the Gitea instance (it has no Windows runner) and never + # run in random forks (they lack GITEA_TOKEN). + if: github.server_url == 'https://github.com' && github.repository == 'yorickr/plezy' + runs-on: windows-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup Flutter + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + cache-key: "gitea-release-flutter-:os:-:channel:-:version:-:arch:-:hash:" + pub-cache: false + + - name: Cache Pub dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: | + ~\AppData\Local\Pub\Cache + key: gitea-release-${{ runner.os }}-${{ runner.arch }}-pub-${{ hashFiles('pubspec.yaml', 'pubspec.lock') }} + + - name: Install dependencies + shell: pwsh + run: flutter pub get --enforce-lockfile --no-example + + - name: Install patched Flutter engine (DComp) + shell: pwsh + run: | + flutter precache --windows + .\windows\tool\install-patched-engine.ps1 + + - name: Build Windows x64 + shell: pwsh + run: flutter build windows --release ${{ env.RELEASE_DART_DEFINES }} --dart-define=SENTRY_DIST=gitea-windows-x64 --split-debug-info=debug-info/windows-x64 + + - name: Upload symbols to bugs.plezy.app + # Only when this fork has been given the admin token (the production + # token lives on edde746/plezy, which is not the build bridge). + if: secrets.BUGS_ADMIN_TOKEN != '' + shell: pwsh + env: + BUGS_ADMIN_TOKEN: ${{ secrets.BUGS_ADMIN_TOKEN }} + SENTRY_DIST: gitea-windows-x64 + run: .\scripts\upload-symbols.ps1 windows-x64 + + - name: Read version from pubspec + id: version + shell: pwsh + run: | + $v = (Select-String -Path pubspec.yaml -Pattern '^version:\s*(\S+)').Matches[0].Groups[1].Value -replace '\+.*' + echo "version=$v" >> $env:GITHUB_OUTPUT + + - name: Build installer and portable + shell: pwsh + run: .\windows\build-installer.ps1 -X64BuildDir "build\windows\x64\runner\Release" -Version "${{ steps.version.outputs.version }}" + + - name: Sign installer for WinSparkle (EdDSA) + if: secrets.SPARKLE_PRIVATE_KEY != '' + env: + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + shell: pwsh + run: | + $keyPath = Join-Path $env:RUNNER_TEMP "plezy-winsparkle-ed25519.pem" + try { + Set-Content -Path $keyPath -Value $env:SPARKLE_PRIVATE_KEY -Encoding ascii -NoNewline + $output = & dart run auto_updater:sign_update plezy-windows-installer.exe $keyPath + if ($LASTEXITCODE -ne 0) { + throw "WinSparkle signer failed with exit code $LASTEXITCODE" + } + $match = [regex]::Match($output, 'edSignature="([^"]*)"') + if (-not $match.Success) { + throw "WinSparkle signer returned no EdDSA signature" + } + Set-Content -Path win-ed-signature.txt -Value $match.Groups[1].Value -Encoding ascii -NoNewline + Set-Content -Path win-installer-size.txt -Value (Get-Item plezy-windows-installer.exe).Length.ToString() -Encoding ascii -NoNewline + } finally { + Remove-Item -Path $keyPath -Force -ErrorAction SilentlyContinue + } + + - name: Publish to Gitea release + shell: bash + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_SERVER: ${{ vars.GITEA_SERVER || 'https://git.yorickrommers.nl' }} + GITEA_REPO: ${{ vars.GITEA_REPO || 'yorickr/plezy' }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + + if [[ -z "${GITEA_TOKEN}" ]]; then + echo "Missing GITEA_TOKEN secret - cannot publish" >&2 + exit 1 + fi + + API="${GITEA_SERVER}/api/v1/repos/${GITEA_REPO}" + VERSION="${{ steps.version.outputs.version }}" + + # Only publish tags that exist in the Gitea repo (i.e. tags mirrored + # from there, or pushed to Gitea directly). Tags that only exist on + # GitHub are not this pipeline's business. + if ! curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/git/refs/tags/${TAG}" >/dev/null 2>&1; then + echo "Tag ${TAG} does not exist in ${GITEA_REPO} on ${GITEA_SERVER}; skipping publish" + exit 0 + fi + + NOTES=$(cat <<'EOF' + Windows build for Plezy. + + Artifacts: + - **plezy-windows-installer.exe** - Inno Setup installer (silent-update compatible, auto-detects architecture) + - **plezy-windows-x64-portable.7z** - portable x64 build (no installation needed) + + Built automatically on GitHub-hosted runners from this tag and published to + this Gitea release. + EOF + ) + + if RELEASE=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/releases/tags/${TAG}" 2>/dev/null); then + RELEASE_ID=$(echo "${RELEASE}" | jq -r '.id') + echo "Release for ${TAG} already exists (id ${RELEASE_ID}); updating" + BODY=$(jq -n --arg name "Plezy ${VERSION}" --arg body "${NOTES}" \ + '{name: $name, body: $body, draft: false, prerelease: false}') + RESP=$(curl -fsS -X PATCH -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" -d "${BODY}" \ + "${API}/releases/${RELEASE_ID}") + else + echo "Creating release for ${TAG}" + BODY=$(jq -n --arg tag "${TAG}" --arg name "Plezy ${VERSION}" --arg body "${NOTES}" \ + '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}') + RESP=$(curl -fsS -X POST -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" -d "${BODY}" \ + "${API}/releases") + fi + + RELEASE_ID=$(echo "${RESP}" | jq -r '.id') + if [[ -z "${RELEASE_ID}" || "${RELEASE_ID}" == "null" ]]; then + echo "Failed to create/update the release:" >&2 + echo "${RESP}" >&2 + exit 1 + fi + + # Upload each asset, replacing a same-named asset from an earlier run. + upload() { + local FILE="$1" + local NAME + NAME=$(basename "${FILE}") + local EXISTING_ASSET + EXISTING_ASSET=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/releases/${RELEASE_ID}/assets" | jq -r --arg name "${NAME}" \ + '.[] | select(.name == $name) | .id') + if [[ -n "${EXISTING_ASSET}" ]]; then + curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/releases/${RELEASE_ID}/assets/${EXISTING_ASSET}" >/dev/null + fi + curl -fsS -X POST -H "Authorization: token ${GITEA_TOKEN}" \ + -F "attachment=@${FILE}" \ + "${API}/releases/${RELEASE_ID}/assets?name=${NAME}" >/dev/null + echo "Uploaded ${NAME} ($(stat -c%s "${FILE}") bytes)" + } + + upload plezy-windows-x64-portable.7z + upload plezy-windows-installer.exe + + echo "Published release for ${TAG}: ${GITEA_SERVER}/${GITEA_REPO}/releases/tag/${TAG}"