release: v1.4.5 #41
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release artifacts | |
| # Triggered manually or by pushing a `vX.Y.Z` tag. | |
| # Builds the backend & frontend Docker images for both linux/amd64 and | |
| # linux/arm64 (RPi 4/5), exports them as `.tar.gz` files, and uploads | |
| # them as artifacts. The maintainer then attaches the artifacts to a | |
| # GitHub Release manually — we deliberately don't push to a registry | |
| # from CI so a misclick on tag doesn't ship a broken build. | |
| # | |
| # Why tar.gz instead of `docker push`? | |
| # - PiTun installs are typically air-gapped (RPi behind a captive portal, | |
| # factory reset, etc.). Loadable tarballs match that workflow. | |
| # - No registry credentials needed in CI; release stays under the | |
| # maintainer's control end-to-end. | |
| on: | |
| push: | |
| tags: ['v*.*.*'] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version tag to use in artifact filenames (e.g. v1.2.3)' | |
| required: true | |
| default: 'v0.0.0-dev' | |
| permissions: | |
| # `contents: write` is required for the auto-publish step at the end | |
| # to create / update a GitHub Release and attach the built tarballs | |
| # as Release assets. Without it the run can build everything but | |
| # can't expose them on the Releases page. | |
| contents: write | |
| # `actions: read` is required by actions/download-artifact@v4 (the | |
| # combine job below pulls every artifact uploaded by this run). | |
| actions: read | |
| jobs: | |
| # Frontend dist is architecture-independent — single build, attached | |
| # to the release once. RPi nginx will serve it just fine. | |
| frontend: | |
| name: Frontend dist | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: 'frontend/package-lock.json' | |
| # `npm ci` for reproducible release builds — see ci.yml for the | |
| # `--legacy-peer-deps` rationale. | |
| - name: Install dependencies (npm ci, locked) | |
| working-directory: frontend | |
| run: npm ci --legacy-peer-deps --no-audit --no-fund | |
| - run: npm run build | |
| working-directory: frontend | |
| - name: Package dist | |
| env: | |
| VERSION: ${{ github.ref_name || inputs.version }} | |
| run: | | |
| tar -czf "pitun-frontend-${VERSION}.tar.gz" -C frontend/dist . | |
| ls -lh pitun-frontend-*.tar.gz | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: pitun-frontend | |
| path: pitun-frontend-*.tar.gz | |
| retention-days: 14 | |
| # Backend image must be built per-architecture because the Python wheels | |
| # (uvloop, httptools, watchfiles, etc.) are native binaries. | |
| # | |
| # Build on NATIVE runners per arch — `ubuntu-24.04-arm` for arm64, | |
| # `ubuntu-24.04` for amd64 — instead of cross-building amd64 → | |
| # arm64 via QEMU on a single amd64 runner. v1.3.0-beta.1 / .2 went | |
| # out cross-built and the resulting arm64 wheel binaries SIGILL'd on | |
| # an RPi 4 (Cortex-A72): same uvloop==0.22.1, identical pip freeze, | |
| # identical Python 3.11.15, but different .so md5 vs the v1.2.4 build | |
| # — `import uvicorn` exited 132 (illegal instruction). Native runners | |
| # eliminate the cross-build path entirely. | |
| # | |
| # GitHub now provides free public ARM runners (announced 2025-01); the | |
| # `ubuntu-24.04-arm` label maps to a 4-core Cobalt 100 (Neoverse N2) | |
| # runner, which is binary-compatible with RPi 4 / 5. | |
| backend: | |
| name: Backend image (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| arch: amd64 | |
| runner: ubuntu-24.04 | |
| - platform: linux/arm64 | |
| arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| # No QEMU needed any more — runner is native arch. Buildx still | |
| # required for the load: true output, but since target == host | |
| # platform, no emulation runs under the hood. | |
| - name: Set up Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| # Pull the version into an env var BEFORE shell sees it. Direct | |
| # `${{ ... }}` interpolation into a `run:` block is a script- | |
| # injection vector if the input is collaborator-controllable | |
| # (workflow_dispatch). With `env:`, GitHub passes the value to | |
| # the shell as a string, no template-time substitution. | |
| - name: Resolve version | |
| id: ver | |
| env: | |
| INPUT_VERSION: ${{ github.ref_name || inputs.version }} | |
| run: | | |
| echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" | |
| # Build for the target arch and load into the local docker daemon | |
| # so we can `docker save` it into a tarball. | |
| - name: Build & export image | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: ./backend | |
| file: ./backend/Dockerfile | |
| target: production | |
| platforms: ${{ matrix.platform }} | |
| load: true | |
| tags: pitun-backend:${{ steps.ver.outputs.version }} | |
| cache-from: type=gha,scope=backend-${{ matrix.arch }} | |
| cache-to: type=gha,scope=backend-${{ matrix.arch }},mode=max | |
| - name: Save image to tarball | |
| env: | |
| VERSION: ${{ steps.ver.outputs.version }} | |
| ARCH: ${{ matrix.arch }} | |
| run: | | |
| docker save "pitun-backend:${VERSION}" \ | |
| | gzip > "pitun-backend-${VERSION}-${ARCH}.tar.gz" | |
| ls -lh pitun-backend-*.tar.gz | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: pitun-backend-${{ matrix.arch }} | |
| path: pitun-backend-*.tar.gz | |
| retention-days: 14 | |
| # Naive sidecar image — same native-runner story as backend. | |
| # naive doesn't have Python wheels (it's a Caddy + naive_forwardproxy | |
| # binary), so it's less likely to hit instruction-set issues, but | |
| # cross-build via QEMU has been a reliable source of weird subtleties | |
| # (file corruption, mtime drift) — keep it consistent with backend. | |
| naive: | |
| name: Naive sidecar image (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| arch: amd64 | |
| runner: ubuntu-24.04 | |
| - platform: linux/arm64 | |
| arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| # See backend job above for why we use `env:` here. | |
| - name: Resolve version | |
| id: ver | |
| env: | |
| INPUT_VERSION: ${{ github.ref_name || inputs.version }} | |
| run: | | |
| echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" | |
| - name: Build & export image | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: ./docker/naive | |
| file: ./docker/naive/Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| load: true | |
| tags: pitun-naive:${{ steps.ver.outputs.version }} | |
| cache-from: type=gha,scope=naive-${{ matrix.arch }} | |
| cache-to: type=gha,scope=naive-${{ matrix.arch }},mode=max | |
| - name: Save image to tarball | |
| env: | |
| VERSION: ${{ steps.ver.outputs.version }} | |
| ARCH: ${{ matrix.arch }} | |
| run: | | |
| docker save "pitun-naive:${VERSION}" \ | |
| | gzip > "pitun-naive-${VERSION}-${ARCH}.tar.gz" | |
| ls -lh pitun-naive-*.tar.gz | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: pitun-naive-${{ matrix.arch }} | |
| path: pitun-naive-*.tar.gz | |
| retention-days: 14 | |
| # Bundle everything into a single artifact for convenient download. | |
| bundle: | |
| name: Combine artifacts | |
| needs: [frontend, backend, naive] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| # Restrict to our own artifacts. `build-push-action` also | |
| # uploads opaque internal artifacts named `<owner>~<repo>~XXXX.dockerbuild` | |
| # that we don't want to pull. `pattern: 'pitun-*'` matches the | |
| # five we actually publish: pitun-frontend, | |
| # pitun-{backend,naive}-{amd64,arm64}. | |
| pattern: 'pitun-*' | |
| path: artifacts/ | |
| - name: List downloaded artifacts | |
| run: ls -la artifacts/ && find artifacts/ -type f -exec ls -lh {} \; | |
| - name: Flatten into single dir | |
| run: | | |
| mkdir -p release | |
| find artifacts/ -name '*.tar.gz' -exec cp {} release/ \; | |
| ls -lh release/ | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: pitun-release-bundle | |
| path: release/* | |
| retention-days: 14 | |
| # Auto-publish to the GitHub Releases page so install.sh can | |
| # discover assets via the public `/releases/latest` API. Only | |
| # runs on a real tag push (`v*.*.*`); a manual `workflow_dispatch` | |
| # against an arbitrary version string would skip this step, | |
| # since the inputs.version may not correspond to a real tag. | |
| # | |
| # Pre-release detection: any tag containing a hyphen-suffix | |
| # (`-beta.1`, `-rc.2`, `-alpha.0`, etc., per semver 2.0) is | |
| # marked `prerelease: true` and `make_latest: false`. This | |
| # keeps the GitHub `/releases/latest` API pointing at the most | |
| # recent STABLE release, so users running `curl … | sudo bash` | |
| # without an explicit `--version` flag never auto-upgrade onto | |
| # an in-flight beta. Beta testers opt in by running the | |
| # installer with `--version vX.Y.Z-beta.N`. | |
| - name: Detect pre-release tag | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | |
| id: prerelease | |
| run: | | |
| if [[ "${{ github.ref_name }}" == *-* ]]; then | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| echo "Tag ${{ github.ref_name }} contains a hyphen suffix → publishing as pre-release." | |
| else | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| echo "Tag ${{ github.ref_name }} → publishing as stable release (Latest)." | |
| fi | |
| - name: Create / update GitHub Release with assets | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | |
| uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: ${{ github.ref_name }} | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| files: release/* | |
| prerelease: ${{ steps.prerelease.outputs.is_prerelease }} | |
| # `make_latest: legacy` (default) gives Latest only to | |
| # non-prerelease tags. Be explicit so this stays correct | |
| # if a future action version changes the default. | |
| make_latest: ${{ steps.prerelease.outputs.is_prerelease == 'false' && 'true' || 'false' }} |