release #8
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: Build & Release | |
| # Self-versioning builds. | |
| # | |
| # Runs automatically on every push to `main` (code changes), and can also be run | |
| # by hand from Actions → "Build & Release" → Run workflow. Each run computes the | |
| # next version (latest release + 0.0.1 by default), builds Windows + macOS, and | |
| # publishes a GitHub Release with auto-update metadata — no manual version edits. | |
| # | |
| # • bump=patch (default) → 1.0.0 → 1.0.1 → 1.0.2 ... | |
| # • bump=minor/major (manual run only) → 1.0.x → 1.1.0 / 2.0.0 | |
| # • Need an exact version? Set it in desktop/package.json (higher than the | |
| # latest release) and it is used as-is for that run. | |
| # • Manual run with "publish" unchecked → test build (artifacts only, no bump). | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - master | |
| paths-ignore: | |
| - "**.md" | |
| - ".gitignore" | |
| - "LICENSE" | |
| workflow_dispatch: | |
| inputs: | |
| bump: | |
| description: "Version increment" | |
| type: choice | |
| default: patch | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| publish: | |
| description: "Publish a release (uncheck for a test build)" | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: write | |
| jobs: | |
| # Resolve the next version, tag it, and open a draft release up front so the | |
| # platform build jobs can upload to it without racing each other. | |
| version: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.calc.outputs.version }} | |
| tag: ${{ steps.calc.outputs.tag }} | |
| publish: ${{ steps.calc.outputs.publish }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # need all tags to find the latest version | |
| - id: calc | |
| shell: bash | |
| run: | | |
| # Defaults make a push event behave like a normal patch release. | |
| # (GitHub runs bash with `set -eo pipefail`, so be careful below.) | |
| BUMP="${{ inputs.bump }}" | |
| PUBLISH="${{ inputs.publish }}" | |
| if [ -z "$BUMP" ]; then BUMP=patch; fi | |
| if [ -z "$PUBLISH" ]; then PUBLISH=true; fi | |
| PKG=$(node -p "require('./desktop/package.json').version") | |
| # On the first build there are no tags: `grep` matches nothing and | |
| # exits 1, which (with pipefail) would kill the step — `|| true` | |
| # keeps it alive and leaves LATEST empty. | |
| LATEST=$(git tag -l 'v*' | sed 's/^v//' \ | |
| | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1 || true) | |
| bump() { # $1=version $2=level | |
| IFS=. read -r MA MI PA <<< "$1" | |
| case "$2" in | |
| major) echo "$((MA+1)).0.0" ;; | |
| minor) echo "$MA.$((MI+1)).0" ;; | |
| *) echo "$MA.$MI.$((PA+1))" ;; | |
| esac | |
| } | |
| gt() { [ "$1" != "$2" ] && \ | |
| [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ]; } | |
| if [ -z "$LATEST" ]; then | |
| NEW="$PKG" # first release uses package.json as-is | |
| elif gt "$PKG" "$LATEST"; then | |
| NEW="$PKG" # manual bump in package.json wins | |
| else | |
| NEW="$(bump "$LATEST" "$BUMP")" | |
| fi | |
| echo "version=$NEW" >> "$GITHUB_OUTPUT" | |
| echo "tag=v$NEW" >> "$GITHUB_OUTPUT" | |
| echo "publish=$PUBLISH" >> "$GITHUB_OUTPUT" | |
| echo "Resolved: v$NEW (latest tag: ${LATEST:-none}, pkg: $PKG, bump: $BUMP, publish: $PUBLISH)" | |
| - name: Tag version and open draft release | |
| if: steps.calc.outputs.publish == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| TAG="${{ steps.calc.outputs.tag }}" | |
| if ! git rev-parse "$TAG" >/dev/null 2>&1; then | |
| git tag "$TAG" | |
| git push origin "$TAG" | |
| fi | |
| if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| gh release create "$TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --draft --title "Peek Remote $TAG" --generate-notes | |
| fi | |
| build: | |
| needs: version | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| name: windows | |
| - os: macos-latest | |
| name: macos | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Apply resolved version | |
| shell: bash | |
| working-directory: desktop | |
| run: npm version "${{ needs.version.outputs.version }}" --no-git-tag-version --allow-same-version | |
| - name: Install backend dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt pyinstaller | |
| - name: Build frontend (Next.js static export) | |
| working-directory: web | |
| run: | | |
| npm ci | |
| npm run build | |
| - name: Build backend (PyInstaller) | |
| run: pyinstaller peek-backend.spec --noconfirm | |
| - name: Install desktop dependencies | |
| working-directory: desktop | |
| run: npm install | |
| # Only export signing vars when the secrets actually exist. Passing an | |
| # EMPTY CSC_LINK makes electron-builder think a certificate is provided and | |
| # try to read it from path "" → "<dir> not a file" failure on macOS. | |
| - name: Configure code signing | |
| shell: bash | |
| env: | |
| SECRET_CSC_LINK: ${{ secrets.CSC_LINK }} | |
| SECRET_CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
| SECRET_APPLE_ID: ${{ secrets.APPLE_ID }} | |
| SECRET_APPLE_PW: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| SECRET_APPLE_TEAM: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| if [ -n "$SECRET_CSC_LINK" ]; then | |
| echo "CSC_LINK=$SECRET_CSC_LINK" >> "$GITHUB_ENV" | |
| echo "CSC_KEY_PASSWORD=$SECRET_CSC_KEY_PASSWORD" >> "$GITHUB_ENV" | |
| echo "CSC_IDENTITY_AUTO_DISCOVERY=true" >> "$GITHUB_ENV" | |
| echo "signing: ENABLED (certificate provided)" | |
| else | |
| # No cert → tell electron-builder to skip signing cleanly. | |
| echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV" | |
| echo "signing: disabled (no certificate)" | |
| fi | |
| if [ -n "$SECRET_APPLE_ID" ]; then | |
| echo "APPLE_ID=$SECRET_APPLE_ID" >> "$GITHUB_ENV" | |
| echo "APPLE_APP_SPECIFIC_PASSWORD=$SECRET_APPLE_PW" >> "$GITHUB_ENV" | |
| echo "APPLE_TEAM_ID=$SECRET_APPLE_TEAM" >> "$GITHUB_ENV" | |
| fi | |
| - name: Build & publish desktop app | |
| working-directory: desktop | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if [ "${{ needs.version.outputs.publish }}" = "true" ]; then | |
| npx electron-builder --publish always | |
| else | |
| npx electron-builder --publish never | |
| fi | |
| - name: Upload installers (test builds) | |
| if: needs.version.outputs.publish != 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: peek-remote-${{ matrix.name }} | |
| path: | | |
| dist-electron/*.exe | |
| dist-electron/*.dmg | |
| dist-electron/*.zip | |
| dist-electron/*.yml | |
| if-no-files-found: ignore | |
| # Make the draft release public once both platforms have uploaded. | |
| publish: | |
| needs: [version, build] | |
| if: needs.version.outputs.publish == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Publish the release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release edit "${{ needs.version.outputs.tag }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --draft=false --latest |