diff --git a/apps/desktop/electron-builder.config.cjs b/apps/desktop/electron-builder.config.cjs index 4d536ac9dc..67f55c3143 100644 --- a/apps/desktop/electron-builder.config.cjs +++ b/apps/desktop/electron-builder.config.cjs @@ -24,6 +24,24 @@ const { msixAppIdWithOrg } = require('./product-identity.cjs') +// `storeMsix` is optional on the identity type but guaranteed present when +// `store` is true (product-identity.cjs spreads it only in that branch). +// The `store` flag is typed `boolean` in the identity, so checkJs cannot +// correlate the two; `mustStoreMsix` is the single assertion point and the +// invariant lives in product-identity.cjs:33-34/58-68. +/** @type {NonNullable | undefined} */ +const storeMsixWhenStore = storeMsix + +/** + * The store MSIX packaging identity. Callers must only invoke this when + * `store` is true (see the invariant note above). + * @param {NonNullable | undefined} value + * @returns {NonNullable} + */ +function mustStoreMsix(value) { + return /** @type {NonNullable} */ (value) +} + // The out-of-store MSIX publisher (ATS cert subject) — single source, shared // with the .appinstaller generator so the manifest and the App Installer can // never drift (see scripts/msix-shared.mjs). @@ -166,11 +184,11 @@ module.exports = { // A store build uses the Partner Center packaging identity (the Store // re-signs + rewrites the publisher on submission); everything else uses // the out-of-store ATS-cert identity. - identityName: store ? storeMsix.identityName : msixAppIdWithOrg, + identityName: store ? mustStoreMsix(storeMsixWhenStore).identityName : msixAppIdWithOrg, applicationId: appNamePascal, displayName, - publisher: store ? storeMsix.publisher : OUT_OF_STORE_PUBLISHER, - publisherDisplayName: store ? storeMsix.publisherDisplayName : 'Nous Research', + publisher: store ? mustStoreMsix(storeMsixWhenStore).publisher : OUT_OF_STORE_PUBLISHER, + publisherDisplayName: store ? mustStoreMsix(storeMsixWhenStore).publisherDisplayName : 'Nous Research', // Canary MSIX versions are `X.Y.Z.` (see // scripts/msix-shared.mjs). setBuildNumber makes getVersionInWeirdWindowsForm // use the BUILD_NUMBER env (4th component) instead of hardcoding ".0" — a diff --git a/apps/desktop/electron/package-process-reap.ts b/apps/desktop/electron/package-process-reap.ts index 5633686b8d..13076e9935 100644 --- a/apps/desktop/electron/package-process-reap.ts +++ b/apps/desktop/electron/package-process-reap.ts @@ -198,7 +198,7 @@ export function isUnderInstallRoot( const candidates = typeof roots === 'string' || roots == null ? [roots] : roots for (const root of candidates) { - if (!root) { + if (typeof root !== 'string' || !root) { continue } diff --git a/scripts/msix-shared.mjs b/scripts/msix-shared.mjs index 10e2f82104..61a47acd0d 100644 --- a/scripts/msix-shared.mjs +++ b/scripts/msix-shared.mjs @@ -31,7 +31,11 @@ const CONTENT_TYPES = { '.msix': 'application/msix' } -/** The Content-Type to store for a staged release artifact, if any. */ +/** + * The Content-Type to store for a staged release artifact, if any. + * @param {string} filename the artifact filename + * @returns {string | undefined} + */ export function contentTypeFor(filename) { const lower = String(filename).toLowerCase() for (const [suffix, mime] of Object.entries(CONTENT_TYPES)) { @@ -110,6 +114,10 @@ export function resolveWinSdkTools() { process.exit(1) } +/** + * @param {unknown} value any value to XML-escape + * @returns {string} + */ function escapeAttr(value) { return String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } @@ -163,6 +171,10 @@ const STABLE_TAG_RE = /^v\d+\.\d+\.\d+$/ // number to clamp (a clamped number would break monotonicity). const MAX_BUILD_MINUTES = 45 * 24 * 60 +/** + * @param {string} stamp YYYYMMDD[HHMMSS] UTC stamp + * @returns {number} epoch seconds + */ function stampToEpoch(stamp) { const parts = /^(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?$/.exec(stamp) if (!parts) return 0 @@ -170,15 +182,35 @@ function stampToEpoch(stamp) { return Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h ?? 0), Number(mi ?? 0), Number(s ?? 0)) / 1000 } +/** + * List git tags matching `pattern`, newest-first (git's -v:refname sort). + * @param {string} gitRoot the repo root + * @param {string} pattern git tag glob, e.g. "v0.27.*" + * @returns {string[]} + */ export function listGitTags(gitRoot, pattern) { return execFileSync('git', ['tag', '--list', pattern, '--sort=-v:refname'], { cwd: gitRoot, encoding: 'utf8' }) .split('\n').filter(Boolean) } +/** + * The commit time (epoch seconds) of `tag`, for minutes-since-stable math. + * @param {string} gitRoot the repo root + * @param {string} tag a git tag + * @returns {number} + */ export function gitTagCommitTime(gitRoot, tag) { return Number(execFileSync('git', ['log', '-1', '--format=%ct', tag], { cwd: gitRoot, encoding: 'utf8' }).trim()) } +/** + * Minutes between a canary tag's UTC stamp and the given stable epoch — + * the MSIX 4th version component. Null for a stable tag; throws when the + * stable base is older than 45 days (16-bit component would overflow). + * @param {string} tag the release tag + * @param {number} stableEpoch stable tag commit time, epoch seconds + * @returns {number | null} + */ export function canaryBuildMinutesFor(tag, stableEpoch) { const m = CANARY_TAG_RE.exec(String(tag || '')) if (!m) return null @@ -193,6 +225,13 @@ export function canaryBuildMinutesFor(tag, stableEpoch) { return minutes } +/** + * Minutes-since-stable for a canary tag, resolving the stable base from + * the repo's tags on the same major.minor line. + * @param {string} tag the release tag + * @param {string} gitRoot the repo root + * @returns {number | null} + */ export function canaryBuildMinutes(tag, gitRoot) { const m = CANARY_TAG_RE.exec(String(tag || '')) if (!m) return null