name: Release KoalaSync on: push: tags: - 'v*' concurrency: group: release-${{ github.ref_name }} cancel-in-progress: false jobs: preflight: runs-on: ubuntu-latest permissions: contents: read checks: read outputs: version: ${{ steps.release-ref.outputs.version }} tag-commit: ${{ steps.release-ref.outputs.tag_commit }} steps: - name: Checkout release tag uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: '24' cache: 'npm' cache-dependency-path: | package-lock.json server/package-lock.json - name: Validate annotated tag, main commit, and required checks id: release-ref run: node scripts/release-preflight.mjs env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install root dependencies run: npm ci - name: Install server dependencies run: npm ci working-directory: server - name: Run complete release verification run: npm run verify - name: Install Playwright browsers run: npx playwright install --with-deps chromium chromium-headless-shell firefox webkit - name: Run browser E2E suite run: npm run test:e2e - name: Build relay container without publishing run: docker build --file server/Dockerfile --tag koalasync-release-preflight . - name: Smoke-test relay container run: | CONTAINER_ID=$(docker run --detach --publish 127.0.0.1::3000 --env SERVER_SALT=release-preflight-salt-with-more-than-thirty-two-chars koalasync-release-preflight) trap 'docker rm --force "$CONTAINER_ID" >/dev/null 2>&1 || true' EXIT HOST_PORT=$(docker port "$CONTAINER_ID" 3000/tcp | sed 's/.*://') for attempt in $(seq 1 30); do if curl --fail --silent "http://127.0.0.1:$HOST_PORT/health" >/dev/null; then exit 0 fi sleep 1 done docker logs "$CONTAINER_ID" exit 1 release-extension-draft: needs: preflight runs-on: ubuntu-latest permissions: contents: write id-token: write attestations: write steps: - name: Checkout release tag uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: '24' cache: 'npm' - name: Install dependencies and build release artifacts run: | npm ci npm run build:extension node website/build.cjs - name: Validate Firefox package run: npx addons-linter --warnings-as-errors dist/koalasync-firefox.zip - name: Generate extension checksums working-directory: dist run: sha256sum koalasync-chrome.zip koalasync-firefox.zip > SHA256SUMS - name: Validate release assets before publication run: node scripts/verify-published-release.mjs "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --asset-dir dist --skip-attestation - name: Attest extension archives uses: actions/attest@v4 with: subject-path: dist/koalasync-*.zip - name: Create draft GitHub release uses: softprops/action-gh-release@v3 with: files: | dist/koalasync-chrome.zip dist/koalasync-firefox.zip dist/SHA256SUMS name: Release ${{ github.ref_name }} generate_release_notes: true draft: true prerelease: false - name: Verify draft extension release run: node scripts/verify-published-release.mjs "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload website artifacts uses: actions/upload-artifact@v7 with: name: website-www path: website/www/ if-no-files-found: error release-server: needs: [preflight, release-extension-draft] runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write attestations: write steps: - name: Checkout release tag uses: actions/checkout@v7 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository }} tags: | type=raw,value=latest type=ref,event=tag - name: Build and push Docker image id: build uses: docker/build-push-action@v7 with: context: . file: server/Dockerfile push: true platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Attest relay image uses: actions/attest@v4 with: subject-name: ghcr.io/${{ github.repository }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true - name: Verify manifest, provenance, and running image env: DIGEST: ${{ steps.build.outputs.digest }} IMAGE: ghcr.io/${{ github.repository }} SOURCE_DIGEST: ${{ needs.preflight.outputs.tag-commit }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | docker buildx imagetools inspect "$IMAGE@$DIGEST" --raw > /tmp/koalasync-manifest.json node -e 'const m=require("/tmp/koalasync-manifest.json"); const p=new Set(m.manifests.map(x=>`${x.platform.os}/${x.platform.architecture}`)); for (const x of ["linux/amd64","linux/arm64"]) if(!p.has(x)) throw new Error(`missing platform ${x}`)' gh attestation verify "oci://$IMAGE@$DIGEST" --repo "$GITHUB_REPOSITORY" --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" --source-ref "$GITHUB_REF" --source-digest "$SOURCE_DIGEST" --deny-self-hosted-runners docker pull --platform linux/amd64 "$IMAGE@$DIGEST" CONTAINER_ID=$(docker run --detach --publish 127.0.0.1::3000 --env SERVER_SALT=release-smoke-salt-with-more-than-thirty-two-chars "$IMAGE@$DIGEST") trap 'docker rm --force "$CONTAINER_ID" >/dev/null 2>&1 || true' EXIT HOST_PORT=$(docker port "$CONTAINER_ID" 3000/tcp | sed 's/.*://') for attempt in $(seq 1 30); do if curl --fail --silent "http://127.0.0.1:$HOST_PORT/health" >/dev/null; then exit 0 fi sleep 1 done docker logs "$CONTAINER_ID" exit 1 finalize-release: needs: [preflight, release-extension-draft, release-server] runs-on: ubuntu-latest permissions: contents: write steps: - name: Publish verified GitHub release run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false --verify-tag env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}