mirror of
https://github.com/travelerdev/frameloader.git
synced 2026-10-17 13:02:07 +02:00
Build releases when they're published instead of cutting them from a workflow
Publishing a vX.Y.Z release on GitHub now builds that tag as version X.Y.Z and attaches the installers to the release. The version-bump commit to main is gone, so releases work with branch protection. The update checker only announces a release once its installers are attached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2c7f9bf5b
commit
db639ff569
4 files changed
+31
-81
No files matched your search
@@ -1,19 +1,15 @@
|
||||
name: Release
|
||||
|
||||
# Run from the Actions tab: "Release" → Run workflow (on main).
|
||||
# Builds the current head of main as the given version (default: the version
|
||||
# in apps/desktop/package.json), publishes it as the latest GitHub release with
|
||||
# the installers attached, then bumps main to the next patch version.
|
||||
# Create a release in GitHub (Releases → Draft a new release) with a new tag
|
||||
# like v1.2.3 on main, write the notes, and publish it. This workflow then
|
||||
# builds that tag as version 1.2.3 and attaches the installers to the release.
|
||||
# Nothing is pushed to main, so it works with branch protection.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to release, e.g. 1.2.0. Leave empty to release the version in package.json."
|
||||
type: string
|
||||
default: ""
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
group: release-${{ github.event.release.tag_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
@@ -21,39 +17,21 @@ permissions:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Pick version
|
||||
name: Read version from tag
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.v.outputs.version }}
|
||||
next: ${{ steps.v.outputs.next }}
|
||||
sha: ${{ steps.v.outputs.sha }}
|
||||
steps:
|
||||
- name: Only from main
|
||||
if: github.ref != 'refs/heads/main'
|
||||
run: |
|
||||
echo "::error::Releases are cut from main. Pick main in the branch dropdown."
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
- id: v
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
version="${INPUT_VERSION:-$(node scripts/version.mjs get)}"
|
||||
version="${version#v}"
|
||||
node scripts/version.mjs next "$version" > /dev/null # validates the format
|
||||
if git rev-parse -q --verify "refs/tags/v$version" > /dev/null; then
|
||||
echo "::error::v$version is already released. Pass a newer version."
|
||||
if [[ ! "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
echo "::error::Release tags must look like v1.2.3 (got \"$TAG\")."
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "version=$version"
|
||||
echo "next=$(node scripts/version.mjs next "$version")"
|
||||
echo "sha=$(git rev-parse HEAD)"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Releasing v$version from $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "version=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
|
||||
echo "Building ${TAG} as version ${BASH_REMATCH[1]}" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -61,11 +39,11 @@ jobs:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
ref: ${{ needs.prepare.outputs.sha }}
|
||||
ref: refs/tags/${{ github.event.release.tag_name }}
|
||||
|
||||
publish:
|
||||
name: Publish GitHub release
|
||||
needs: [prepare, build]
|
||||
attach:
|
||||
name: Attach installers to the release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -75,37 +53,11 @@ jobs:
|
||||
pattern: frameloader-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Create release
|
||||
- name: Upload
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
SHA: ${{ needs.prepare.outputs.sha }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
ls -la dist
|
||||
gh release create "v$VERSION" dist/* \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--target "$SHA" \
|
||||
--title "Frameloader $VERSION" \
|
||||
--generate-notes \
|
||||
--latest
|
||||
|
||||
bump:
|
||||
name: Bump main to the next version
|
||||
needs: [prepare, publish]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- name: Commit next version
|
||||
env:
|
||||
NEXT: ${{ needs.prepare.outputs.next }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
node scripts/version.mjs set "$NEXT"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -am "Start $NEXT after releasing $VERSION"
|
||||
git push origin HEAD:main
|
||||
# --clobber so re-running a failed release replaces partial uploads.
|
||||
gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber
|
||||
@@ -68,15 +68,15 @@ Every push and pull request runs `.github/workflows/build.yml`: typecheck, tests
|
||||
|
||||
To ship a release:
|
||||
|
||||
1. Open **Actions → Release → Run workflow** on `main`.
|
||||
2. Leave the version empty to release the version in `apps/desktop/package.json`, or type one (e.g. `0.2.0`).
|
||||
3. The workflow builds `main` with that version, publishes the `vX.Y.Z` GitHub release with the installers as the latest release, then commits the next patch version to `main`.
|
||||
1. On GitHub, go to **Releases → Draft a new release**.
|
||||
2. Create a new tag named `vX.Y.Z` (for example `v0.1.0`) targeting `main`, write the notes (or use **Generate release notes**), and publish.
|
||||
3. Publishing runs `.github/workflows/release.yml`, which builds that tag stamped as version `X.Y.Z` and attaches the installers to the release. It takes a few minutes. If it fails, re-run the workflow; uploads replace any partial ones.
|
||||
|
||||
The workflow pushes that version bump straight to `main`. If `main` gets branch protection, allow GitHub Actions to push or change the last job to open a pull request.
|
||||
Nothing is pushed to `main`, so this works with branch protection. The version in `apps/desktop/package.json` is only what local and CI builds report; releases take their version from the tag.
|
||||
|
||||
Builds are not code-signed yet. On macOS the app is ad-hoc signed, so the first launch needs right-click → Open (or `xattr -dr com.apple.quarantine /Applications/Frameloader.app`). Windows SmartScreen will warn until the installer is signed.
|
||||
|
||||
Installed apps notice new releases on their own: they check GitHub's releases API and show a banner with a link. Nothing is downloaded or installed automatically, and Settings → Updates turns the check off.
|
||||
Installed apps notice new releases on their own: they check GitHub's releases API and show a banner with a link once a release has its installers attached. Nothing is downloaded or installed automatically, and Settings → Updates turns the check off.
|
||||
|
||||
## Website hosting (Cloudflare Workers)
|
||||
|
||||
|
||||
@@ -48,9 +48,11 @@ class Updates extends EventEmitter {
|
||||
} else if (!res.ok) {
|
||||
throw new Error(`GitHub answered HTTP ${res.status}`);
|
||||
} else {
|
||||
const body = (await res.json()) as { tag_name?: string; html_url?: string; draft?: boolean; prerelease?: boolean };
|
||||
const body = (await res.json()) as { tag_name?: string; html_url?: string; draft?: boolean; prerelease?: boolean; assets?: unknown[] };
|
||||
const latest = (body.tag_name ?? "").replace(/^v/i, "");
|
||||
const available = !!latest && !body.draft && !body.prerelease && compareVersions(latest, current) > 0;
|
||||
// A release is announced only once its installers are attached; the build takes a few minutes after publishing.
|
||||
const ready = Array.isArray(body.assets) && body.assets.length > 0;
|
||||
const available = !!latest && ready && !body.draft && !body.prerelease && compareVersions(latest, current) > 0;
|
||||
this.status = { current, latest, available, url: body.html_url, checkedAt: new Date().toISOString() };
|
||||
if (available) activity.info(`Frameloader ${latest} is available (you have ${current})`);
|
||||
}
|
||||
|
||||
+1
-5
@@ -2,7 +2,6 @@
|
||||
// The app's version lives in apps/desktop/package.json.
|
||||
// node scripts/version.mjs get -> prints the current version
|
||||
// node scripts/version.mjs set 1.2.3 -> sets it
|
||||
// node scripts/version.mjs next 1.2.3 -> prints 1.2.4 (the version after a release)
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
@@ -27,10 +26,7 @@ if (cmd === "get") {
|
||||
pkg.version = check(arg);
|
||||
writeFileSync(PKG, JSON.stringify(pkg, null, 2) + "\n");
|
||||
console.log(pkg.version);
|
||||
} else if (cmd === "next") {
|
||||
const [, a, b, c] = check(arg ?? pkg.version).match(SEMVER);
|
||||
console.log(`${a}.${b}.${Number(c) + 1}`);
|
||||
} else {
|
||||
console.error("usage: version.mjs get | set X.Y.Z | next [X.Y.Z]");
|
||||
console.error("usage: version.mjs get | set X.Y.Z");
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user