Release: 0.2.0-rc0 #3
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: Tagged releases | |
| # Sequential publish: e2e → docker → github-release → screenshots-pr. The | |
| # GitHub Release lands LAST so a Docker-push failure can't strand the project | |
| # with an immutable Release pointing at a non-existent image. Pages deployment | |
| # is decoupled into deploy-pages.yml — it runs from a workflow_run trigger. | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| validate-tag: | |
| name: Validate tag | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Check tag-name format | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| # Allow vM.m.p as well (suffix-free, reserved for future "stable" releases) — | |
| # the prerelease format vM.m.p-(rc|beta)N is the only one bump.sh currently | |
| # emits, but accepting both makes :latest Docker promotion sensible. | |
| if [[ ! "${REF_NAME}" =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-(rc|beta)[0-9]{1,2})?$ ]]; then | |
| echo "Tag '${REF_NAME}' does not match vM.m.p or vM.m.p-(rc|beta)N" >&2 | |
| echo "Releases must be cut via the 'Release prepare' workflow." >&2 | |
| exit 1 | |
| fi | |
| - name: Checkout source code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify tag commit is on main | |
| run: | | |
| set -euo pipefail | |
| git fetch origin main --no-tags | |
| if ! git merge-base --is-ancestor HEAD origin/main; then | |
| echo "::error::Tag commit is not reachable from origin/main. Tags must come from main." >&2 | |
| exit 1 | |
| fi | |
| - name: Setup environment | |
| uses: ./.github/actions/common-setup | |
| - name: Verify package.json#version matches tag | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| tag_name="${REF_NAME#v}" | |
| parsed=$(./tools/release/bump.sh --mode=check) | |
| current_name=$(echo "$parsed" | grep -E '^current_name=' | cut -d= -f2) | |
| if [[ "$current_name" != "$tag_name" ]]; then | |
| echo "Tag '${REF_NAME}' does not match package.json#version '$current_name'" >&2 | |
| echo "package.json must equal the tag — releases must be cut via 'Release prepare'." >&2 | |
| exit 1 | |
| fi | |
| e2e: | |
| needs: validate-tag | |
| name: E2E + screenshots (real sync-server) | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 25 | |
| # Boots the same digest-pinned sync-server the smoke job uses, seeds a fake | |
| # Pixel 7 peer, drives the SPA through Playwright, captures screenshots. | |
| # Failure here BLOCKS docker/github-release/pages — see job ordering below. | |
| services: | |
| sync-server: | |
| image: ghcr.io/d4rken-org/octi-server@sha256:3829efba5ca5a4d407a0d0a048b8d0c20264ad2f9e389aa087c857ea0d0bddaa | |
| ports: | |
| - 18080:8080 | |
| env: | |
| OCTI_CORS_ALLOWED_ORIGINS: 'http://127.0.0.1:4173,http://localhost:4173' | |
| steps: | |
| - name: Checkout source code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: { persist-credentials: false } | |
| - name: Setup environment | |
| uses: ./.github/actions/common-setup | |
| - name: Wait for sync-server | |
| run: | | |
| set -euo pipefail | |
| for attempt in $(seq 1 30); do | |
| if curl -sf http://127.0.0.1:18080/v1/status > /dev/null; then | |
| echo "sync-server up after ${attempt}s" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "sync-server did not respond within 30s" >&2 | |
| docker logs ${{ job.services.sync-server.id }} || true | |
| exit 1 | |
| - name: Bootstrap fake phone peer | |
| env: | |
| SYNC_SERVER_URL: http://127.0.0.1:18080 | |
| OUTPUT_PATH: bootstrap-peer.json | |
| run: pnpm bootstrap-peer | |
| - name: Install Playwright browsers | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Build SPA (stable channel, tag version) | |
| env: | |
| VITE_CHANNEL: stable | |
| VITE_COMMIT_SHA: ${{ github.sha }} | |
| VITE_APP_VERSION: ${{ github.ref_name }} | |
| run: pnpm build | |
| - name: Run E2E screenshot capture | |
| env: | |
| BOOTSTRAP_PEER_FILE: bootstrap-peer.json | |
| run: pnpm e2e | |
| - name: Generate thumbnails | |
| run: pnpm exec node scripts/generate-thumbs.mjs | |
| - name: Upload screenshots artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: screenshots-${{ github.ref_name }} | |
| path: screenshots/ | |
| retention-days: 14 | |
| - name: Upload Playwright trace on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: playwright-trace-${{ github.run_id }} | |
| path: | | |
| test-results/ | |
| playwright-report/ | |
| if-no-files-found: ignore | |
| - name: Dump sync-server logs on failure | |
| if: failure() | |
| run: docker logs ${{ job.services.sync-server.id }} || true | |
| release-docker: | |
| needs: e2e | |
| name: Publish Docker image (ghcr.io) | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout source code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Log in to ghcr.io | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| # `:latest` only on suffix-free tags (vM.m.p) — RCs and betas don't become | |
| # the default. Today bump.sh always emits a suffix; this is forward-compat | |
| # for when we cut a stable 1.0.0. | |
| - name: Compute Docker tags | |
| id: tags | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| tags=("ghcr.io/${{ github.repository }}:${REF_NAME}") | |
| if [[ ! "${REF_NAME}" == *-* ]]; then | |
| tags+=("ghcr.io/${{ github.repository }}:latest") | |
| fi | |
| printf '%s\n' "${tags[@]}" | |
| # Join with newlines for docker/build-push-action's tags input. | |
| { | |
| echo "list<<EOF" | |
| printf '%s\n' "${tags[@]}" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Build and push image | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ steps.tags.outputs.list }} | |
| platforms: linux/amd64,linux/arm64 | |
| build-args: | | |
| VITE_BASE=/ | |
| VITE_CHANNEL=stable | |
| VITE_COMMIT_SHA=${{ github.sha }} | |
| VITE_APP_VERSION=${{ github.ref_name }} | |
| release-github: | |
| needs: release-docker | |
| name: Create GitHub release | |
| permissions: | |
| contents: write | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Checkout source code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup environment | |
| uses: ./.github/actions/common-setup | |
| - name: Build production bundle | |
| env: | |
| VITE_CHANNEL: stable | |
| VITE_COMMIT_SHA: ${{ github.sha }} | |
| VITE_APP_VERSION: ${{ github.ref_name }} | |
| run: pnpm build | |
| - name: Package dist tarball | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| tar -czf "octi-web-${REF_NAME}.tar.gz" -C dist . | |
| ls -la "octi-web-${REF_NAME}.tar.gz" | |
| # Use `gh release create` directly so the release is created WITH its assets | |
| # in a single API call. softprops/action-gh-release creates-then-attaches, | |
| # which fails on repos with "Immutable Releases" enabled (the release is | |
| # frozen at creation time and assets can't be added after the fact). | |
| - name: Create release with assets | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| flags=( | |
| --title "${REF_NAME}" | |
| --generate-notes | |
| ) | |
| # Pre-release for -rc/-beta tags (anything with a suffix); stable for vM.m.p. | |
| if [[ "${REF_NAME}" == *-* ]]; then | |
| flags+=(--prerelease) | |
| else | |
| flags+=(--latest) | |
| fi | |
| gh release create "${REF_NAME}" "${flags[@]}" "octi-web-${REF_NAME}.tar.gz" | |
| screenshots-pr: | |
| needs: release-github | |
| name: Open screenshots PR | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Mint App token | |
| # Same App as release-prepare — needed to bypass main branch ruleset | |
| # protection when pushing the screenshots/refresh branch. | |
| id: app-token | |
| uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| with: | |
| client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| - name: Resolve bot identity | |
| id: bot | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| APP_SLUG: ${{ steps.app-token.outputs.app-slug }} | |
| run: | | |
| set -euo pipefail | |
| user_id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id) | |
| echo "user_name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT" | |
| echo "user_email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT" | |
| - name: Checkout main (App-authenticated) | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: main | |
| fetch-depth: 1 | |
| persist-credentials: true | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Setup environment | |
| uses: ./.github/actions/common-setup | |
| - name: Download screenshots artifact (from this run) | |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | |
| with: | |
| name: screenshots-${{ github.ref_name }} | |
| path: screenshots/ | |
| - name: Re-generate thumbnails defensively | |
| run: pnpm exec node scripts/generate-thumbs.mjs | |
| - name: Open / refresh PR | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REF_NAME: ${{ github.ref_name }} | |
| BOT_USER_NAME: ${{ steps.bot.outputs.user_name }} | |
| BOT_USER_EMAIL: ${{ steps.bot.outputs.user_email }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "${BOT_USER_NAME}" | |
| git config user.email "${BOT_USER_EMAIL}" | |
| # Fixed branch name → at most one open "screenshots/refresh" PR exists. | |
| # Each new release force-updates the branch with the new PNGs. | |
| branch="screenshots/refresh" | |
| git checkout -B "${branch}" | |
| git add screenshots/ | |
| if git diff --staged --quiet; then | |
| echo "No screenshot changes — nothing to PR." | |
| exit 0 | |
| fi | |
| git commit -m "Refresh screenshots for ${REF_NAME}" | |
| git push --force --set-upstream origin "${branch}" | |
| # Open or update the PR (gh pr create errors if one already exists for | |
| # the branch; in that case the push above already updated the diff). | |
| if gh pr view "${branch}" >/dev/null 2>&1; then | |
| echo "PR already open for ${branch}; force-push updated it." | |
| else | |
| gh pr create --base main --head "${branch}" \ | |
| --title "Refresh screenshots (${REF_NAME})" \ | |
| --body "Automated refresh from the ${REF_NAME} release. Review the diffs and merge if the new shots look right." | |
| fi |