feat(release): add draft GitHub release flow
This commit is contained in:
@@ -99,6 +99,19 @@ jobs:
|
|||||||
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
||||||
NODE
|
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
|
- name: Build and publish macOS Tauri release
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -116,7 +129,7 @@ jobs:
|
|||||||
tagName: ${{ env.RELEASE_TAG }}
|
tagName: ${{ env.RELEASE_TAG }}
|
||||||
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
||||||
releaseBody: See the assets to download and install this version.
|
releaseBody: See the assets to download and install this version.
|
||||||
releaseDraft: false
|
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: --target ${{ matrix.rust_target }}
|
args: --target ${{ matrix.rust_target }}
|
||||||
|
|
||||||
@@ -195,6 +208,19 @@ jobs:
|
|||||||
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
||||||
NODE
|
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
|
- name: Build and publish Linux Tauri release
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -206,7 +232,7 @@ jobs:
|
|||||||
tagName: ${{ env.RELEASE_TAG }}
|
tagName: ${{ env.RELEASE_TAG }}
|
||||||
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
||||||
releaseBody: See the assets to download and install this version.
|
releaseBody: See the assets to download and install this version.
|
||||||
releaseDraft: false
|
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: --bundles appimage
|
args: --bundles appimage
|
||||||
|
|
||||||
@@ -280,6 +306,19 @@ jobs:
|
|||||||
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
fs.writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`);
|
||||||
NODE
|
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
|
- name: Build and publish Windows Tauri release
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -291,6 +330,6 @@ jobs:
|
|||||||
tagName: ${{ env.RELEASE_TAG }}
|
tagName: ${{ env.RELEASE_TAG }}
|
||||||
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
releaseName: Paseo ${{ env.RELEASE_TAG }}
|
||||||
releaseBody: See the assets to download and install this version.
|
releaseBody: See the assets to download and install this version.
|
||||||
releaseDraft: false
|
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: --bundles nsis,msi
|
args: --bundles nsis,msi
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
draft:
|
||||||
|
description: "Create missing release as draft."
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-notes-sync-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }}
|
group: release-notes-sync-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }}
|
||||||
@@ -43,6 +48,7 @@ jobs:
|
|||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
INPUT_TAG: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.inputs.tag }}
|
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_CREATE_IF_MISSING: ${{ github.event.inputs.create_if_missing }}
|
||||||
|
INPUT_DRAFT: ${{ github.event.inputs.draft }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -64,4 +70,8 @@ jobs:
|
|||||||
args+=(--create-if-missing)
|
args+=(--create-if-missing)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "${INPUT_DRAFT:-false}" = "true" ]; then
|
||||||
|
args+=(--draft)
|
||||||
|
fi
|
||||||
|
|
||||||
node scripts/sync-release-notes-from-changelog.mjs "${args[@]}"
|
node scripts/sync-release-notes-from-changelog.mjs "${args[@]}"
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# 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
|
## 0.1.18 - 2026-03-06
|
||||||
### Added
|
### Added
|
||||||
- Added a desktop `Mod+W` shortcut to close the current tab.
|
- Added a desktop `Mod+W` shortcut to close the current tab.
|
||||||
|
|||||||
@@ -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)
|
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:
|
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.
|
- `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.
|
- `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: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: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",
|
"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: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: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"
|
"release:major": "npm run version:all:major && npm run release:check && npm run release:publish && npm run release:push"
|
||||||
|
|||||||
@@ -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 rootDir = path.resolve(__dirname, "..");
|
||||||
const rootPackagePath = path.join(rootDir, "package.json");
|
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) {
|
function run(cmd, args) {
|
||||||
execFileSync(cmd, args, { cwd: rootDir, stdio: "inherit" });
|
execFileSync(cmd, args, { cwd: rootDir, stdio: "inherit" });
|
||||||
}
|
}
|
||||||
@@ -15,12 +20,39 @@ function runQuiet(cmd, args) {
|
|||||||
return execFileSync(cmd, args, { cwd: rootDir, encoding: "utf8" }).trim();
|
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 rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8"));
|
||||||
const version = typeof rootPackage.version === "string" ? rootPackage.version.trim() : "";
|
const version = typeof rootPackage.version === "string" ? rootPackage.version.trim() : "";
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error('Root package.json must contain a valid "version"');
|
throw new Error('Root package.json must contain a valid "version"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
const tag = `v${version}`;
|
const tag = `v${version}`;
|
||||||
const headCommit = runQuiet("git", ["rev-parse", "HEAD"]);
|
const headCommit = runQuiet("git", ["rev-parse", "HEAD"]);
|
||||||
|
|
||||||
@@ -55,4 +87,17 @@ if (remoteTagExists) {
|
|||||||
run("git", ["push", "origin", tag]);
|
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}`);
|
console.log(`Release push complete: branch HEAD and tag ${tag}`);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Options:
|
|||||||
--repo <owner/repo> Repository slug. Defaults to $GITHUB_REPOSITORY.
|
--repo <owner/repo> Repository slug. Defaults to $GITHUB_REPOSITORY.
|
||||||
--tag <tag> Release tag (e.g. v0.1.14). Defaults to latest changelog entry.
|
--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.
|
--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(usage.trimStart());
|
||||||
process.stderr.write("\n");
|
process.stderr.write("\n");
|
||||||
@@ -24,6 +25,7 @@ function parseArgs(argv) {
|
|||||||
repo: process.env.GITHUB_REPOSITORY || "",
|
repo: process.env.GITHUB_REPOSITORY || "",
|
||||||
tag: "",
|
tag: "",
|
||||||
createIfMissing: false,
|
createIfMissing: false,
|
||||||
|
draft: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let index = 0; index < argv.length; index += 1) {
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
@@ -53,6 +55,11 @@ function parseArgs(argv) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg === "--draft") {
|
||||||
|
args.draft = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
usageAndExit();
|
usageAndExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +193,7 @@ function main() {
|
|||||||
"--notes-file",
|
"--notes-file",
|
||||||
notesPath,
|
notesPath,
|
||||||
"--verify-tag",
|
"--verify-tag",
|
||||||
|
...(args.draft ? ["--draft"] : []),
|
||||||
]);
|
]);
|
||||||
console.log(`Created release ${targetTag} with changelog notes.`);
|
console.log(`Created release ${targetTag} with changelog notes.`);
|
||||||
} catch (createError) {
|
} catch (createError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user