Add release skill

This commit is contained in:
Benjamin Shafii
2026-01-13 18:54:05 -08:00
parent aeab2ec71a
commit f244691026
4 changed files with 197 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# release
Create a human-friendly **unsigned macOS DMG** release for OpenWork (or similar Tauri apps), and publish it on GitHub.
This skill is intentionally lightweight: its mostly a checklist + a couple of sanity scripts.
## What this skill is for
- You have a Tauri app.
- You want to publish a **DMG** on GitHub Releases.
- You are **not** code signing / notarizing yet (so macOS will warn users).
## Prereqs
- `pnpm`
- Rust toolchain (`cargo`, `rustc`)
- `gh` authenticated (`gh auth status`)
- macOS tools: `codesign`, `spctl`, `hdiutil`
## Release checklist (recommended)
### 1) Clean working tree
```bash
git status
```
### 2) Bump version everywhere
- `package.json` (`version`)
- `src-tauri/tauri.conf.json` (`version`)
- `src-tauri/Cargo.toml` (`version`)
### 3) Validate builds
```bash
pnpm typecheck
pnpm build:web
cargo check --manifest-path src-tauri/Cargo.toml
```
### 4) Build DMG
```bash
pnpm tauri build --bundles dmg
```
This should produce something like:
- `src-tauri/target/release/bundle/dmg/OpenWork_<version>_aarch64.dmg`
### 5) Verify “unsigned” state
Unsigned here means: **not Developer ID signed / not notarized**.
Quick checks:
```bash
# mount the dmg read-only
hdiutil attach -nobrowse -readonly "src-tauri/target/release/bundle/dmg/<DMG_NAME>.dmg"
# verify signature details (expect ad-hoc or not notarized)
codesign -dv --verbose=4 "/Volumes/<VOLUME>/<APP>.app"
# gatekeeper assessment (expect rejected)
spctl -a -vv "/Volumes/<VOLUME>/<APP>.app" || true
# unmount
hdiutil detach "/Volumes/<VOLUME>"
```
### 6) Tag + push
```bash
git commit -am "Prepare vX.Y.Z release"
git tag -a vX.Y.Z -m "OpenWork vX.Y.Z"
git push
git push origin vX.Y.Z
```
### 7) Create / update GitHub Release
```bash
gh release create vX.Y.Z \
--title "OpenWork vX.Y.Z" \
--notes "<human summary>"
gh release upload vX.Y.Z "src-tauri/target/release/bundle/dmg/<DMG_NAME>.dmg" --clobber
```
## Local helper scripts
- `bun .opencode/skill/release/first-call.ts` checks prerequisites and prints the current version.
## Notes
- If you later add signing/notarization, this skill should be updated to include that flow.

View File

@@ -0,0 +1,37 @@
import { spawn } from "node:child_process";
export async function run(
command: string,
args: string[],
options?: { cwd?: string; allowFailure?: boolean },
): Promise<{ ok: boolean; code: number; stdout: string; stderr: string }> {
const cwd = options?.cwd;
const child = spawn(command, args, {
cwd,
stdio: ["ignore", "pipe", "pipe"],
env: process.env,
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (d) => (stdout += d));
child.stderr.on("data", (d) => (stderr += d));
const code = await new Promise<number>((resolve) => {
child.on("close", (c) => resolve(c ?? -1));
});
const ok = code === 0;
if (!ok && !options?.allowFailure) {
throw new Error(
`Command failed (${code}): ${command} ${args.join(" ")}\n${stderr || stdout}`,
);
}
return { ok, code, stdout, stderr };
}

View File

@@ -0,0 +1,35 @@
import { readFile } from "node:fs/promises";
import { loadEnv } from "./load-env";
import { run } from "./client";
async function main() {
await loadEnv();
await run("gh", ["auth", "status"], { allowFailure: false });
const pkgRaw = await readFile("package.json", "utf8");
const pkg = JSON.parse(pkgRaw) as { name?: string; version?: string };
console.log(
JSON.stringify(
{
ok: true,
package: pkg.name ?? null,
version: pkg.version ?? null,
next: [
"pnpm typecheck",
"pnpm tauri build --bundles dmg",
"gh release upload vX.Y.Z <dmg> --clobber",
],
},
null,
2,
),
);
}
main().catch((e) => {
const message = e instanceof Error ? e.message : String(e);
console.error(message);
process.exit(1);
});

View File

@@ -0,0 +1,28 @@
import { run } from "./client";
const REQUIRED = [
"pnpm",
"cargo",
"gh",
"hdiutil",
"codesign",
"spctl",
];
export async function loadEnv() {
const missing: string[] = [];
for (const bin of REQUIRED) {
try {
await run("/usr/bin/env", ["bash", "-lc", `command -v ${bin}`], { allowFailure: false });
} catch {
missing.push(bin);
}
}
if (missing.length) {
throw new Error(`Missing required tools: ${missing.join(", ")}`);
}
return { ok: true as const };
}