Skip to content

feat(updater): route in-app updates through R2 with GitHub fallback (#219) - #1003

Merged
Astro-Han merged 8 commits into
devfrom
i219-updater-r2-feed
May 30, 2026
Merged

feat(updater): route in-app updates through R2 with GitHub fallback (#219)#1003
Astro-Han merged 8 commits into
devfrom
i219-updater-r2-feed

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

Mainland China users frequently hit slow / intermittent / blocked GitHub update checks. The prod release feed is already mirrored to Cloudflare R2 (dl.pawwork.ai, #1000 / mirror-release-to-r2.yml). This wires that mirror into the desktop updater so checks and downloads try R2 first and fall back to GitHub.

Closes #219.

Approach

electron-updater does not fail over across providers, and it binds the download source at check time (downloadUpdate() reuses the updateInfoAndProvider from the last checkForUpdates()). So feed selection is done at runtime via setFeedURL, not by changing the baked app-update.yml.

  • update-feed.ts (new): provider-agnostic feed selector. check() tries feeds in order (R2 first, GitHub fallback) with a per-feed timeout; the winning feed becomes active and serves the download. download() retries on GitHub if an R2 download fails. A generation guard stops a timed-out check that resolves late from clobbering the active feed.
  • index.ts: builds the ordered feeds (R2 + GitHub for prod; GitHub-only for beta, which has no mirror) and routes the controller's check/download through the selector. setupAutoUpdater sets an initial feed (with its own GitHub fallback) and logs the selection.
  • app-update.yml left as provider: github on purpose. The runtime always re-selects the feed before each check, so the baked value is only a startup safety net. Keeping GitHub there avoids touching the CI grep checks, the three app-update tests, and mac/Windows packaging, with no behavior difference (the baked value is overridden every check). This was the explicit design decision, cross-checked with a second reviewer.
  • mirror-release-to-r2.ts: now fails the mirror if a latest*.yml pointer references an asset it would not upload — guards the R2 feed alignment (the easy-to-break part).
  • Dev harness: PAWWORK_DEV_UPDATER=1 enables forceDevUpdateConfig under dev:desktop to exercise the real R2 feed without a signed build.

Verification

  • bun test in packages/desktop-electron: 423 pass, 1 fail — the lone failure is the pre-existing packaged-app smoke test that needs a built/signed binary (unrelated, fails on dev too).
  • typecheck (tsgo), biome lint on touched files, and electron-vite build all green.
  • New unit tests cover feed selection, throw/timeout fallback, download fallback, single-feed (beta), the generation guard, and the mirror pointer-alignment check.
  • Live check (v2026.5.29): R2 latest-mac.yml / latest.yml are byte-identical to the GitHub release, and every referenced binary (.zip / .dmg / .exe / .blockmap) HEADs 200 on dl.pawwork.ai — confirms the sha512 chain holds and the feed resolves.

Not done here

  • Full in-app electron-updater walk via dev:desktop (GUI) was not run in this environment. The harness is wired; run PAWWORK_DEV_UPDATER=1 bun run dev:desktop and trigger Check for Updates to watch the update feed selected / update metadata fetched logs and the GitHub fallback. The live-endpoint checks above cover the network side.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • R2 mirror used as primary update feed with automatic GitHub fallback.
    • Per-feed timeout for update checks to avoid hangs.
    • Opt-in dev updater and clearer updater enablement at startup.
    • Validation to ensure pointer-referenced release assets are included in mirrors.
  • Tests

    • Added tests for feed selection, failover/download retry behavior, timeout handling, and pointer-asset validation.

Review Change Stack

…219)

Mainland China users frequently hit slow, intermittent, or blocked GitHub
update checks. The prod release feed is already mirrored to Cloudflare R2
(dl.pawwork.ai, PR #1000 / mirror-release-to-r2.yml). This wires that
mirror into the desktop updater.

electron-updater does not fail over across providers and binds the download
source at check time (downloadUpdate reuses the updateInfoAndProvider from
the last checkForUpdates). So feed selection is done at runtime via
setFeedURL, not by changing the baked app-update.yml:

- new update-feed.ts: a provider-agnostic feed selector. check() tries feeds
  in order (R2 first, GitHub fallback) with a per-feed timeout; the winning
  feed becomes active and serves the download. download() retries on GitHub
  if an R2 download fails. A generation guard stops a timed-out check that
  resolves late from clobbering the active feed.
- index.ts: build the ordered feeds (R2 for prod, GitHub fallback; beta is
  GitHub-only with no mirror) and route the controller's check/download
  through the selector. setupAutoUpdater sets an initial feed (with its own
  GitHub fallback) and logs the selection.
- app-update.yml is intentionally left provider: github. The runtime always
  re-selects the feed before each check, so the baked value is only a startup
  safety net; keeping GitHub there avoids touching CI grep checks, the three
  app-update tests, and Windows/mac packaging, with no behavior difference.
- mirror-release-to-r2.ts now fails the mirror if a latest*.yml pointer
  references an asset it would not upload, guarding the R2 feed alignment.
- dev harness: PAWWORK_DEV_UPDATER=1 enables forceDevUpdateConfig under
  dev:desktop to exercise the real R2 feed without a signed build.

Verification: bun test (423 pass; the lone failure is the pre-existing
packaged-app smoke test that needs a built binary), typecheck, lint, and
electron-vite build all green. Live check against the current release
(v2026.5.29): R2 latest-mac.yml / latest.yml are byte-identical to the
GitHub release and every referenced binary HEADs 200 on dl.pawwork.ai.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 56 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c6f0c66e-c16b-48b6-80c8-4597e0e10468

📥 Commits

Reviewing files that changed from the base of the PR and between b5276f4 and fe1b590.

📒 Files selected for processing (1)
  • packages/desktop-electron/src/main/index.ts
📝 Walkthrough

Walkthrough

Adds an R2-primary/GitHub-fallback updater feed system with per-feed probes and timeout, enforces pointer YAML asset alignment during mirroring, introduces updater configuration constants, rewires updater initialization to use the feed selector, and adds tests for feed selection, download failover, and pointer validation.

Changes

Multi-feed Updater with R2 Mirror Fallback

Layer / File(s) Summary
R2 mirror pointer validation
packages/desktop-electron/scripts/mirror-release-to-r2.ts, packages/desktop-electron/scripts/mirror-release-to-r2.test.ts
Adds POINTER_YMLS, exports pointerReferencedAssets() and missingPointerReferences(), validates pointer YAML references prior to upload, and tests parsing/deduplication/URL→filename reduction and missing-reference detection.
Updater configuration constants
packages/desktop-electron/src/main/constants.ts
Exports DEV_UPDATER, UPDATER_ACTIVE, UPDATE_CHANNEL, UPDATE_GITHUB_OWNER, UPDATE_GITHUB_REPO, DOWNLOAD_PUBLIC_BASE, and UPDATE_R2_ENABLED.
Feed types, probe, and helpers
packages/desktop-electron/src/main/update-feed.ts
Defines feed types and Deps shape, implements HEAD reachability probe with per-feed timeout and timer injection, and adds r2Feed() / githubFeed() builders.
Feed selection and operations
packages/desktop-electron/src/main/update-feed.ts
Implements createUpdateFeed() with sequential feed selection, check() that rebinding across feeds on failure, download() that retries once via GitHub fallback enforcing version equality, and activeFeed() accessor.
Update-feed tests
packages/desktop-electron/src/main/update-feed.test.ts
Adds tests for feed selection, probe timeout/abort, download retry and fail-closed cases, and feed builder expectations.
Updater initialization and wiring
packages/desktop-electron/src/main/index.ts, packages/desktop-electron/src/main/index-updater-source.test.ts
Adds UPDATE_FEED_TIMEOUT_MS, builds ordered feeds (R2-first when enabled), creates updateFeed via createUpdateFeed, delegates controller check/download to feed, rewrites setupAutoUpdater() to gate on UPDATER_ACTIVE and set initial feed URL with fallback; updates test guard naming.
Updater module seam
packages/desktop-electron/src/main/updater.ts
Re-exports createUpdateFeed, r2Feed, githubFeed, and FeedTarget for consumers.

Sequence Diagram

sequenceDiagram
  participant App as app (setupAutoUpdater)
  participant Feed as createUpdateFeed
  participant Probe as HEAD probe
  participant R2 as R2 feed
  participant GH as GitHub feed
  participant Updater as electron-updater

  App->>Feed: build feeds (R2 first, GitHub fallback)
  Feed->>Probe: probe R2 (with timeout)
  Probe-->>Feed: reachable / unreachable / timeout
  Feed->>Updater: setFeedURL(chosen feed)
  App->>Updater: checkForUpdates() (delegated to Feed.check)
  Feed->>R2: checkForUpdates (if active)
  R2-->>Feed: success / throw
  Feed->>GH: fallback check when R2 fails
  Feed->>Updater: downloadUpdate() (delegated to Feed.download)
  Updater-->>Feed: download failure
  Feed->>GH: switch to GitHub, rebind & checkForUpdates, retry download once
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

desktop

🐰 I hopped through feeds with a careful plan,
R2 seeks first, GitHub lends a hand,
Pointers checked, no missing tile,
Timeouts trim the hanging trial,
Mirrors hum—updates land!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and specifically summarizes the main change: routing in-app updates through R2 with GitHub fallback, directly corresponding to the PR's primary objective.
Description check ✅ Passed Description is well-structured with Summary, Why, Related Issue, Review Focus, Risk Notes, How To Verify, and mentions multiple checklist items though not all appear completed.
Linked Issues check ✅ Passed Code changes fully implement the core objectives from #219: R2 mirror as primary feed [update-feed.ts, index.ts], GitHub fallback with timeout/retry logic [update-feed.ts, index.ts], signature verification preserved [implicit via byte-identical mirrors], and dev harness for testing [PAWWORK_DEV_UPDATER flag in constants.ts].
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of routing updates through R2 with GitHub fallback; no unrelated refactors, test utilities, or out-of-scope alterations detected.

✏️ 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 i219-updater-r2-feed

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.

@github-actions github-actions Bot added platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions P2 Medium priority labels May 30, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested priority: P2 (includes user-path files (packages/desktop-electron/src/main/constants.ts, packages/desktop-electron/src/main/index.ts, packages/desktop-electron/src/main/update-feed.test.ts, packages/desktop-electron/src/main/update-feed.ts, packages/desktop-electron/src/main/updater.ts)).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a runtime update feed selection mechanism for the in-app updater, prioritizing Cloudflare R2 with a fallback to GitHub to improve update reliability and speed. It also adds unit tests and validation to ensure all referenced assets are mirrored before uploading. However, there are several critical code duplications that must be resolved: duplicate constant declarations in constants.ts and index.ts which will cause TypeScript compilation errors, and an identical duplicated test suite in mirror-release-to-r2.test.ts.

Comment thread packages/desktop-electron/src/main/constants.ts Outdated
Comment thread packages/desktop-electron/src/main/index.ts Outdated
Comment thread packages/desktop-electron/scripts/mirror-release-to-r2.test.ts Outdated
@Astro-Han Astro-Han added enhancement New feature or request P1 High priority labels May 30, 2026
@github-actions github-actions Bot removed the P2 Medium priority label May 30, 2026
Astro-Han and others added 3 commits May 30, 2026 23:20
A bad edit replay during development inserted the constants.ts feed block
and the index.ts UPDATE_FEED_TIMEOUT_MS const twice, which compiled locally
in a stale state but failed CI typecheck (TS2451 redeclare) and broke the
desktop build/tests. Deduplicate to a single declaration of each.

Verified locally: typecheck, biome lint, electron-vite build, and bun test
(535 pass, 0 fail) all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The source-contract test still grepped the old `!UPDATER_ENABLED` guard,
but setupAutoUpdater now early-returns on `!UPDATER_ACTIVE`. This edit was
lost in an earlier replay and never made the first commit, so unit-desktop
failed. Point the regex at the current guard.

Verified: bun test in packages/desktop-electron is 436 pass, 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same edit-replay glitch that duplicated the constants/index declarations
also inserted the "pointer reference alignment" describe twice in the mirror
test (it passed because the cases just ran twice). Gemini flagged it; remove
the second copy.

Verified: bun test mirror-release-to-r2.test.ts is 7 pass, 0 fail; biome lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Astro-Han

Copy link
Copy Markdown
Owner Author

All three duplication findings are resolved (constants.ts + index.ts in 0ee7381, mirror test in d3601ed). Local typecheck, lint, build, and bun test (436 pass) are green.

/gemini review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/desktop-electron/src/main/index.ts`:
- Around line 709-714: The renderer-visible updater flag is out of sync with the
main-process enablement (UPDATER_ACTIVE/DEV_UPDATER); make the renderer state
derive from the same source of truth. Update diagnostics() and getWindowConfig()
to compute and return UPDATER_ENABLED from the same constant used to enable the
updater (UPDATER_ACTIVE || DEV_UPDATER or a single exported UPDATER_ACTIVE
constant), and ensure any window config property (e.g., updaterEnabled /
UPDATER_ENABLED) mirrors that value so the renderer and main process always
agree when autoUpdater is enabled.

In `@packages/desktop-electron/src/main/update-feed.ts`:
- Around line 114-120: The fallback path to GitHub must run the same bounded &
validated re-check as the normal flow: when you detect a github feed (deps.feeds
/ github) and before calling deps.downloadUpdate(), call the same
timeout-wrapped check logic (the one used by check()) against
deps.checkForUpdates() so it cannot hang, then validate the returned result is
non-null and result.isUpdateAvailable === true; only then call
deps.downloadUpdate(); otherwise log a clear error via deps.error and
exit/throw/return the failure (do not proceed to download). Also ensure
activeLabel is only set to "github" after the re-check succeeds. Use the
existing symbols deps.checkForUpdates, deps.downloadUpdate, deps.setFeedURL,
activeLabel and the check() timeout wrapper to implement this.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f129c8e-25ab-4dbf-8375-e873872fdf4d

📥 Commits

Reviewing files that changed from the base of the PR and between ef05cb0 and d3601ed.

📒 Files selected for processing (8)
  • packages/desktop-electron/scripts/mirror-release-to-r2.test.ts
  • packages/desktop-electron/scripts/mirror-release-to-r2.ts
  • packages/desktop-electron/src/main/constants.ts
  • packages/desktop-electron/src/main/index-updater-source.test.ts
  • packages/desktop-electron/src/main/index.ts
  • packages/desktop-electron/src/main/update-feed.test.ts
  • packages/desktop-electron/src/main/update-feed.ts
  • packages/desktop-electron/src/main/updater.ts

Comment thread packages/desktop-electron/src/main/index.ts
Comment thread packages/desktop-electron/src/main/update-feed.ts Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a runtime update feed selection mechanism for the Electron in-app updater, prioritizing a Cloudflare R2 mirror with a fallback to GitHub to improve reachability in mainland China. The feedback highlights two critical issues in the implementation: a race condition in update-feed.ts where a timed-out update check can resolve late and corrupt the shared state of the autoUpdater singleton, and a missing timeout on the fallback update check during the download phase, which could cause the updater to hang indefinitely under poor network conditions.

Comment thread packages/desktop-electron/src/main/update-feed.ts Outdated
Comment thread packages/desktop-electron/src/main/update-feed.ts Outdated
… check race (#219)

Review P1: the R2-timeout fallback raced electron-updater's checkForUpdates()
with Promise.race but never cancelled the underlying check. A slow R2 check
could resolve late on the shared autoUpdater instance and rebind the provider
back to R2 *after* we had fallen back to GitHub — so "fell back to GitHub" was
a lie and the download could still go to the slow/unreachable R2 the fallback
exists to avoid. The generation guard only protected our return value, not
electron-updater's internal updateInfoAndProvider.

Fix: choose the feed with a cancellable reachability probe (HEAD on the channel
file, aborted on timeout via AbortController), then run exactly one
checkForUpdates() against the winning feed. Only one real check ever runs, bound
to the chosen feed; downloadUpdate() reuses that provider. No abandoned check
can rebind it.

Also addresses review P2: the download-fallback now compares the GitHub
re-check version against the version the controller validated and fails closed
on mismatch, so it can never mark a different release ready than it checked.

Verified: typecheck, lint, build, bun test (433 pass, 0 fail). New tests assert
single-check-on-fallback (incl. the abort/timeout path) and the version-mismatch
fail-closed. Real electron-updater fallback (point R2 at a bad host under
PAWWORK_DEV_UPDATER=1 dev:desktop) remains a manual pre-merge check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Astro-Han

Copy link
Copy Markdown
Owner Author

Addressed the review in 96d83bc9c8.

P1 (blocking) — fixed. The timeout fallback no longer races the uncancellable checkForUpdates(). Feed selection now uses a cancellable reachability probe (HEAD on the channel latest*.yml, aborted on timeout via AbortController); only the winning feed gets a single real autoUpdater.checkForUpdates(). Since no R2 check is left in flight, none can resolve late and rebind the provider after a GitHub fallback. New test asserts that on an aborted/timed-out R2 probe exactly one check runs and it is bound to GitHub.

P2 — fixed. The download fallback now compares the GitHub re-check version against the version the controller validated and fails closed on mismatch, so it can never mark a release ready that it did not check. New test covers R2 vA → R2 download fail → GitHub re-check vB → throws.

Verified locally: typecheck, lint, build, bun test (433 pass, 0 fail). The real electron-updater fallback harness (point R2 at a bad host under PAWWORK_DEV_UPDATER=1 bun run dev:desktop) stays a manual pre-merge check.

… to renderer (#219)

- check() now catches R2 checkForUpdates() reject and retries on the
  next feed (GitHub). Covers "R2 probe 200 but metadata fetch fails"
  — including the 60 s socket timeout built into builder-util-runtime.
- diagnostics() and getWindowConfig() now expose UPDATER_ACTIVE instead
  of UPDATER_ENABLED so the renderer reflects dev-harness updater state.
- 3 new tests: R2 check reject → GitHub fallback, both feeds reject →
  throw, download fallback re-check reject → fail closed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Astro-Han

Copy link
Copy Markdown
Owner Author

Addressed in 8510379.

P1 (blocking) — fixed. check() now wraps deps.checkForUpdates() in a try/catch loop over remaining feeds. If R2's probe passes but the real metadata fetch rejects (DNS flip, transient 5xx, or the 60 s socket timeout built into builder-util-runtime's addErrorAndTimeoutHandlers), we setFeedURL(github) and retry. The last feed (GitHub) throws on failure so the caller gets a definitive error.

Key electron-updater internals verified in source (node_modules/.bun/electron-updater@6.8.3):

  • checkForUpdatesPromise is nullized on reject (line 262–273 of AppUpdater.js), so a second checkForUpdates() call creates a fresh promise — no dedup hazard.
  • setFeedURL() replaces clientPromise with the new provider (line 247), so the retry uses GitHub's provider.
  • The underlying HTTP layer has a default timeout = 60 * 1000 socket timeout (httpExecutor.js:106), so checkForUpdates() cannot hang indefinitely.

New tests:

  • R2 probe succeeds → R2 check rejects → falls back to GitHub (asserts setFeedURL called twice, activeFeed() === "github")
  • Both feeds reject → throws
  • Download fallback: GitHub re-check rejects → throws (fail closed, no GitHub download attempted)

P2 — addressed by design. The download fallback's await deps.checkForUpdates() will reject after the same 60 s socket timeout if GitHub is unreachable. The error propagates from download() — fail closed. Added a log line before the re-check for observability. Adding our own Promise.race timeout would reintroduce the P1 race (we can't cancel checkForUpdates()), and 60 s is acceptable for a background download recovery path. New test covers the re-check reject path.

P3 — fixed. diagnostics() and getWindowConfig() now expose UPDATER_ACTIVE instead of UPDATER_ENABLED. Removed the unused UPDATER_ENABLED import from index.ts.

Astro-Han and others added 2 commits May 31, 2026 01:21
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Astro-Han
Astro-Han merged commit ff40869 into dev May 30, 2026
27 checks passed
@Astro-Han
Astro-Han deleted the i219-updater-r2-feed branch May 30, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request P1 High priority platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Updater reliability for mainland China (non-GitHub mirror / CDN)

1 participant