Skip to content

feat: automatic update detection and release pipeline#12

Merged
mateoltd merged 3 commits into
mainfrom
updater-v1
Apr 1, 2026
Merged

feat: automatic update detection and release pipeline#12
mateoltd merged 3 commits into
mainfrom
updater-v1

Conversation

@mateoltd
Copy link
Copy Markdown
Owner

@mateoltd mateoltd commented Mar 31, 2026

adds the first real updater slice.

  • desktop update detection in the app
  • linux appimage in releases
  • windows msix + appinstaller prep
  • github pages stable manifest

notes:

  • windows public auto-update is not exposed yet
  • release jobs need real ci runs to shake out packaging issues

Summary by CodeRabbit

  • New Features

    • Top‑bar update notification with dismissible “update” action; Updates panel in Settings showing current/latest version, last‑check time, and manual check; background auto‑update checks on desktop startup.
  • Packaging

    • Added Linux AppImage and Windows MSIX/AppInstaller release artifacts and published update metadata.
  • Documentation

    • Architecture and conventions updated to document the update‑checking flow and release metadata; note restricting public Windows updater distribution until production signing is available.

Copilot AI review requested due to automatic review settings March 31, 2026 23:12
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

Warning

Rate limit exceeded

@mateoltd has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 2 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 2 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 696b1ad9-b621-489a-80c0-50ddf4bb1397

📥 Commits

Reviewing files that changed from the base of the PR and between ce26267 and bb30fa3.

📒 Files selected for processing (20)
  • .github/workflows/release.yml
  • README.md
  • docs/ARCHITECTURE.md
  • docs/CONVENTIONS.md
  • frontend/src/components/App.tsx
  • frontend/src/components/SettingsView.tsx
  • frontend/src/main.tsx
  • frontend/src/state.ts
  • frontend/src/store.ts
  • frontend/src/types.ts
  • frontend/src/updater.ts
  • frontend/static/style.css
  • internal/server/api.go
  • internal/server/server.go
  • internal/update/update.go
  • internal/update/update_test.go
  • main.go
  • packaging/linux/croopor.desktop
  • packaging/windows/AppxManifest.xml.template
  • packaging/windows/croopor.appinstaller.template
📝 Walkthrough

Walkthrough

Adds a complete update detection and distribution system: new update service that fetches release manifests, server API and caching, frontend updater module/UI and scheduled checks, build pipeline producing AppImage/MSIX and Pages metadata, tests, docs, and packaging templates.

Changes

Cohort / File(s) Summary
CI / Release pipeline
\.github/workflows/release.yml
Expanded workflow permissions, added APPIMAGETOOL_VERSION, renamed job releaserelease-binaries, and added linux-appimage, windows-msix, and pages jobs to build/upload AppImage, MSIX/AppInstaller, and deploy Pages update metadata.
Release tooling config
\.goreleaser.yml
Switched frontend install hooks from npmpnpm with frozen lockfile and ignored scripts.
Server integration & API
main.go, internal/server/server.go, internal/server/api.go
Passes app version and updater into server; adds UpdateChecker interface, server fields for appVersion/updater/cache, registers GET /api/v1/update, implements cache TTL and cached-read helper, and returns real version from /status.
Update service package + tests
internal/update/update.go, internal/update/update_test.go
New internal/update Service: manifest models, version normalization/comparison, platform-specific action resolution, HTTP fetch with timeout/limits; extensive unit tests validating selection, edge cases, and invalid manifests.
Frontend types, state & store
frontend/src/types.ts, frontend/src/state.ts, frontend/src/store.ts
Added UpdateInfo/UpdateKind types; extended LocalPrefs with lastUpdateCheckAt and dismissedUpdateVersion; exported updateInfo and updateCheckState signals.
Frontend updater logic
frontend/src/updater.ts, frontend/src/main.tsx
New updater module with check/dismiss/open helpers, concurrency token, persistent stamps, scheduleAutoUpdateCheck() and integration into app init; dev-flush/cleanup UX tweaks.
Frontend UI & styles
frontend/src/components/App.tsx, frontend/src/components/SettingsView.tsx, frontend/static/style.css
Topbar update chip and dismiss, Settings “Updates” panel with current/latest/last-check and action buttons; CSS for update chip and settings grid; Advanced section always shown.
Docs & conventions
README.md, docs/ARCHITECTURE.md, docs/CONVENTIONS.md
Documented desktop update detection, update architecture and flow, API requirement for /api/v1/update, and Windows signing/todo notes.
Packaging templates
packaging/linux/croopor.desktop, packaging/windows/AppxManifest.xml.template, packaging/windows/croopor.appinstaller.template
Added Linux desktop entry and Windows AppX/AppInstaller templates with parameterized fields and update settings.
Misc — packaging / manifest
.goreleaser.yml (already listed), README edits
Minor README feature-list update to mention desktop update detection and release distribution constraints for Windows artifacts.

Sequence Diagram

sequenceDiagram
    participant App as App Init / Scheduler
    participant Frontend as Frontend (UI)
    participant API as Backend API (/api/v1/update)
    participant Updater as Update Service
    participant Manifest as Remote Manifest (Pages)

    App->>Frontend: scheduleAutoUpdateCheck()
    Frontend->>API: GET /api/v1/update
    API->>Updater: Check(currentVersion)
    Updater->>Manifest: HTTP GET stable.json (2s timeout)
    Manifest-->>Updater: JSON manifest
    Updater->>Updater: normalize & compare versions
    alt update available
        Updater-->>API: Result{available:true,kind,action_url,...}
        API-->>Frontend: 200 OK with UpdateInfo
        Frontend->>Frontend: set updateInfo signal
        Frontend->>Frontend: render update chip / settings
        Frontend->>Frontend: user -> openUpdateAction() or dismissAvailableUpdate()
    else no update
        Updater-->>API: Result{available:false,kind:"none"}
        API-->>Frontend: 200 OK
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through JSON, cookies, and a page,

Sniffed manifest crumbs from CI's new stage.
I stitched AppImage, signed an MSIX bell,
Told the frontend politely, "There's a new shell."
A carrot-toast for releases — hop, deploy and engage!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: automatic update detection and release pipeline' accurately summarizes the main changes: addition of update detection functionality and new release pipeline stages for Linux AppImage, Windows MSIX, and GitHub Pages deployment.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch updater-v1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
frontend/src/main.tsx (1)

96-96: Isolate updater scheduling failures from app bootstrap.

Line 96 runs inside the main bootstrap try; if scheduling throws, startup flips to global error state. Consider guarding it locally so update checks are best-effort.

Proposed refactor
     watchVersions();
-    scheduleAutoUpdateCheck();
+    try {
+      scheduleAutoUpdateCheck();
+    } catch (err: unknown) {
+      appendLog('system', `Update scheduler disabled: ${errMessage(err)}`);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/main.tsx` at line 96, The call to scheduleAutoUpdateCheck is
currently inside the main bootstrap try block and can throw, flipping the app
into a global error state; move or wrap the scheduleAutoUpdateCheck invocation
in its own local try/catch so update scheduling is best-effort: call
scheduleAutoUpdateCheck() inside a small try block, catch and log the error
(using the same logger used during bootstrap) without rethrowing, so failures in
scheduleAutoUpdateCheck do not affect the surrounding bootstrap flow or error
state.
frontend/static/style.css (1)

97-109: Add explicit keyboard focus styles for the new update controls.

Defining :focus-visible for .update-chip and .update-chip-dismiss will make focus indication consistent with the rest of the custom-themed UI.

♿ Suggested CSS patch
 .update-chip:hover{background:color-mix(in srgb,var(--accent) 18%,var(--surface-1));border-color:var(--accent)}
+.update-chip:focus-visible{
+  outline:1px solid color-mix(in srgb,var(--accent) 60%,transparent);
+  box-shadow:0 0 0 2px var(--accent-glow);
+}
 .update-chip-dismiss{
   border:none;background:transparent;color:var(--text-muted);font-size:10px;font-weight:600;
   font-family:var(--font-mono);cursor:pointer;padding:3px 4px;border-radius:var(--r-sm);
 }
 .update-chip-dismiss:hover{background:var(--surface-2);color:var(--text)}
+.update-chip-dismiss:focus-visible{
+  outline:1px solid color-mix(in srgb,var(--accent) 60%,transparent);
+  box-shadow:0 0 0 2px var(--accent-glow);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/static/style.css` around lines 97 - 109, Add explicit keyboard
focus-visible styles for the interactive controls .update-chip and
.update-chip-dismiss by defining :focus-visible rules that match the rest of the
theme (use the existing theme tokens such as --accent, --focus, --focus-ring,
--r-sm or similar) to provide a visible outline/box-shadow and ensure outline:
none is not removing visibility; ensure the styles include clear contrast (e.g.,
a 2px solid or a subtle ring via box-shadow) and preserve current border-radius
and padding so keyboard focus looks consistent with other components.
internal/server/server.go (1)

22-29: Inject updater dependency (or manifest URL) instead of hardcoding it in NewServer.

Hard-wiring DefaultManifestURL and constructing the concrete updater in the constructor makes testing and non-prod environments harder than necessary.

♻️ Suggested refactor
 type Server struct {
   mu             sync.RWMutex
   mcDir          string
   appVersion     string
@@
-  updater        *appupdate.Service
+  updater        updaterChecker
   mux            *http.ServeMux
   frontend       fs.FS
 }
+
+type updaterChecker interface {
+  Check(currentVersion string) (appupdate.Result, error)
+}
 
-func NewServer(mcDir string, appVersion string, cfg *config.Config, instances *instance.InstanceStore, frontend fs.FS) *Server {
+func NewServer(mcDir string, appVersion string, cfg *config.Config, instances *instance.InstanceStore, frontend fs.FS, updater updaterChecker) *Server {
+  if updater == nil {
+    updater = appupdate.NewService(appupdate.DefaultManifestURL, runtime.GOOS, runtime.GOARCH)
+  }
   s := &Server{
@@
-    updater:        appupdate.NewService(appupdate.DefaultManifestURL, runtime.GOOS, runtime.GOARCH),
+    updater:        updater,

Also applies to: 47-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/server.go` around lines 22 - 29, The constructor NewServer
currently hardcodes DefaultManifestURL and constructs the concrete
appupdate.Service inside, making tests brittle; change NewServer to accept an
injected updater (appupdate.Service) or a manifestURL string as an additional
parameter and set the server.updater field from that parameter instead of
creating it inside NewServer; update all NewServer call sites and tests to pass
a mock or alternate updater (or manifest URL) and remove any use of
DefaultManifestURL within NewServer so the creation responsibility is moved to
callers or test helpers.
internal/server/api.go (1)

127-134: Consider short-TTL caching for update checks.

This path does a remote lookup on each request; a small in-memory TTL cache would reduce latency and external dependency pressure while keeping update freshness acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/api.go` around lines 127 - 134, Add a short in-memory TTL
cache to Server.handleUpdate so s.updater.Check(appVersion) is not called on
every request; create a package-level or Server-scoped cached struct (e.g., type
updateCache { mu sync.RWMutex; data *UpdateResult; ts time.Time }) and a TTL
constant (e.g., 1m). In handleUpdate, first read-lock and return cached data via
writeJSON if ts+TTL is valid; otherwise escalate to write-lock, call
s.updater.Check(s.appVersion), update the cache fields (data and ts) and then
writeJSON; on upstream error use writeError as currently done. Ensure all cache
accesses use the mutex and cache key includes s.appVersion if multiple versions
must be supported.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 140-142: Replace the mutable download+chmod+run sequence (the
curl, chmod, and ARCH=... APPIMAGE_EXTRACT_AND_RUN lines) with a reproducible
build step that clones AppImage/AppImageKit and runs its ci/build-in-docker.sh
to produce appimagetool, then invoke that built binary on dist/appimage/AppDir
to create dist/croopor-linux-amd64.AppImage; alternatively swap to using
linuxdeploy or appimage-builder instead of appimagetool. Specifically: remove
the curl + chmod + APPIMAGE_EXTRACT_AND_RUN invocation and add steps to git
clone https://github.com/AppImage/AppImageKit, checkout a pinned commit or tag,
run ci/build-in-docker.sh (or use the recommended tool), and use the resulting
appimagetool binary to package (the current packaging invocation is the code to
replace). Ensure you pin the AppImageKit commit/tag for reproducibility and
update workflow envs to point to the locally built appimagetool.

In `@frontend/src/updater.ts`:
- Around line 92-101: scheduleAutoUpdateCheck currently sets a one-shot timer
(autoCheckTimer) that calls checkForUpdates once and then stops; change it to a
recurring scheduler so it retries quickly if startup/install/launch are busy and
otherwise schedules the next daily check after a completed check. Implement
logic inside scheduleAutoUpdateCheck (or a helper used by it) to: clear any
existing autoCheckTimer, compute whether to set a short retry timeout (e.g.,
AUTO_CHECK_RETRY_MS) when bootstrapState.value !== 'ready' or
installState.value.status !== 'idle' or launchState.value.status !== 'idle',
otherwise call checkForUpdates({ silent: true }) and on its completion schedule
the next run after AUTO_CHECK_INTERVAL_MS; keep using autoCheckTimer to track
the pending window and ensure timers are nulled before/after firing so multiple
timers cannot be armed. Reference scheduleAutoUpdateCheck, autoCheckTimer,
checkForUpdates, AUTO_CHECK_DELAY_MS (replace/augment with retry/interval
constants) when making the changes.
- Around line 83-85: The timestamp is being updated in the finally block so any
failed poll still sets lastUpdateCheckAt; move the call to stampUpdateCheck()
out of the finally and only invoke it after a confirmed successful check (e.g.,
after a successful fetch/response handling path) while still clearing
pendingCheck = null in finally; update the code around stampUpdateCheck(),
pendingCheck and the function performing the poll so only successful completion
calls stampUpdateCheck().

In `@internal/update/update.go`:
- Around line 164-176: The parseStableVersion function accepts prefixes because
fmt.Sscanf tolerates trailing characters; change it to explicitly validate that
raw matches exactly three dot-separated numeric components:
normalizeVersion(raw), then split on '.' (or use a strict regexp) and convert
each part with strconv.Atoi into stableVersion.major/minor/patch, returning an
error if there are not exactly three numeric parts or any conversion fails (and
reject any trailing non-numeric characters); update imports to include strconv
and add regression tests for inputs like "1.2.3.4" and "1.2.3foo" to assert
parseStableVersion returns an error.

In `@packaging/windows/AppxManifest.xml.template`:
- Around line 2-6: The Package root element in the manifest is missing the
IgnorableNamespaces attribute which causes validation failures; update the
Package element (the declaration that currently includes xmlns, xmlns:uap,
xmlns:desktop and xmlns:rescap) to add IgnorableNamespaces listing the declared
extension prefixes (e.g., "uap desktop rescap") so the manifest consumer will
ignore those extension namespaces during validation.

---

Nitpick comments:
In `@frontend/src/main.tsx`:
- Line 96: The call to scheduleAutoUpdateCheck is currently inside the main
bootstrap try block and can throw, flipping the app into a global error state;
move or wrap the scheduleAutoUpdateCheck invocation in its own local try/catch
so update scheduling is best-effort: call scheduleAutoUpdateCheck() inside a
small try block, catch and log the error (using the same logger used during
bootstrap) without rethrowing, so failures in scheduleAutoUpdateCheck do not
affect the surrounding bootstrap flow or error state.

In `@frontend/static/style.css`:
- Around line 97-109: Add explicit keyboard focus-visible styles for the
interactive controls .update-chip and .update-chip-dismiss by defining
:focus-visible rules that match the rest of the theme (use the existing theme
tokens such as --accent, --focus, --focus-ring, --r-sm or similar) to provide a
visible outline/box-shadow and ensure outline: none is not removing visibility;
ensure the styles include clear contrast (e.g., a 2px solid or a subtle ring via
box-shadow) and preserve current border-radius and padding so keyboard focus
looks consistent with other components.

In `@internal/server/api.go`:
- Around line 127-134: Add a short in-memory TTL cache to Server.handleUpdate so
s.updater.Check(appVersion) is not called on every request; create a
package-level or Server-scoped cached struct (e.g., type updateCache { mu
sync.RWMutex; data *UpdateResult; ts time.Time }) and a TTL constant (e.g., 1m).
In handleUpdate, first read-lock and return cached data via writeJSON if ts+TTL
is valid; otherwise escalate to write-lock, call s.updater.Check(s.appVersion),
update the cache fields (data and ts) and then writeJSON; on upstream error use
writeError as currently done. Ensure all cache accesses use the mutex and cache
key includes s.appVersion if multiple versions must be supported.

In `@internal/server/server.go`:
- Around line 22-29: The constructor NewServer currently hardcodes
DefaultManifestURL and constructs the concrete appupdate.Service inside, making
tests brittle; change NewServer to accept an injected updater
(appupdate.Service) or a manifestURL string as an additional parameter and set
the server.updater field from that parameter instead of creating it inside
NewServer; update all NewServer call sites and tests to pass a mock or alternate
updater (or manifest URL) and remove any use of DefaultManifestURL within
NewServer so the creation responsibility is moved to callers or test helpers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb0d4862-d4ef-49a2-bca4-18fc0169e8e6

📥 Commits

Reviewing files that changed from the base of the PR and between 11b73af and 61f3a0c.

📒 Files selected for processing (21)
  • .github/workflows/release.yml
  • .goreleaser.yml
  • README.md
  • docs/ARCHITECTURE.md
  • docs/CONVENTIONS.md
  • frontend/src/components/App.tsx
  • frontend/src/components/SettingsView.tsx
  • frontend/src/main.tsx
  • frontend/src/state.ts
  • frontend/src/store.ts
  • frontend/src/types.ts
  • frontend/src/updater.ts
  • frontend/static/style.css
  • internal/server/api.go
  • internal/server/server.go
  • internal/update/update.go
  • internal/update/update_test.go
  • main.go
  • packaging/linux/croopor.desktop
  • packaging/windows/AppxManifest.xml.template
  • packaging/windows/croopor.appinstaller.template

Comment thread .github/workflows/release.yml Outdated
Comment thread frontend/src/updater.ts Outdated
Comment thread frontend/src/updater.ts
Comment thread internal/update/update.go
Comment thread packaging/windows/AppxManifest.xml.template Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
internal/update/update.go (3)

51-59: Consider removing the client-level timeout.

The HTTP client has a 2s timeout (line 53), but fetchManifest also creates a context with a 2s timeout (line 105). Since the context timeout is per-request and more precise, the client-level timeout is redundant.

♻️ Suggested simplification
 func NewService(manifestURL, platform, arch string) *Service {
 	return &Service{
-		client:      &http.Client{Timeout: 2 * time.Second},
+		client:      &http.Client{},
 		manifestURL: manifestURL,
 		platform:    platform,
 		arch:        arch,
 		now:         time.Now,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/update/update.go` around lines 51 - 59, The Service constructor
NewService sets an http.Client with a 2s Timeout which is redundant because
fetchManifest already uses a per-request context timeout; remove the
client-level timeout (i.e., stop constructing client as &http.Client{Timeout: 2
* time.Second} and instead use &http.Client{} or nil timeout) so the per-request
context in fetchManifest controls request deadlines and avoids double timeouts.

148-154: Consider returning "none" when AppImageURL is empty for consistency.

When entry.AppImageURL is empty, this returns kind = "appimage" with an empty URL. The caller handles this correctly by checking for empty actionURL, but it would be more consistent to return "none" here (similar to the Windows fallback pattern or the !ok case).

♻️ Suggested improvement
 	case "linux":
 		entry, ok := manifest.Linux[arch]
 		if !ok {
 			return "none", "", ""
 		}
 		actionURL := strings.TrimSpace(entry.AppImageURL)
+		if actionURL == "" {
+			return "none", "", ""
+		}
 		return "appimage", actionURL, "Download AppImage"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/update/update.go` around lines 148 - 154, In the "linux" case
handling (see manifest.Linux and entry.AppImageURL), ensure you treat an empty
or whitespace-only AppImageURL as no action: trim entry.AppImageURL and if it's
empty return "none", "", "" instead of returning kind "appimage" with an empty
URL; otherwise return "appimage", the trimmed URL, "Download AppImage".

123-126: Limit response body size to prevent potential memory exhaustion.

The JSON decoder reads directly from the unbounded response body. While the manifest URL points to a controlled GitHub Pages endpoint, a compromised CDN or MITM attack could serve an arbitrarily large response. Consider limiting to a reasonable size (e.g., 1MB is generous for a manifest).

🛡️ Suggested fix using io.LimitReader

Add "io" to imports, then:

-	var manifest Manifest
-	if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
+	const maxManifestSize = 1 << 20 // 1 MB
+	var manifest Manifest
+	if err := json.NewDecoder(io.LimitReader(resp.Body, maxManifestSize)).Decode(&manifest); err != nil {
 		return nil, fmt.Errorf("decode update manifest: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/update/update.go` around lines 123 - 126, The JSON decoder currently
reads directly from resp.Body (json.NewDecoder(resp.Body)), which can be
unbounded; change it to decode from a size-limited reader using io.LimitReader
(e.g., io.LimitReader(resp.Body, 1<<20)) and add the "io" import; optionally
define a constant like maxManifestSize = 1<<20 and use
json.NewDecoder(io.LimitReader(resp.Body, maxManifestSize)) when decoding the
manifest variable to prevent memory exhaustion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 207-235: The release step currently writes and uploads a
non-arch-specific asset name ($appInstallerPath -> croopor.appinstaller) causing
name collisions between matrix jobs; change the generated asset name to include
the architecture (e.g., set $appInstallerPath to Join-Path $dist
"croopor-windows-${goarch}.appinstaller" and update the $appInstaller creation
so it writes that file), and update the upload step file entry to
dist/windows-${{ matrix.goarch }}/croopor-windows-${{ matrix.goarch
}}.appinstaller so each matrix job uploads a unique asset (alternatively
serialize uploads or use overwrite_files: true if you intentionally want a
single asset).
- Around line 219-246: The workflow currently creates a new self-signed cert
with New-SelfSignedCertificate (and Export-Certificate) each run which breaks
MSIX trust and update continuity; replace that block to load a persistent PFX
from secrets (e.g., decode GITHUB_SECRET_SIGNING_PFX to a file and use its
password from GITHUB_SECRET_SIGNING_PFX_PASSWORD) and call signtool.exe with the
/f <pfxPath> (and /p <password> and /fd SHA256) instead of using $cert. Keep
makeappx usage for packaging, remove per-run certificate export ($cerPath) and
the upload of a transient cert artifact (windows-cert-*) so the workflow uses
the long-lived PFX across releases and preserves update trust.

In `@frontend/src/updater.ts`:
- Around line 117-135: Both early-return branches stop re-arming the auto-check
timer when shouldAutoCheck() is false; ensure a replacement timer is scheduled
instead. In scheduleAutoUpdateCheck() and at the start of runAutoUpdateCheck(),
replace the immediate returns when !shouldAutoCheck() with calls to
queueAutoUpdateCheck(AUTO_CHECK_INTERVAL_MS) (or call it first, then return) so
a future re-evaluation is queued; keep existing logic that clears/sets
autoCheckTimer and avoid creating duplicate timers by preserving the
autoCheckTimer check in scheduleAutoUpdateCheck().

In `@internal/server/api.go`:
- Around line 135-158: The manual "Check for updates" route currently always
returns the warm cache; modify the handler to accept a cache-bypass flag (e.g.
query param "force" or "bypass") and when that flag is present skip both
readCachedUpdate() and the s.updateCache TTL short-circuit so the handler always
calls s.updater.Check(s.appVersion). After a successful forced check still
update s.updateCache (updateCacheEntry fields) and return the fresh result; keep
existing locking via s.updateCacheMu and error handling with writeError. Use the
presence/value of r.URL.Query().Get("force") (or similar) to implement the
bypass logic around the cached branches.

---

Nitpick comments:
In `@internal/update/update.go`:
- Around line 51-59: The Service constructor NewService sets an http.Client with
a 2s Timeout which is redundant because fetchManifest already uses a per-request
context timeout; remove the client-level timeout (i.e., stop constructing client
as &http.Client{Timeout: 2 * time.Second} and instead use &http.Client{} or nil
timeout) so the per-request context in fetchManifest controls request deadlines
and avoids double timeouts.
- Around line 148-154: In the "linux" case handling (see manifest.Linux and
entry.AppImageURL), ensure you treat an empty or whitespace-only AppImageURL as
no action: trim entry.AppImageURL and if it's empty return "none", "", ""
instead of returning kind "appimage" with an empty URL; otherwise return
"appimage", the trimmed URL, "Download AppImage".
- Around line 123-126: The JSON decoder currently reads directly from resp.Body
(json.NewDecoder(resp.Body)), which can be unbounded; change it to decode from a
size-limited reader using io.LimitReader (e.g., io.LimitReader(resp.Body,
1<<20)) and add the "io" import; optionally define a constant like
maxManifestSize = 1<<20 and use json.NewDecoder(io.LimitReader(resp.Body,
maxManifestSize)) when decoding the manifest variable to prevent memory
exhaustion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32c273ac-8a76-4170-b825-4e9f53f8637c

📥 Commits

Reviewing files that changed from the base of the PR and between 61f3a0c and 346f9b5.

📒 Files selected for processing (21)
  • .github/workflows/release.yml
  • .goreleaser.yml
  • README.md
  • docs/ARCHITECTURE.md
  • docs/CONVENTIONS.md
  • frontend/src/components/App.tsx
  • frontend/src/components/SettingsView.tsx
  • frontend/src/main.tsx
  • frontend/src/state.ts
  • frontend/src/store.ts
  • frontend/src/types.ts
  • frontend/src/updater.ts
  • frontend/static/style.css
  • internal/server/api.go
  • internal/server/server.go
  • internal/update/update.go
  • internal/update/update_test.go
  • main.go
  • packaging/linux/croopor.desktop
  • packaging/windows/AppxManifest.xml.template
  • packaging/windows/croopor.appinstaller.template
✅ Files skipped from review due to trivial changes (9)
  • docs/CONVENTIONS.md
  • docs/ARCHITECTURE.md
  • README.md
  • packaging/linux/croopor.desktop
  • frontend/src/state.ts
  • .goreleaser.yml
  • packaging/windows/croopor.appinstaller.template
  • frontend/src/store.ts
  • packaging/windows/AppxManifest.xml.template
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/main.tsx
  • internal/server/server.go
  • frontend/src/types.ts

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment thread frontend/src/updater.ts
Comment thread internal/server/api.go Outdated
@mateoltd mateoltd changed the title wip: updater flow + release packaging feat: automatic update detection and release pipeline Apr 1, 2026
coderabbitai[bot]

This comment was marked as off-topic.

@mateoltd mateoltd merged commit 1b37a9a into main Apr 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants