mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
feat(release): add draft GitHub release flow
This commit is contained in:
45
.github/workflows/desktop-release.yml
vendored
45
.github/workflows/desktop-release.yml
vendored
@@ -99,6 +99,19 @@ jobs:
|
||||
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
||||
NODE
|
||||
|
||||
- name: Detect existing GitHub release state
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if release_draft="$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
release_draft="false"
|
||||
fi
|
||||
echo "RELEASE_DRAFT=$release_draft" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish macOS Tauri release
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -116,7 +129,7 @@ jobs:
|
||||
tagName: ${{ env.RELEASE_TAG }}
|
||||
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
||||
releaseBody: See the assets to download and install this version.
|
||||
releaseDraft: false
|
||||
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
||||
prerelease: false
|
||||
args: --target ${{ matrix.rust_target }}
|
||||
|
||||
@@ -195,6 +208,19 @@ jobs:
|
||||
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
||||
NODE
|
||||
|
||||
- name: Detect existing GitHub release state
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if release_draft="$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
release_draft="false"
|
||||
fi
|
||||
echo "RELEASE_DRAFT=$release_draft" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Linux Tauri release
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -206,7 +232,7 @@ jobs:
|
||||
tagName: ${{ env.RELEASE_TAG }}
|
||||
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
||||
releaseBody: See the assets to download and install this version.
|
||||
releaseDraft: false
|
||||
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
||||
prerelease: false
|
||||
args: --bundles appimage
|
||||
|
||||
@@ -280,6 +306,19 @@ jobs:
|
||||
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
||||
NODE
|
||||
|
||||
- name: Detect existing GitHub release state
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if release_draft="$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
release_draft="false"
|
||||
fi
|
||||
echo "RELEASE_DRAFT=$release_draft" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Windows Tauri release
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -291,6 +330,6 @@ jobs:
|
||||
tagName: ${{ env.RELEASE_TAG }}
|
||||
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
||||
releaseBody: See the assets to download and install this version.
|
||||
releaseDraft: false
|
||||
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
||||
prerelease: false
|
||||
args: --bundles nsis,msi
|
||||
|
||||
10
.github/workflows/release-notes-sync.yml
vendored
10
.github/workflows/release-notes-sync.yml
vendored
@@ -19,6 +19,11 @@ on:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
draft:
|
||||
description: "Create missing release as draft."
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: release-notes-sync-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }}
|
||||
@@ -43,6 +48,7 @@ jobs:
|
||||
REF: ${{ github.ref }}
|
||||
INPUT_TAG: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.inputs.tag }}
|
||||
INPUT_CREATE_IF_MISSING: ${{ github.event.inputs.create_if_missing }}
|
||||
INPUT_DRAFT: ${{ github.event.inputs.draft }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -64,4 +70,8 @@ jobs:
|
||||
args+=(--create-if-missing)
|
||||
fi
|
||||
|
||||
if [ "${INPUT_DRAFT:-false}" = "true" ]; then
|
||||
args+=(--draft)
|
||||
fi
|
||||
|
||||
node scripts/sync-release-notes-from-changelog.mjs "${args[@]}"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.19 - 2026-03-09
|
||||
### Added
|
||||
- Added a draft GitHub release flow so maintainers can upload and review desktop and Android release assets before publishing the final release.
|
||||
|
||||
## 0.1.18 - 2026-03-06
|
||||
### Added
|
||||
- Added a desktop `Mod+W` shortcut to close the current tab.
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@@ -212,6 +212,21 @@ npm run release:publish
|
||||
npm run release:push # pushes HEAD and current version tag (triggers desktop + Android APK + EAS mobile workflows)
|
||||
```
|
||||
|
||||
### Draft release flow
|
||||
|
||||
```bash
|
||||
# Stage a draft GitHub release with assets, but do not publish npm yet.
|
||||
npm run draft-release:patch
|
||||
|
||||
# Publish npm and promote the same GitHub draft release to final.
|
||||
npm run release:finalize
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- `draft-release:patch` bumps the version, runs release checks, pushes `HEAD` and the new `v*` tag, and creates the matching GitHub Release as a draft so desktop assets, APK uploads, and synced notes attach to that same draft release.
|
||||
- `release:finalize` requires that the current tag already has a GitHub draft release, publishes the npm packages for that exact version, and promotes the same GitHub Release from draft to published.
|
||||
- Use the same semver tag for both draft and final states; do not cut a second tag just to publish the release.
|
||||
|
||||
Notes:
|
||||
- `version:all:*` bumps the root package version and runs the root `version` lifecycle script to sync workspace versions and internal `@getpaseo/*` dependency versions before the release commit/tag is created.
|
||||
- `release:prepare` refreshes workspace `node_modules` links to prevent stale local package types during release checks.
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
"release:publish:dry-run": "npm publish --dry-run --workspace=@getpaseo/relay --access public && npm publish --dry-run --workspace=@getpaseo/server --access public && npm publish --dry-run --workspace=@getpaseo/cli --access public",
|
||||
"release:publish": "npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public",
|
||||
"release:push": "node scripts/push-current-release-tag.mjs",
|
||||
"draft-release:push": "node scripts/push-current-release-tag.mjs --draft-release",
|
||||
"draft-release:patch": "npm run version:all:patch && npm run release:check && npm run draft-release:push",
|
||||
"draft-release:minor": "npm run version:all:minor && npm run release:check && npm run draft-release:push",
|
||||
"draft-release:major": "npm run version:all:major && npm run release:check && npm run draft-release:push",
|
||||
"release:finalize": "node scripts/finalize-current-release.mjs",
|
||||
"release:patch": "npm run version:all:patch && npm run release:check && npm run release:publish && npm run release:push",
|
||||
"release:minor": "npm run version:all:minor && npm run release:check && npm run release:publish && npm run release:push",
|
||||
"release:major": "npm run version:all:major && npm run release:check && npm run release:publish && npm run release:push"
|
||||
|
||||
66
scripts/finalize-current-release.mjs
Normal file
66
scripts/finalize-current-release.mjs
Normal file
@@ -0,0 +1,66 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const rootPackagePath = path.join(rootDir, "package.json");
|
||||
|
||||
function usageAndExit(code = 0) {
|
||||
process.stderr.write(`Usage: node scripts/finalize-current-release.mjs\n`);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function run(cmd, args) {
|
||||
execFileSync(cmd, args, { cwd: rootDir, stdio: "inherit" });
|
||||
}
|
||||
|
||||
function runQuiet(cmd, args) {
|
||||
return execFileSync(cmd, args, { cwd: rootDir, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
function parseRepoSlug(remoteUrl) {
|
||||
const trimmed = remoteUrl.trim().replace(/\.git$/, "");
|
||||
const httpsMatch = trimmed.match(/github\.com[/:]([^/]+\/[^/]+)$/);
|
||||
if (httpsMatch) {
|
||||
return httpsMatch[1];
|
||||
}
|
||||
throw new Error(`Could not determine GitHub repo from origin URL: ${remoteUrl}`);
|
||||
}
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
const arg = process.argv[2];
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
usageAndExit(0);
|
||||
}
|
||||
usageAndExit(1);
|
||||
}
|
||||
|
||||
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8"));
|
||||
const version = typeof rootPackage.version === "string" ? rootPackage.version.trim() : "";
|
||||
if (!version) {
|
||||
throw new Error('Root package.json must contain a valid "version"');
|
||||
}
|
||||
|
||||
const tag = `v${version}`;
|
||||
const repo = parseRepoSlug(runQuiet("git", ["remote", "get-url", "origin"]));
|
||||
const releaseJson = runQuiet("gh", [
|
||||
"release",
|
||||
"view",
|
||||
tag,
|
||||
"--repo",
|
||||
repo,
|
||||
"--json",
|
||||
"isDraft,tagName,url",
|
||||
]);
|
||||
const release = JSON.parse(releaseJson);
|
||||
|
||||
if (!release.isDraft) {
|
||||
throw new Error(`Release ${tag} is already published. Refusing to finalize a non-draft release.`);
|
||||
}
|
||||
|
||||
run("npm", ["run", "release:publish"]);
|
||||
run("gh", ["release", "edit", tag, "--repo", repo, "--draft=false"]);
|
||||
|
||||
console.log(`Finalized release ${tag}: npm packages published and GitHub draft release promoted.`);
|
||||
@@ -7,6 +7,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const rootPackagePath = path.join(rootDir, "package.json");
|
||||
|
||||
function usageAndExit(code = 0) {
|
||||
process.stderr.write(`Usage: node scripts/push-current-release-tag.mjs [--draft-release]\n`);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function run(cmd, args) {
|
||||
execFileSync(cmd, args, { cwd: rootDir, stdio: "inherit" });
|
||||
}
|
||||
@@ -15,12 +20,39 @@ function runQuiet(cmd, args) {
|
||||
return execFileSync(cmd, args, { cwd: rootDir, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { draftRelease: false };
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg === "--draft-release") {
|
||||
args.draftRelease = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
usageAndExit(0);
|
||||
}
|
||||
usageAndExit(1);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function parseRepoSlug(remoteUrl) {
|
||||
const trimmed = remoteUrl.trim().replace(/\.git$/, "");
|
||||
const httpsMatch = trimmed.match(/github\.com[/:]([^/]+\/[^/]+)$/);
|
||||
if (httpsMatch) {
|
||||
return httpsMatch[1];
|
||||
}
|
||||
throw new Error(`Could not determine GitHub repo from origin URL: ${remoteUrl}`);
|
||||
}
|
||||
|
||||
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8"));
|
||||
const version = typeof rootPackage.version === "string" ? rootPackage.version.trim() : "";
|
||||
if (!version) {
|
||||
throw new Error('Root package.json must contain a valid "version"');
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const tag = `v${version}`;
|
||||
const headCommit = runQuiet("git", ["rev-parse", "HEAD"]);
|
||||
|
||||
@@ -55,4 +87,17 @@ if (remoteTagExists) {
|
||||
run("git", ["push", "origin", tag]);
|
||||
}
|
||||
|
||||
if (args.draftRelease) {
|
||||
const repo = parseRepoSlug(runQuiet("git", ["remote", "get-url", "origin"]));
|
||||
run("node", [
|
||||
path.join("scripts", "sync-release-notes-from-changelog.mjs"),
|
||||
"--repo",
|
||||
repo,
|
||||
"--tag",
|
||||
tag,
|
||||
"--create-if-missing",
|
||||
"--draft",
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(`Release push complete: branch HEAD and tag ${tag}`);
|
||||
|
||||
@@ -13,6 +13,7 @@ Options:
|
||||
--repo <owner/repo> Repository slug. Defaults to $GITHUB_REPOSITORY.
|
||||
--tag <tag> Release tag (e.g. v0.1.14). Defaults to latest changelog entry.
|
||||
--create-if-missing Create release if it does not already exist.
|
||||
--draft Create missing release as draft.
|
||||
`;
|
||||
process.stderr.write(usage.trimStart());
|
||||
process.stderr.write("\n");
|
||||
@@ -24,6 +25,7 @@ function parseArgs(argv) {
|
||||
repo: process.env.GITHUB_REPOSITORY || "",
|
||||
tag: "",
|
||||
createIfMissing: false,
|
||||
draft: false,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -53,6 +55,11 @@ function parseArgs(argv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--draft") {
|
||||
args.draft = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
usageAndExit();
|
||||
}
|
||||
|
||||
@@ -186,6 +193,7 @@ function main() {
|
||||
"--notes-file",
|
||||
notesPath,
|
||||
"--verify-tag",
|
||||
...(args.draft ? ["--draft"] : []),
|
||||
]);
|
||||
console.log(`Created release ${targetTag} with changelog notes.`);
|
||||
} catch (createError) {
|
||||
|
||||
Reference in New Issue
Block a user