diff --git a/.github/scripts/release/github/stable-notes.sh b/.github/scripts/release/github/stable-notes.sh index 4eab3dfadba..3aad78683a4 100644 --- a/.github/scripts/release/github/stable-notes.sh +++ b/.github/scripts/release/github/stable-notes.sh @@ -28,7 +28,8 @@ cat > "$notes_file" </dev/null 2>&1; then @@ -953,6 +963,7 @@ jobs: MAC_ARM64_RESULT: ${{ needs.build_mac_arm64.result }} MAC_X64_RESULT: skipped RELEASE_LATEST_CAS_REQUIRED: "true" + RELEASE_LATEST_FORCE: ${{ inputs.force_latest }} RELEASE_ASSET_SUFFIX: auto RELEASE_CHANNEL: betas RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index d1ea6a72fed..238c4a17ee0 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -33,6 +33,11 @@ on: required: false type: string default: "" + force_latest: + description: "Allow this run to move beta latest metadata backward. Use only for release metadata recovery." + required: true + type: boolean + default: false release_origin: description: "Release storage origin." required: true @@ -184,6 +189,10 @@ on: required: false type: string default: "" + force_latest: + required: false + type: boolean + default: false release_origin: required: false type: string @@ -387,6 +396,7 @@ jobs: env: GITHUB_SHA: ${{ env.BUILT_SHA }} GITHUB_REF_NAME: ${{ inputs.ref != '' && inputs.ref || github.ref_name }} + OPEN_DESIGN_RELEASE_FORCE_LATEST: ${{ inputs.force_latest }} run: pnpm exec tools-release prepare beta build_mac_arm64: @@ -1409,6 +1419,7 @@ jobs: MAC_X64_RESULT: ${{ needs.build_mac_x64.result }} RELEASE_ASSET_SUFFIX: ${{ needs.metadata.outputs.asset_version_suffix }} RELEASE_CHANNEL: beta + RELEASE_LATEST_FORCE: ${{ inputs.force_latest }} RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests RELEASE_METADATA_DIR: ${{ runner.temp }}/release-metadata RELEASE_OUTPUTS_PATH: ${{ runner.temp }}/release-metadata/outputs.json diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 317748def77..02d6ac57113 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -4,9 +4,10 @@ on: workflow_dispatch: inputs: prerelease_version: - description: "Exact validated prerelease version to promote, for example 0.5.1-prerelease.3." - required: true + description: "Exact validated prerelease version to promote, for example 0.5.1-prerelease.3. Optional only for release-dryrun/vX.Y.Z prepublish validation." + required: false type: string + default: "" dry_run: description: "Release mode. metadata stops after promotion metadata; prepublish runs build/smoke/report/plan without publishing; publish performs the stable release." required: false @@ -17,7 +18,7 @@ on: - publish default: metadata ref: - description: "Stable release branch to build, for example release/v0.5.1. Empty builds the dispatch ref, which must also be release/vX.Y.Z." + description: "Stable release branch to build, for example release/v0.5.1. Prepublish-only validation may use release-dryrun/v0.5.1. Empty builds the dispatch ref." required: false type: string default: "" @@ -43,16 +44,17 @@ on: workflow_call: inputs: prerelease_version: - description: "Exact validated prerelease version to promote." - required: true + description: "Exact validated prerelease version to promote. Optional only for release-dryrun/vX.Y.Z prepublish validation." + required: false type: string + default: "" dry_run: description: "Release mode: metadata, prepublish, or publish. The legacy false value is also accepted for callers." required: false type: string default: "metadata" ref: - description: "Stable release branch to build. Empty builds the caller's ref, which must also be release/vX.Y.Z." + description: "Stable release branch to build. Empty builds the caller's ref. Prepublish-only validation may use release-dryrun/vX.Y.Z." required: false type: string default: "" @@ -300,6 +302,7 @@ jobs: runs-on: macos-14 env: GH_TOKEN: ${{ github.token }} + RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} steps: - name: Checkout uses: actions/checkout@v6.0.2 @@ -557,6 +560,7 @@ jobs: runs-on: macos-15-intel env: GH_TOKEN: ${{ github.token }} + RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} steps: - name: Checkout uses: actions/checkout@v6.0.2 @@ -671,6 +675,7 @@ jobs: runs-on: windows-latest env: GH_TOKEN: ${{ github.token }} + RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} steps: - name: Checkout uses: actions/checkout@v6.0.2 @@ -927,6 +932,7 @@ jobs: runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} + RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} steps: - name: Checkout uses: actions/checkout@v6.0.2 diff --git a/apps/web/src/components/UpdaterPopup.tsx b/apps/web/src/components/UpdaterPopup.tsx index 0c42aafff0d..4ee20c6b80d 100644 --- a/apps/web/src/components/UpdaterPopup.tsx +++ b/apps/web/src/components/UpdaterPopup.tsx @@ -8,11 +8,12 @@ import { deriveUpdaterModel, openUpdaterInstaller, quitAfterUpdaterInstallerOpen, + releaseNotesJumpToFromStatus, readUpdaterStatus, subscribeToUpdaterStatus, type UpdaterModel, } from '../lib/updater'; -import { useT } from '../i18n'; +import { useI18n } from '../i18n'; import type { Dict } from '../i18n/types'; import { useAnalytics, useAppVersion } from '../analytics/provider'; import { @@ -21,6 +22,7 @@ import { trackUpdateInstallResult, trackUpdatePromptSurfaceView, } from '../analytics/events'; +import { openExternalUrl } from '../providers/registry'; const INSTALL_HANDOFF_WATCHDOG_MS = 10_000; @@ -69,12 +71,13 @@ function updaterErrorCode(model: UpdaterModel): string | undefined { } export function UpdaterPopup() { - const t = useT(); + const { t } = useI18n(); const wrapRef = useRef(null); const actionInFlightRef = useRef(false); const handoffWatchdogRef = useRef(null); const [model, setModel] = useState(() => deriveUpdaterModel(null)); const [panelOpen, setPanelOpen] = useState(false); + const [panelExpanded, setPanelExpanded] = useState(true); const [installState, setInstallState] = useState('idle'); const clearHandoffWatchdog = useCallback(() => { @@ -87,6 +90,7 @@ export function UpdaterPopup() { handoffWatchdogRef.current = null; actionInFlightRef.current = false; setInstallState('recoverable'); + setPanelExpanded(true); setPanelOpen(true); }, []); @@ -132,6 +136,10 @@ export function UpdaterPopup() { () => updateVersionProps(model, appVersionBefore), [appVersionBefore, model.availableVersion], ); + const releaseNotesJumpTo = useMemo( + () => releaseNotesJumpToFromStatus(model.status), + [model.status], + ); const indicatorSurfaceKey = `${model.currentVersion ?? 'unknown'}->${model.availableVersion ?? 'unknown'}:${model.status?.downloadPath ?? 'unknown'}`; const lastIndicatorSurfaceKeyRef = useRef(null); @@ -200,6 +208,7 @@ export function UpdaterPopup() { actionInFlightRef.current = true; clearHandoffWatchdog(); setInstallState('opening'); + setPanelExpanded(true); setPanelOpen(true); trackUpdateIndicatorClick(analytics.track, { page_name: 'home', @@ -264,6 +273,18 @@ export function UpdaterPopup() { } }; + const openReleaseNotes = useCallback(() => { + if (releaseNotesJumpTo == null) return; + trackUpdateIndicatorClick(analytics.track, { + page_name: 'home', + area: 'update_prompt', + element: 'release_notes', + action: 'open_link', + ...versionProps, + }); + void openExternalUrl(releaseNotesJumpTo.url); + }, [analytics.track, releaseNotesJumpTo, versionProps]); + if (!showControl) return null; return ( @@ -290,6 +311,7 @@ export function UpdaterPopup() { action: 'open_prompt', ...versionProps, }); + setPanelExpanded(true); setPanelOpen(true); }} > @@ -301,7 +323,7 @@ export function UpdaterPopup() { {panelOpen ? ( +
+ +

{t('updater.ready')}

{versionText(t, model)}

{channelLabel != null ? {channelLabel} : null} +
+

{t('updater.releaseNotesPreview')}

+ {releaseNotesJumpTo != null ? ( + + ) : null} +