# J621 CD — manual release workflow (Actions tab -> "Run workflow"). # # One dispatch does everything; each half can be skipped with the `images` # and `desktop` inputs: # * builds and pushes the backend + frontend images (multi-arch, :latest # and :, GIT_HASH baked in for the version pill), # * builds the desktop packages and attaches them (plus the update # metadata) to the Gitea release tagged `desktop-v`. # # The live update feed (deploy/data/desktop, served by the frontend nginx at # /desktop/) is not touched here: it is runtime state on the deploy host and # is still published with `deploy/push_desktop.sh --no-build` from a machine # that can reach it. # # Registry login uses a repo PAT with the minimal write:package scope (the # Gitea registry rejects the automatic job token, go-gitea/gitea#23642); # release creation uses the automatic job token. Jobs run on the user-scoped # nitro-ci runner (ubuntu-latest). name: CD on: workflow_dispatch: inputs: images: description: Build and push the Docker images required: false default: "true" desktop: description: Build the desktop release required: false default: "true" platforms: description: Image platforms (comma separated) required: false default: linux/amd64,linux/arm64 windows: description: Also cross-build the Windows installer (needs wine, slow) required: false default: "false" concurrency: group: cd cancel-in-progress: false jobs: images: name: Build & push images if: ${{ inputs.images != 'false' }} runs-on: ubuntu-latest # The Gitea container registry does not accept the automatic job token # (go-gitea/gitea#23642 is still open), so the push uses a repo PAT with # the minimal write:package scope. Releases use the job token instead. permissions: contents: read env: REGISTRY_USER: ${{ secrets.REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check the registry credentials run: | if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_TOKEN" ]; then echo "Set the REGISTRY_USER and REGISTRY_TOKEN repo secrets" >&2 echo "(a PAT with the write:package scope)." >&2 exit 1 fi - name: Register binfmt (multi-arch builds) run: docker run --privileged --rm tonistiigi/binfmt --install all - name: Build & push both images run: | set -euo pipefail SHA="$(git rev-parse --short HEAD)" echo "Publishing $SHA for $PLATFORMS" PLATFORMS="$PLATFORMS" ./deploy/push_frontend.sh "$SHA" PLATFORMS="$PLATFORMS" ./deploy/push_backend.sh "$SHA" desktop: name: Desktop release if: ${{ inputs.desktop != 'false' }} runs-on: ubuntu-latest # Creating the release and uploading its assets uses the automatic job # token, so it needs write access to the repository's releases. permissions: contents: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" - name: Install packaging tools run: | sudo apt-get update sudo apt-get install -y --no-install-recommends fakeroot libarchive-tools if [ "${{ inputs.windows }}" = "true" ]; then # electron-builder runs the 32-bit NSIS installer under wine to # build the uninstaller: that needs a virtual display (Xvfb) and # 32-bit wine libraries. sudo dpkg --add-architecture i386 sudo apt-get update sudo apt-get install -y --no-install-recommends xvfb wine wine32:i386 fi - name: Install frontend + desktop dependencies run: | npm --prefix frontend ci --no-audit --no-fund npm --prefix desktop ci --no-audit --no-fund - name: Build desktop packages env: # Keep wine from trying to fetch Gecko/Mono on first run. WINEDLLOVERRIDES: mscoree,mshtml= run: | if [ "${{ inputs.windows }}" = "true" ]; then xvfb-run -a ./deploy/build_desktop.sh --all else ./deploy/build_desktop.sh --linux fi - name: Add the Gitea release env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }} run: | set -euo pipefail VERSION="$(node -p "require('./desktop/package.json').version")" TAG="desktop-v$VERSION" API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" AUTH="Authorization: token $GITEA_TOKEN" NOTES="$(printf 'J621 desktop %s\n\n' "$VERSION" cd desktop/release sha256sum ./*.deb ./*.pkg.tar.zst ./*.exe 2>/dev/null || true)" RELEASE_ID="$(curl -sf -H "$AUTH" "$API/releases/tags/$TAG" \ | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' \ 2>/dev/null || true)" if [ -z "$RELEASE_ID" ]; then echo "Creating release $TAG" PAYLOAD="$(python3 - "$TAG" "${{ github.sha }}" "$NOTES" <<'PY' import json, sys print(json.dumps({ "tag_name": sys.argv[1], "name": sys.argv[1], "body": sys.argv[3], "target_commitish": sys.argv[2], })) PY )" RELEASE_ID="$(curl -sf -X POST -H "$AUTH" \ -H "Content-Type: application/json" -d "$PAYLOAD" "$API/releases" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" else echo "Release $TAG already exists (id $RELEASE_ID); attaching missing files." fi EXISTING="$(curl -sf -H "$AUTH" "$API/releases/$RELEASE_ID/assets" \ || echo '[]')" for FILE in desktop/release/*"$VERSION"*.deb \ desktop/release/*"$VERSION"*.pkg.tar.zst \ desktop/release/latest-linux.yml \ desktop/release/latest.yml \ desktop/release/*"$VERSION"*.exe \ desktop/release/*"$VERSION"*.exe.blockmap; do [ -e "$FILE" ] || continue NAME="$(basename "$FILE")" # Replace the asset when it is already there: latest*.yml must # reference the installers built by *this* run (NSIS builds are # not bit-reproducible), so old copies are deleted first. ASSET_ID="$(printf '%s' "$EXISTING" | python3 -c ' import json, sys name = sys.argv[1] print(next((str(a["id"]) for a in json.load(sys.stdin) if a["name"] == name), "")) ' "$NAME")" if [ -n "$ASSET_ID" ]; then echo " replacing $NAME" curl -sf -X DELETE -H "$AUTH" \ "$API/releases/$RELEASE_ID/assets/$ASSET_ID" >/dev/null else echo " attaching $NAME" fi # Names like "J621 Setup 0.1.1.exe" contain spaces: encode them # or curl refuses the URL (exit 3). ENCODED="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$NAME")" curl -sf -X POST -H "$AUTH" -H "Content-Type: application/octet-stream" \ --data-binary @"$FILE" "$API/releases/$RELEASE_ID/assets?name=$ENCODED" >/dev/null done echo "Release: ${{ github.server_url }}/${{ github.repository }}/releases/tag/$TAG"