Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# PawWork Release Checklist

Use this checklist for stable PawWork desktop releases.

## 1. Prepare

- Confirm the release PR targets `dev` and all required CI checks are green.
- Confirm the version bump is merged into `dev`.
- Confirm `dev` is up to date locally:

```bash
git switch dev
git pull --ff-only
```

- Confirm the release tag does not already exist:

```bash
git fetch origin --tags
git tag -l vX.Y.Z
gh release view vX.Y.Z --repo Astro-Han/pawwork
```

## 2. Draft Release Notes Before Publishing

Create or update the GitHub Release body before publishing the release. Use English first, direct user download links, then a short Chinese section.

```md
## Downloads

- [macOS Apple Silicon](https://github.com/Astro-Han/pawwork/releases/download/vX.Y.Z/pawwork-mac-arm64.dmg)
- [macOS Intel](https://github.com/Astro-Han/pawwork/releases/download/vX.Y.Z/pawwork-mac-x64.dmg)
- [Windows](https://github.com/Astro-Han/pawwork/releases/download/vX.Y.Z/pawwork-win-x64.exe)

## Highlights

Comment thread
Astro-Han marked this conversation as resolved.
- User-facing changes, bug fixes, or packaging fixes.

## Runtime And Maintenance

- Build, updater, notarization, dependency, or CI maintenance.

## Verification

- macOS Apple Silicon submit/finalize completed successfully, including notarization.
- macOS Intel submit/finalize completed successfully, including notarization.
- Windows x64 release build completed successfully.
- vX.Y.Z is published as the latest stable release.

## 中文版本

Comment thread
Astro-Han marked this conversation as resolved.
### 下载

- [macOS Apple 芯片](https://github.com/Astro-Han/pawwork/releases/download/vX.Y.Z/pawwork-mac-arm64.dmg)
- [macOS Intel 芯片](https://github.com/Astro-Han/pawwork/releases/download/vX.Y.Z/pawwork-mac-x64.dmg)
- [Windows](https://github.com/Astro-Han/pawwork/releases/download/vX.Y.Z/pawwork-win-x64.exe)

Comment thread
Astro-Han marked this conversation as resolved.
### 主要更新

- 用一句话概括主要变化。
```

Do not rely on the GitHub Assets list as the primary download UI. It mixes user installers with updater metadata, so direct links make the intended downloads clear.

## 3. Build Release Artifacts

Submit macOS notarization for both architectures:

```bash
gh workflow run build.yml --repo Astro-Han/pawwork --ref dev -f phase=submit -f channel=prod -f target=macos -f arch=arm64
gh workflow run build.yml --repo Astro-Han/pawwork --ref dev -f phase=submit -f channel=prod -f target=macos -f arch=x64
```

Record each submit run's source run ID, source run attempt, source ref, source sha, workflow ref, workflow sha, and Apple submission ID from the workflow summary.

The submit workflow summary should include values like this:

```text
source_run_id: 123456789
source_run_attempt: 1
source_ref: dev
source_sha: 0123456789abcdef0123456789abcdef01234567
source_workflow_ref: dev
source_workflow_sha: 0123456789abcdef0123456789abcdef01234567
Comment thread
Astro-Han marked this conversation as resolved.
submission_id: 00000000-0000-0000-0000-000000000000
```

Finalize each macOS architecture with the exact command emitted by its submit workflow summary. If writing the commands manually, keep the arm64 and x64 values separate:

For arm64, replace `ARM64_SOURCE_RUN_ID` with the `source_run_id` value from the arm64 submit summary, and apply the same mapping for the other `ARM64_` placeholders. Repeat with the x64 submit summary for the `X64_` placeholders.

```bash
gh workflow run build.yml --repo Astro-Han/pawwork --ref ARM64_SOURCE_WORKFLOW_REF -f phase=finalize -f channel=prod -f arch=arm64 -f source_run_id=ARM64_SOURCE_RUN_ID -f source_run_attempt=ARM64_SOURCE_RUN_ATTEMPT -f source_ref=ARM64_SOURCE_REF -f source_sha=ARM64_SOURCE_SHA -f source_workflow_ref=ARM64_SOURCE_WORKFLOW_REF -f source_workflow_sha=ARM64_SOURCE_WORKFLOW_SHA -f submission_id=ARM64_SUBMISSION_ID
gh workflow run build.yml --repo Astro-Han/pawwork --ref X64_SOURCE_WORKFLOW_REF -f phase=finalize -f channel=prod -f arch=x64 -f source_run_id=X64_SOURCE_RUN_ID -f source_run_attempt=X64_SOURCE_RUN_ATTEMPT -f source_ref=X64_SOURCE_REF -f source_sha=X64_SOURCE_SHA -f source_workflow_ref=X64_SOURCE_WORKFLOW_REF -f source_workflow_sha=X64_SOURCE_WORKFLOW_SHA -f submission_id=X64_SUBMISSION_ID
```

Build and publish the Windows installer:

```bash
gh workflow run build.yml --repo Astro-Han/pawwork --ref dev -f phase=full -f channel=prod -f target=windows -f arch=x64
```

Comment thread
Astro-Han marked this conversation as resolved.
## 4. Publish

Verify the draft release has all expected user-facing installers before publishing:

```bash
gh release view vX.Y.Z --repo Astro-Han/pawwork --json isDraft,isPrerelease,assets,url
```

Publish the release as the latest stable release:

```bash
gh release edit vX.Y.Z --repo Astro-Han/pawwork --draft=false --latest --prerelease=false
```

## 5. Post-Release Verification

Run the verification helper:

```bash
export GH_TOKEN="$(gh auth token)"
bun packages/desktop-electron/scripts/verify-release.ts vX.Y.Z
```

`GH_TOKEN` is recommended so GitHub API requests use the authenticated rate limit.

The helper verifies:

- The GitHub Release is not a draft.
- The GitHub Release is not a prerelease.
- `pawwork-mac-arm64.dmg` exists.
- `pawwork-mac-x64.dmg` exists.
- `pawwork-win-x64.exe` exists.
- updater `.zip` and `.blockmap` assets exist.
- `latest.yml` points to `pawwork-win-x64.exe`.
- `latest-mac.yml` includes both `pawwork-mac-arm64.zip` and `pawwork-mac-x64.zip`.

Keep `.zip`, `.blockmap`, and `latest*.yml` assets unless updater requirements are proven safe without them.

If verification fails, check the reported missing or malformed asset first, rerun only the affected build phase, and publish the release only after the verification helper passes.

## 6. Close Release Issues

Only close release-blocking issues after post-release verification passes. Leave a short comment with the release link and the verified artifact names.
248 changes: 248 additions & 0 deletions packages/desktop-electron/scripts/verify-release.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { afterEach, describe, expect, test } from "bun:test"

import {
fetchJson,
fetchText,
normalizeTag,
parseUpdaterFileUrls,
verifyReleasePayload,
type GithubRelease,
} from "./verify-release"

const originalFetch = globalThis.fetch

afterEach(() => {
globalThis.fetch = originalFetch
})

const baseRelease: GithubRelease = {
Comment thread
Astro-Han marked this conversation as resolved.
tag_name: "v0.2.6",
draft: false,
prerelease: false,
assets: [
{
name: "pawwork-mac-arm64.dmg",
browser_download_url: "https://example.com/pawwork-mac-arm64.dmg",
},
{
name: "pawwork-mac-arm64.zip",
browser_download_url: "https://example.com/pawwork-mac-arm64.zip",
},
{
name: "pawwork-mac-arm64.zip.blockmap",
browser_download_url: "https://example.com/pawwork-mac-arm64.zip.blockmap",
},
{
name: "pawwork-mac-x64.dmg",
browser_download_url: "https://example.com/pawwork-mac-x64.dmg",
},
{
name: "pawwork-mac-x64.zip",
browser_download_url: "https://example.com/pawwork-mac-x64.zip",
},
{
name: "pawwork-mac-x64.zip.blockmap",
browser_download_url: "https://example.com/pawwork-mac-x64.zip.blockmap",
},
{
name: "pawwork-win-x64.exe",
browser_download_url: "https://example.com/pawwork-win-x64.exe",
},
{
name: "pawwork-win-x64.exe.blockmap",
browser_download_url: "https://example.com/pawwork-win-x64.exe.blockmap",
},
{
name: "latest.yml",
browser_download_url: "https://example.com/latest.yml",
},
{
name: "latest-mac.yml",
browser_download_url: "https://example.com/latest-mac.yml",
},
],
}

describe("verify-release", () => {
test("normalizes release tags", () => {
expect(normalizeTag("0.2.6")).toBe("v0.2.6")
expect(normalizeTag("v0.2.6")).toBe("v0.2.6")
expect(() => normalizeTag("vv0.2.6")).toThrow("Invalid release tag")
expect(() => normalizeTag("abc")).toThrow("Invalid release tag")
})

test("parses updater file urls and path entries", () => {
expect(
parseUpdaterFileUrls(`version: 0.2.6
files:
- url: pawwork-mac-arm64.zip
size: 1
- url: pawwork-mac-x64.zip
size: 2
path: pawwork-mac-arm64.zip
`),
).toEqual(["pawwork-mac-arm64.zip", "pawwork-mac-x64.zip", "pawwork-mac-arm64.zip"])
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("parses quoted updater file urls and path entries", () => {
expect(
parseUpdaterFileUrls(`files:
- url: "pawwork-mac-arm64.zip"
- url: 'pawwork-mac-x64.zip' # Intel macOS updater asset
- url: "pawwork-mac#arm64.zip"
path: "pawwork-win-x64.exe" # Windows updater asset
`),
).toEqual(["pawwork-mac-arm64.zip", "pawwork-mac-x64.zip", "pawwork-mac#arm64.zip", "pawwork-win-x64.exe"])
})

test("keeps inline comments outside escaped quoted values", () => {
expect(
parseUpdaterFileUrls(String.raw`files:
- url: "pawwork-mac\"arm64.zip" # comment
- url: "pawwork-mac\\"
path: pawwork-win-x64.exe
`),
).toEqual([String.raw`pawwork-mac\"arm64.zip`, String.raw`pawwork-mac\\`, "pawwork-win-x64.exe"])
})

test("accepts a stable release with expected assets and updater metadata", () => {
expect(
verifyReleasePayload({
release: baseRelease,
latestYml: "files:\n - url: pawwork-win-x64.exe\n",
latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n",
}),
).toEqual([])
})

test("accepts updater metadata entries with full download URLs", () => {
expect(
verifyReleasePayload({
release: baseRelease,
latestYml: "files:\n - url: https://github.com/Astro-Han/pawwork/releases/download/v0.2.6/pawwork-win-x64.exe\n",
latestMacYml:
"files:\n - url: https://github.com/Astro-Han/pawwork/releases/download/v0.2.6/pawwork-mac-arm64.zip\n - url: https://github.com/Astro-Han/pawwork/releases/download/v0.2.6/pawwork-mac-x64.zip\n",
}),
).toEqual([])
})

test("reports missing macOS updater architecture metadata", () => {
expect(
verifyReleasePayload({
release: baseRelease,
latestYml: "files:\n - url: pawwork-win-x64.exe\n",
latestMacYml: "files:\n - url: pawwork-mac-x64.zip\n",
}),
).toContain("latest-mac.yml does not include pawwork-mac-arm64.zip")
})

test("reports updater metadata that points to a missing asset", () => {
expect(
verifyReleasePayload({
release: {
...baseRelease,
assets: baseRelease.assets.filter((asset) => asset.name !== "pawwork-mac-arm64.zip"),
},
latestYml: "files:\n - url: pawwork-win-x64.exe\n",
latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n",
}),
).toContain("latest-mac.yml references missing release asset: pawwork-mac-arm64.zip")
})

test("reports missing installer and updater sidecar assets", () => {
const failures = verifyReleasePayload({
release: {
...baseRelease,
assets: baseRelease.assets.filter(
(asset) => asset.name !== "pawwork-mac-arm64.dmg" && asset.name !== "pawwork-win-x64.exe.blockmap",
),
},
latestYml: "files:\n - url: pawwork-win-x64.exe\n",
latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n",
})

expect(failures).toContain("Missing release asset: pawwork-mac-arm64.dmg")
expect(failures).toContain("Missing release asset: pawwork-win-x64.exe.blockmap")
})

test("reports missing updater metadata assets without requiring metadata downloads", () => {
const failures = verifyReleasePayload({
release: {
...baseRelease,
assets: baseRelease.assets.filter((asset) => asset.name !== "latest.yml" && asset.name !== "latest-mac.yml"),
},
latestYml: "",
latestMacYml: "",
})

expect(failures).toContain("Missing release asset: latest.yml")
expect(failures).toContain("Missing release asset: latest-mac.yml")
expect(failures).toContain("latest.yml does not include pawwork-win-x64.exe")
expect(failures).toContain("latest-mac.yml does not include pawwork-mac-arm64.zip")
expect(failures).toContain("latest-mac.yml does not include pawwork-mac-x64.zip")
})

test("reports draft releases", () => {
expect(
verifyReleasePayload({
release: { ...baseRelease, draft: true },
latestYml: "files:\n - url: pawwork-win-x64.exe\n",
latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n",
}),
).toContain("Release v0.2.6 is still a draft")
})

test("reports prerelease releases", () => {
expect(
verifyReleasePayload({
release: { ...baseRelease, prerelease: true },
latestYml: "files:\n - url: pawwork-win-x64.exe\n",
latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n",
}),
).toContain("Release v0.2.6 is marked as a prerelease")
})

test("reports malformed updater metadata as missing required updater entries", () => {
const failures = verifyReleasePayload({
release: baseRelease,
latestYml: "files:\n - broken: pawwork-win-x64.exe\n",
latestMacYml: "files:\n - broken: pawwork-mac-arm64.zip\n",
})

expect(failures).toContain("latest.yml does not include pawwork-win-x64.exe")
expect(failures).toContain("latest-mac.yml does not include pawwork-mac-arm64.zip")
expect(failures).toContain("latest-mac.yml does not include pawwork-mac-x64.zip")
})

test("fetchText reports GitHub rate limit headers on HTTP errors", async () => {
globalThis.fetch = (() =>
Promise.resolve(
new Response("rate limited", {
status: 403,
statusText: "Forbidden",
headers: {
"x-ratelimit-remaining": "0",
"x-ratelimit-reset": "1234567890",
},
}),
)) as typeof fetch

await expect(fetchText("https://api.github.com/example")).rejects.toThrow("rate limit remaining: 0")
})

test("fetchText reports network failures with the request URL", async () => {
globalThis.fetch = (() => Promise.reject(new Error("socket hang up"))) as typeof fetch

await expect(fetchText("https://api.github.com/example")).rejects.toThrow(
"Failed to fetch https://api.github.com/example: socket hang up",
)
})

test("fetchJson reports invalid JSON with the request URL", async () => {
globalThis.fetch = (() => Promise.resolve(new Response("not json", { status: 200 }))) as typeof fetch

await expect(fetchJson("https://api.github.com/example")).rejects.toThrow(
"Failed to parse JSON from https://api.github.com/example",
)
})
})
Loading
Loading