diff --git a/.github/workflows/release-demo.yml b/.github/workflows/release-demo.yml index 27ab202..404b050 100644 --- a/.github/workflows/release-demo.yml +++ b/.github/workflows/release-demo.yml @@ -89,14 +89,40 @@ jobs: - name: Build renderer/main/preload bundles (demo mode) run: npm run build:demo - - name: Package, sign, notarize & publish prerelease to GitHub Releases + - name: Pre-create draft release + # electron-builder races itself creating the GitHub release: the dmg + # and zip artifacts finish near-simultaneously, PublishManager spawns + # two GitHubPublisher instances (async cache-miss race), and both POST + # /releases for the same tag - the loser dies with 422 "Published + # releases must have a valid tag" and the job fails after a successful + # notarization, leaving a half-uploaded release without beta.yml. + # Pre-creating the release means every publisher instance finds and + # reuses it instead of racing to create it. Draft keeps it invisible + # to auto-updaters until all assets (incl. beta.yml) are up. + run: | + TAG="v${{ steps.ver.outputs.version }}" + gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \ + gh release create "$TAG" --repo "$GITHUB_REPOSITORY" \ + --target "$GITHUB_SHA" \ + --title "${{ steps.ver.outputs.version }}" \ + --draft --prerelease \ + --notes "Demo prerelease build (run ${{ github.run_number }})" + + - name: Package, sign, notarize & upload prerelease assets # extraMetadata.version sets the app/asset/yaml version without editing # package.json. channel: beta writes beta.yml; releaseType: prerelease # marks the GitHub release as a prerelease (both required for the GitHub - # provider to resolve the demo channel correctly). + # provider to resolve the demo channel correctly). Assets go into the + # draft pre-created above; the next step flips it public. run: | npx electron-builder --mac --publish always \ -c.extraMetadata.version="${{ steps.ver.outputs.version }}" \ -c.mac.notarize=${{ inputs.notarize }} \ -c.publish.channel=beta \ -c.publish.releaseType=prerelease + + - name: Publish prerelease + # All assets + beta.yml are uploaded - make the release visible. + run: | + gh release edit "v${{ steps.ver.outputs.version }}" \ + --repo "$GITHUB_REPOSITORY" --draft=false --prerelease diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e12a404..420a80d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,15 +89,37 @@ jobs: - name: Build renderer/main/preload bundles (release mode) run: npm run build:release - - name: Package, sign, notarize & publish to GitHub Releases - # Tag = v${version} from package.json. releaseType `release` publishes - # immediately; `draft` uploads assets into a draft you publish in the UI - # (safer once Windows/Linux jobs are added - publish once all are up). + - name: Pre-create draft release + # electron-builder races itself creating the GitHub release (two + # publisher instances, both POST /releases, loser 422s - see + # release-demo.yml for details). Pre-create the release as a draft so + # every publisher instance finds and reuses it; the last step publishes + # it once all assets (incl. latest.yml) are uploaded. run: | - RELEASE_TYPE=release - [ "${{ inputs.publish }}" != "true" ] && RELEASE_TYPE=draft - echo "Releasing $(node -p "require('./package.json').version") as: $RELEASE_TYPE" + VERSION=$(node -p "require('./package.json').version") + gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \ + gh release create "v$VERSION" --repo "$GITHUB_REPOSITORY" \ + --target "$GITHUB_SHA" \ + --title "$VERSION" \ + --draft \ + --notes "Edison Watch $VERSION" + + - name: Package, sign, notarize & upload to GitHub Releases + # Tag = v${version} from package.json. Assets upload into the draft + # pre-created above; the next step publishes it (or leaves it a draft + # to publish manually in the UI when the publish input is false). + run: | + echo "Releasing $(node -p "require('./package.json').version")" npx electron-builder --mac --publish always \ -c.mac.notarize=true \ -c.publish.channel=latest \ - -c.publish.releaseType="$RELEASE_TYPE" + -c.publish.releaseType=draft + + - name: Publish release + # Only when publish=true; otherwise the draft stays for manual publish + # in the UI (safer once Windows/Linux jobs are added - publish once all + # are up). + if: ${{ inputs.publish }} + run: | + VERSION=$(node -p "require('./package.json').version") + gh release edit "v$VERSION" --repo "$GITHUB_REPOSITORY" --draft=false --latest diff --git a/src/main/infra/setupConfig.ts b/src/main/infra/setupConfig.ts index e2041d8..9a75f73 100644 --- a/src/main/infra/setupConfig.ts +++ b/src/main/infra/setupConfig.ts @@ -22,15 +22,21 @@ if (DRY_RUN) console.log("[dry-run] Dry-run mode enabled - config files will not // ── Debug environment switcher ─────────────────────────────────────── -export const DEBUG_ENV_NAMES = ["demo", "release", "dev"] as const; +// "temp-local-stack" = Railway-hosted offline stack (local-stack-backend in the +// edison-watch demo project); URLs come from the shared TEMP_LOCAL_STACK_CONFIG. +export const DEBUG_ENV_NAMES = ["demo", "release", "dev", "temp-local-stack"] as const; export type DebugEnvName = (typeof DEBUG_ENV_NAMES)[number]; +function isDebugEnvName(v: string | undefined): v is DebugEnvName { + return (DEBUG_ENV_NAMES as readonly string[]).includes(v ?? ""); +} + /** The environment this binary was compiled for (from VITE_DEPLOY_ENV at build time). */ export function getBuildDefaultEnv(): DebugEnvName | null { const is = { get dev() { return !app.isPackaged; } }; if (is.dev) return "dev"; const v = import.meta.env.VITE_DEPLOY_ENV as string | undefined; - if (v === "demo" || v === "release" || v === "dev") return v; + if (isDebugEnvName(v)) return v; return null; } @@ -60,7 +66,7 @@ export function getDebugEnvOverride(): DebugEnvName | null { if (!existsSync(p)) return null; const raw = readFileSync(p, "utf-8"); const data = JSON.parse(raw) as { env?: string }; - if (data.env === "demo" || data.env === "release" || data.env === "dev") return data.env; + if (isDebugEnvName(data.env)) return data.env; return null; } catch { return null; diff --git a/src/main/menus/appMenu.ts b/src/main/menus/appMenu.ts index 5d2ec46..a7185ab 100644 --- a/src/main/menus/appMenu.ts +++ b/src/main/menus/appMenu.ts @@ -33,7 +33,12 @@ export function buildAppMenu(deps: AppMenuDeps): Menu { const showDeveloperMenu = getBuildDefaultEnv() !== 'release' const currentEnv = getDebugEnvOverride() ?? getBuildDefaultEnv() const envSubmenu: MenuItemConstructorOptions[] = DEBUG_ENV_NAMES.map((name) => ({ - label: name === 'dev' ? 'dev (localhost)' : name, + label: + name === 'dev' + ? 'dev (localhost)' + : name === 'temp-local-stack' + ? 'temp-local-stack (railway offline)' + : name, type: 'radio' as const, checked: currentEnv === name, click: async () => {