Skip to content

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 22 Jul 03:34
· 18 commits to main since this release
a55636b

2.0.0 (2026-07-22)

Migrating from 1.x

2.0 is a major release: ESM-only, hardened routes, and
update.electronjs.org semantics. Every breaking change and its replacement
is listed in the migration guide.

Bug Fixes

  • fix!: explicit channel requests 404 instead of falling back to any channel (#59) (aa86495), closes #15 #15
  • fix!: token-delimited filename architecture detection (#60) (16229c0)
  • better legacy support (e2a9e4d)
  • do not include draft releases (15cf4c8)
  • don't crash on failed background cache refresh (#58) (a016204), closes #14
  • download urls on update endpoint (df4f68c)
  • fall back to the default when PECANS_CACHE_MAX_AGE is not a number (#71) (9b2063d)
  • fetch missing (#7) (0bd2aa7)
  • github: default octokit to native fetch to prevent empty release lists (#28) (0f6eac6)
  • honor route params in downloads, dead code removal, small fixes (Phase 2) (#31) (9fd8f79)
  • missing preferUniversal from options (8dbb31d)
  • normalize every CRLF when parsing a RELEASES manifest (#73) (a55636b), closes String#replace
  • only include dist in pkg (d8fdb25)
  • platform mapping on RELEASES endpoint (5aede2a)
  • semantic-release trusted publishing (4f1ee88)
  • semantic-release trusted publishing (#53) (3d4d4ce)
  • surface unmapped platform combinations instead of dropping silently (#72) (db02442)
  • tests switch to vitest and expand test coverage. (#22) (bbd4ccb)
  • unrecognized architecture preventing start up (#9) (8f74b5f)
  • version and arch resolution (4e00394)
  • working generic refresh webhook; document raw as the backend-private asset slot (Phase 7 PR A) (#43) (23d1d9f)

chore

  • chore!: remove getChannelFromQuery, rename download event payload keys (#63) (6cbdf98)
  • chore!: remove the 2.0-era deprecations (#67) (7cb9f50), closes #57
  • esm only (#24) (1b8a546)

Features

  • feat!: first-class windows and linux arm64 platforms (#62) (ac44426)

  • feat!: PECANS_REFRESH_SECRET env wiring; header-only secret on the generic backend (#64) (c1e0d4e)

  • feat!: strip backend-private payloads from API responses (#61) (cab1e34)

  • feat!: unified ReleaseService resolution pipeline on discrete os/arch/pkg (Phase 7 PR B) (#44) (8a2f06a)

  • feat!: update.electronjs.org-compatible format routes; remove /update ?filetype (#66) (e658500)

  • add support for osx universal binaries. (#5) (902f283)

  • automated builds (d8792b1)

  • cache releases in abstract Backend class with stale-while-revalidate strategy (#19) (173e74c)

  • cjs and esm module distribution (906e725)

  • dependency modernization — Express 5, octokit 22, remove UA autodetection (Phase 5) (#41) (8ea329a)

  • implement TRUST_PROXY for the standalone server (#65) (e45174a)

  • includeVersionInReleaseNotes setting (#17) (05449b0)

  • integration tests for /dl/:filename (#10) (20f8140)

  • log requests for debugging (e8e3b1c)

  • make pecans package executable (558b3fd)

  • modernize packaging and dev tooling (Phase 3) (#32) (3345ed7)

  • preferUniversal option (#6) (74c4f04)

  • recognize .msix / .msixbundle assets and 'msix' package format (#46) (4f848b9), closes #26

  • typed HTTP errors with router-scoped error handling (Phase 6) (#42) (e3b7b54)

BREAKING CHANGES

  • GitHubBackend, PecansSettings.timeout, and
    PecansReleaseDTO.channel are removed from the module API, and GET
    /update (query-style) returns 404. 2.x ships with no carried
    deprecations.

  • /update/:platform/:version no longer honors ?filetype;
    use /update/:platform/msix/:version for the MSIX feed. The default
    squirrel feed behavior is unchanged.

  • fix: electron process.platform-arch aliases; refresh stale filetype comments

    Copilot review: two comments still described the removed /update
    ?filetype query; reworded for the format segment. Alias map gains the
    remaining ${process.platform}-${process.arch} ids electron apps build
    directly (win32-ia32, linux-x64, linux-amd64), with a unit test pinning
    the full matrix.

  • feat: channel variants for the format routes

    Channel-aware clients (missioncontrol) configure feed urls through the
    existing /update/channel/... routes, which the format segment didn't
    cover - there was no way to express "beta channel, msix feed".
    /update/channel/:channel/:platform/:format/:version (+ /RELEASES)
    mirror the existing channel routes. Channel-in-path rather than a
    ?channel= query because Squirrel.Windows appends /RELEASES to the
    configured feed url (a query string would swallow it), it keeps one
    channel idiom across the surface, and default CDN cache keys ignore
    query strings (which would serve cross-channel feeds).

  • the generic backend's /webhook/refresh no longer
    accepts ?secret=; send the secret in the X-Pecans-Secret header.

  • docs: document PECANS_REFRESH_SECRET in the webhook section

    Copilot review: the section explained refreshSecret for module usage
    but not how the standalone server enables it.

  • test: exercise the header transport in wrong-secret and env-unset cases

    Copilot review round 4: the wrong-secret cases still sent the secret
    via ?secret=, which now behaves identically to the missing-header path,
    and the env-unset configure() test relied on the developer's
    environment not defining PECANS_REFRESH_SECRET. Send wrong secrets via
    X-Pecans-Secret and explicitly clear the env var at test start.

  • Pecans.getChannelFromQuery is removed, and the
    beforeDownload/afterDownload event payloads are now {req, release,
    asset} instead of {req, version, platform}.

  • arm64 windows/linux assets that were previously
    invisible now ingest and are served for explicit arm64 queries; releases
    whose only asset for an os is arm64 now satisfy bare-os availability
    checks. Bare-os and x64 queries keep their previous resolutions.

  • docs: document arm64 platforms and aliases in the README

  • /api/versions asset objects no longer include raw, and
    /api/channels entries no longer include releases or latest_release.
    Clients needing a download should use the /download or /dl routes
    rather than raw.browser_download_url.

  • fix: allowlist toJSON fields instead of rest-spreading around raw

    CI eslint rejected the unused _raw binding from the rest-spread. The
    explicit field list is also the safer shape: a future private field can
    never leak by omission from a denylist.

  • assets whose filenames contain version digits ("1.32.0"),
    x86_64, or win32-x64 now ingest as 64-bit instead of 32-bit, and
    filenames with "arm" inside a word no longer classify as arm builds.
    Feeds over existing releases may resolve different assets after upgrade.

  • style: prettier formatting for new spec cases

  • /download/channel/:channel/* and /download?channel=
    return 404 when the named channel has no matching release, instead of
    silently serving a release from another channel.

  • @dopry/pecans is now ESM-only. require('@dopry/pecans')
    is no longer supported; use import (Node >= 22.12).

  • refactor: import model types with import type in runtime modules

    Follows up on Copilot review: PecansRelease/PecansReleases (and other
    names used only in type positions) are classes, so tsc accepts plain
    imports, but with verbatimModuleSyntax they would stay in the emitted
    JS as runtime imports. Convert the type-only usages to import type to
    keep the runtime module graph minimal and cycle-free.

  • Pecans no longer exposes a versions property.

  • feat!: remove the Versions and resolveReleaseAssetForVersion adapters

    The deprecation shims this PR introduced are dropped instead of carried
    to 3.0: route handlers and consumers resolve through ReleaseService /
    resolveAssetForRelease directly. The table-driven specs that pinned the
    legacy composite-id resolution semantics are migrated onto the pipeline
    (via platformToQuery) so the behavioral pins survive the adapter
    removal; unique Versions coverage moved into service.spec.

    Pre-existing deprecations (GitHubBackend, PecansSettings.timeout,
    PecansReleaseDTO.channel) keep their 3.0 schedule.

  • Versions, VersionFilterOpts, PlatformQuery, and
    resolveReleaseAssetForVersion are no longer exported.

  • getArchFromUserAgent, getOsFromUserAgent, and
    getPlatformFromUserAgent now take the internal UserAgentDetails type
    instead of express-useragent's Details, and getArchFromUserAgent
    defaults Windows and Linux to '64' (32-bit desktops are effectively
    extinct; the function is not used internally).

    pecans consumed exactly four booleans from the unmaintained
    express-useragent package; src/utils/userAgent.ts derives them from the
    User-Agent header directly, with mobile exclusions the old library
    handled via separate flags (iOS UAs contain 'like Mac OS X', Android
    UAs contain 'Linux'). The middleware attaches the same req.useragent
    shape. Unit specs cover the parser; the Phase 1 UA-driven download
    contract tests pass unchanged.

  • fix: validate update-route params through getStringParam consistently

    Review feedback: handleUpdateOSX truthiness-checked req.params directly
    but read values through getStringParam, and handleUpdateWin had no
    version guard at all; a missing tag would have produced a '>=undefined'
    range. Validate once through the helper and reuse the validated values.

  • fix: exclude Macintosh+Mobile webview UAs from macOS detection

    Review feedback (partial): a Mobile token alongside Macintosh indicates
    an iPad-class webview masquerading as a Mac; genuine macOS browsers
    never send it. Fixture + test added. Note true iPadOS desktop-mode UAs
    are byte-identical to Mac Safari and undetectable by any parser.

  • fix: short-circuit dlfilename when the filename param is absent

    Review feedback: an undefined filename passed into queryReleases matches
    every release (the predicate treats undefined as no-filter), which would
    serve an arbitrary asset instead of a 404. Unreachable via the current
    route but guarded for consistency with the update handlers.

  • feat: remove user-agent platform autodetection

  • selecting a platform is now the client's
    responsibility. GET / is no longer a download route, and the platform
    segment is required on /download, /download/version/:tag, and
    /download/channel/:channel (a missing platform returns 400). The
    user-agent parser, its middleware, and the getPlatformFromUserAgent /
    getArchFromUserAgent / getOsFromUserAgent helpers are removed.

    Autodetection only ever served bare browser links - Squirrel update
    clients and /dl/* always send explicit platforms - and reliable device
    detection is better handled client-side where UA Client Hints are
    available. Reverting this commit restores the feature wholesale if
    anyone misses it.

  • fix: remove imports orphaned by the autodetection removal

  • fix: validate tag ranges early in validateReqQueryTag

    Review feedback: validRange's result was discarded, so invalid tags only
    failed deep in release matching with a generic 'Invalid Range Specified'
    error. Invalid ranges now throw UnsupportedTagError at the parameter
    boundary ('latest' stays allowed), and the error message no longer says
    'channel' for tags (copy-paste from UnsupportedChannelError).

  • build output moves from dist/cjs + dist/mjs to a tsup
    bundle (dist/index.js CJS, dist/index.mjs ESM). Deep imports into dist
    paths no longer resolve; all models (PecansRelease, PecansAsset,
    PecansReleases, ...) are now exported from the package root instead.

  • replace the dual-tsc + fixup.sh build with tsup (CJS + ESM + d.ts +
    sourcemaps, node22 target); consolidate four tsconfigs into one
    typecheck-only tsconfig.json
  • add a proper exports map with types for both module systems; verified
    with publint and arethetypeswrong (all green: node10/node16/bundler)
  • declare debug and qs as real dependencies - both are imported directly
    but were only present transitively via express, which broke the ESM
    bundle (inlined CJS require calls)
  • guard the run-directly check with typeof require so the ESM build is
    importable; node dist/index.js still starts the server
  • ts-node out of runtime dependencies; dev now runs tsx watch; start
    runs the compiled dist; drop nodemon
  • ESLint 9 flat config + prettier (replaces the stale mocha-era
    .eslintrc); fix the 19 findings it surfaced (unused imports/vars,
    case-block declarations, error causes, no-useless-assignment)
  • add explicit @types/node; add lint/format/typecheck scripts
  • package.json: type commonjs, sideEffects false, canonical repo url
  • fix: rank .tar.gz assets by their full extension in resolveForVersion

    path.extname reports '.gz' for .tar.gz filenames, so the sort fallback
    ranked them at prefs.indexOf(-1) - ahead of every genuine preference -
    whenever .tgz and .tar.gz assets coexisted. Use getSupportedExt, which
    handles the double extension, matching the Phase 2 fix to
    PecansAsset.satisfiesExtensions. Regression test added.

  • fix: throw Error from configure(), correct Listening typo

    Review feedback: the default switch case in configure() threw a raw
    string (no stack trace); the startup log said 'Lisening'. The test that
    pinned the typo is updated to match.

  • fix: throw on invalid os in getDownloadExtensionsByOs

    Review feedback: the switch had no default, so an invalid OperatingSystem
    cast in at runtime silently returned undefined against the declared
    SupportedFileExtension[] return type. Fail loudly instead; edge-case test
    updated to pin the throw.

  • Configuration requires new arguments