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
6 changes: 5 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,19 @@ jobs:
include:
- runner: macos-latest
args: --target aarch64-apple-darwin --bundles app,dmg
prerelease_args: --target aarch64-apple-darwin --bundles app,dmg
asset_platform: macos
- runner: macos-latest
args: --target x86_64-apple-darwin --bundles app,dmg
prerelease_args: --target x86_64-apple-darwin --bundles app,dmg
asset_platform: macos
- runner: ubuntu-22.04
args: --bundles appimage,deb,rpm
prerelease_args: --bundles appimage,deb,rpm
asset_platform: linux
- runner: windows-latest
args: --bundles nsis,msi
prerelease_args: --bundles nsis
asset_platform: windows
runs-on: ${{ matrix.runner }}
steps:
Expand Down Expand Up @@ -239,7 +243,7 @@ jobs:
updaterJsonPreferNsis: false
retryAttempts: 3
releaseAssetNamePattern: skill-deck_[version]_${{ matrix.asset_platform }}_[arch][setup][ext]
args: ${{ matrix.args }}
args: ${{ needs.validate.outputs.prerelease == 'true' && matrix.prerelease_args || matrix.args }}

verify-release:
needs: [validate, prepare-release, build-release]
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ docker run --rm \

质量检查通过后,工作流会创建或复用草稿 Release,并由官方 `tauri-action` 构建和上传各平台安装包、更新签名与 `latest.json`。自动校验完成后,由维护者检查版本、Release 正文、安装包和更新信息,再通过 GitHub 界面公开发布。

预发布版本在 Windows 上提供 NSIS 安装包;稳定版本同时提供 NSIS 和 MSI。MSI 的数值版本不能表达 SemVer 预发布标识,发布工作流和资产验证器根据版本类型使用对应的安装包契约。

Release 正文来自标签对应提交中的 `CHANGELOG.md`。公开安装包名称由 `README.md` 和 `README.zh-CN.md` 维护,完整资产和更新清单契约由 `scripts/verify-release-assets.mjs` 及其测试维护。该契约要求 `latest.json` 不超过 1 MiB,更新器引用的安装资产不超过 256 MiB。修改发布流程时,需要同步更新工作流、验证脚本、脚本测试和受影响的用户文档。

## 提交内容
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Skill availability is determined by its installation location and agent associat

Download the installer for your platform from [GitHub Releases](https://github.com/hccake/skill-deck/releases):

- **Windows**: `skill-deck_x.x.x_windows_x64-setup.exe` or `skill-deck_x.x.x_windows_x64.msi`
- **Windows**: `skill-deck_x.x.x_windows_x64-setup.exe`; stable releases also provide `skill-deck_x.x.x_windows_x64.msi`
- **macOS Apple Silicon**: `skill-deck_x.x.x_macos_aarch64.dmg`
- **macOS Intel**: `skill-deck_x.x.x_macos_x64.dmg`
> macOS builds are currently unsigned. If macOS blocks the app after installation, run:
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Skill 是否可用取决于它的安装位置以及 Agent 是否读取该位置

从 [GitHub Releases](https://github.com/hccake/skill-deck/releases) 下载对应平台的安装包:

- **Windows**:`skill-deck_x.x.x_windows_x64-setup.exe` `skill-deck_x.x.x_windows_x64.msi`
- **Windows**:`skill-deck_x.x.x_windows_x64-setup.exe`;稳定版本还会提供 `skill-deck_x.x.x_windows_x64.msi`
- **macOS Apple Silicon**:`skill-deck_x.x.x_macos_aarch64.dmg`
- **macOS Intel**:`skill-deck_x.x.x_macos_x64.dmg`
> macOS 构建目前没有 Apple 开发者签名。如果安装后被系统拦截,可执行:
Expand Down
41 changes: 21 additions & 20 deletions scripts/__tests__/release-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { readFile } from "node:fs/promises";
import test from "node:test";
import { parse } from "yaml";

import { documentedInstallerNames } from "../verify-release-assets.mjs";
import {
documentedInstallerNames,
expectedReleaseAssetNames,
} from "../verify-release-assets.mjs";

const workflowUrl = new URL(
"../../.github/workflows/release.yml",
Expand Down Expand Up @@ -179,21 +182,22 @@ test("release workflow prepares one draft and lets tauri-action upload each plat
assert.equal(workflow.jobs.aggregate, undefined);
});

test("release workflow builds every public installer with explicit bundle arguments", async () => {
const workflow = await readFile(workflowUrl, "utf8");
const build =
workflow.match(/\n build-release:[\s\S]*?(?=\n verify-release:)/)?.[0] ??
"";
test("release workflow omits MSI for prereleases and retains it for stable releases", async () => {
const workflow = await readWorkflow(workflowUrl);
const build = workflow.jobs["build-release"];
const windows = build.strategy.matrix.include.find(
(entry) => entry.runner === "windows-latest",
);
const action = build.steps.find((step) =>
step.uses?.startsWith("tauri-apps/tauri-action@"),
);

assert.match(build, /args: --target aarch64-apple-darwin --bundles app,dmg/);
assert.match(build, /args: --target x86_64-apple-darwin --bundles app,dmg/);
assert.match(build, /args: --bundles appimage,deb,rpm/);
assert.match(build, /args: --bundles nsis,msi/);
assert.match(
build,
/releaseId: \$\{\{ needs\.prepare-release\.outputs\.release_id \}\}/,
assert.equal(windows.args, "--bundles nsis,msi");
assert.equal(windows.prerelease_args, "--bundles nsis");
assert.equal(
action.with.args,
"${{ needs.validate.outputs.prerelease == 'true' && matrix.prerelease_args || matrix.args }}",
);
assert.doesNotMatch(workflow, /Expected 8 installer\/signature files/);
});

test("release helpers come from the workflow commit instead of the tagged application commit", async () => {
Expand Down Expand Up @@ -304,14 +308,11 @@ test("release workflow derives and enforces GitHub prerelease state from the ver
});

test("release verification checks the complete remote asset and updater manifest contract", async () => {
const [workflow, verifier] = await Promise.all([
readFile(workflowUrl, "utf8"),
readFile(releaseVerifierUrl, "utf8"),
]);
const workflow = await readFile(workflowUrl, "utf8");
const verify = workflow.match(/\n verify-release:[\s\S]*/)?.[0] ?? "";

assert.match(verifier, /Expected 17 Release assets/i);
assert.match(verifier, /Expected 11 updater platform keys/i);
assert.equal(expectedReleaseAssetNames("1.7.0-beta.4").length, 15);
assert.equal(expectedReleaseAssetNames("1.7.0").length, 17);
assert.match(verify, /gh release download[\s\S]*latest\.json/);
assert.match(verify, /gh release download[\s\S]*\*\.sig/);
assert.match(verify, /--mode complete/);
Expand Down
99 changes: 77 additions & 22 deletions scripts/__tests__/verify-release-assets.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ const verifierPath = fileURLToPath(
new URL("../verify-release-assets.mjs", import.meta.url),
);
const version = "1.7.0-beta.4";
const stableVersion = "1.7.0";
const tag = `v${version}`;
const repository = "hccake/skill-deck";
const notes = "## Changes\n\n- Complete updater matrix.\n";

const platformAssets = Object.freeze({
const commonPlatformAssets = Object.freeze({
"darwin-aarch64": `skill-deck_${version}_macos_aarch64.app.tar.gz`,
"darwin-aarch64-app": `skill-deck_${version}_macos_aarch64.app.tar.gz`,
"darwin-x86_64": `skill-deck_${version}_macos_x64.app.tar.gz`,
Expand All @@ -31,12 +32,30 @@ const platformAssets = Object.freeze({
"linux-x86_64-appimage": `skill-deck_${version}_linux_amd64.AppImage`,
"linux-x86_64-deb": `skill-deck_${version}_linux_amd64.deb`,
"linux-x86_64-rpm": `skill-deck_${version}_linux_x86_64.rpm`,
"windows-x86_64": `skill-deck_${version}_windows_x64.msi`,
"windows-x86_64-msi": `skill-deck_${version}_windows_x64.msi`,
"windows-x86_64": `skill-deck_${version}_windows_x64-setup.exe`,
"windows-x86_64-nsis": `skill-deck_${version}_windows_x64-setup.exe`,
});

async function createCompleteFixture() {
function platformAssetsFor(releaseVersion) {
const renderVersion = (name) => name.replaceAll(version, releaseVersion);
const assets = Object.fromEntries(
Object.entries(commonPlatformAssets).map(([platform, name]) => [
platform,
renderVersion(name),
]),
);
if (!releaseVersion.includes("-")) {
const msi = `skill-deck_${releaseVersion}_windows_x64.msi`;
assets["windows-x86_64"] = msi;
assets["windows-x86_64-msi"] = msi;
}
return assets;
}

async function createCompleteFixture(releaseVersion = version) {
const fixtureTag = `v${releaseVersion}`;
const fixturePrerelease = releaseVersion.includes("-");
const platformAssets = platformAssetsFor(releaseVersion);
const root = await mkdtemp(path.join(os.tmpdir(), "skill-deck-release-"));
const signaturesDirectory = path.join(root, "release-downloads");
await mkdir(signaturesDirectory);
Expand All @@ -54,15 +73,15 @@ async function createCompleteFixture() {
);
}

const releaseAssetNames = expectedReleaseAssetNames(version);
const releaseAssetNames = expectedReleaseAssetNames(releaseVersion);
const assetApiUrls = new Map(
releaseAssetNames.map((name, index) => [
name,
`https://api.github.com/repos/hccake/skill-deck/releases/assets/${1000 + index}`,
]),
);
const manifest = {
version,
version: releaseVersion,
notes,
pub_date: "2026-08-08T12:00:00.000Z",
platforms: Object.fromEntries(
Expand All @@ -83,9 +102,9 @@ async function createCompleteFixture() {
await writeFile(notesFile, notes);

const release = {
tagName: tag,
tagName: fixtureTag,
isDraft: true,
isPrerelease: true,
isPrerelease: fixturePrerelease,
body: notes.trimEnd(),
assets: releaseAssetNames.map((name) => ({
name,
Expand All @@ -101,10 +120,13 @@ async function createCompleteFixture() {
notesFile,
manifest,
release,
version: releaseVersion,
tag: fixtureTag,
expectedPrerelease: fixturePrerelease,
};
}

test("declares the 17 uploaded assets and seven documented installers", () => {
test("prereleases omit MSI assets that cannot represent SemVer prerelease identifiers", () => {
assert.deepEqual(expectedReleaseAssetNames(version), [
"latest.json",
`skill-deck_${version}_linux_amd64.AppImage`,
Expand All @@ -121,17 +143,31 @@ test("declares the 17 uploaded assets and seven documented installers", () => {
`skill-deck_${version}_macos_x64.dmg`,
`skill-deck_${version}_windows_x64-setup.exe`,
`skill-deck_${version}_windows_x64-setup.exe.sig`,
`skill-deck_${version}_windows_x64.msi`,
`skill-deck_${version}_windows_x64.msi.sig`,
]);
assert.deepEqual(documentedInstallerNames(version), [
`skill-deck_${version}_macos_aarch64.dmg`,
`skill-deck_${version}_macos_x64.dmg`,
`skill-deck_${version}_linux_amd64.AppImage`,
`skill-deck_${version}_linux_amd64.deb`,
`skill-deck_${version}_linux_x86_64.rpm`,
`skill-deck_${version}_windows_x64-setup.exe`,
`skill-deck_${version}_windows_x64.msi`,
});

test("stable releases retain the complete MSI and NSIS asset contract", () => {
const assetNames = expectedReleaseAssetNames(stableVersion);

assert.equal(assetNames.length, 17);
assert.ok(assetNames.includes(`skill-deck_${stableVersion}_windows_x64.msi`));
assert.ok(
assetNames.includes(`skill-deck_${stableVersion}_windows_x64.msi.sig`),
);
assert.ok(
assetNames.includes(`skill-deck_${stableVersion}_windows_x64-setup.exe`),
);
});

test("documents every stable installer offered to users", () => {
assert.deepEqual(documentedInstallerNames(stableVersion), [
`skill-deck_${stableVersion}_macos_aarch64.dmg`,
`skill-deck_${stableVersion}_macos_x64.dmg`,
`skill-deck_${stableVersion}_linux_amd64.AppImage`,
`skill-deck_${stableVersion}_linux_amd64.deb`,
`skill-deck_${stableVersion}_linux_x86_64.rpm`,
`skill-deck_${stableVersion}_windows_x64-setup.exe`,
`skill-deck_${stableVersion}_windows_x64.msi`,
]);
});

Expand Down Expand Up @@ -212,6 +248,25 @@ test("accepts the complete official tauri-action release contract", async () =>
}
});

test("accepts the complete stable release contract with MSI and NSIS", async () => {
const fixture = await createCompleteFixture(stableVersion);
try {
await verifyReleaseAssets({
mode: "complete",
release: fixture.release,
manifest: fixture.manifest,
signaturesDirectory: fixture.signaturesDirectory,
notes,
version: fixture.version,
tag: fixture.tag,
repository,
expectedPrerelease: fixture.expectedPrerelease,
});
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});

test("rejects an updater manifest larger than 1 MiB", async () => {
const fixture = await createCompleteFixture();
try {
Expand Down Expand Up @@ -240,7 +295,7 @@ test("rejects an updater manifest larger than 1 MiB", async () => {
test("rejects an updater asset larger than 256 MiB", async () => {
const fixture = await createCompleteFixture();
try {
const assetName = platformAssets["linux-x86_64"];
const assetName = platformAssetsFor(version)["linux-x86_64"];
fixture.release.assets.find((asset) => asset.name === assetName).size =
MAX_UPDATER_ASSET_BYTES + 1;
await assert.rejects(
Expand All @@ -267,7 +322,7 @@ for (const [name, mutate, expected] of [
[
"missing Release asset",
({ release }) => release.assets.pop(),
/Expected 17 Release assets/i,
/Expected 15 Release assets/i,
],
[
"empty Release asset",
Expand All @@ -279,7 +334,7 @@ for (const [name, mutate, expected] of [
[
"missing updater platform",
({ manifest }) => delete manifest.platforms["linux-x86_64-deb"],
/Expected 11 updater platform keys/i,
/Expected 10 updater platform keys/i,
],
[
"wrong updater URL",
Expand Down
Loading
Loading