.gitattributes keeps docs/**/*.md LF, so the CRLF replacements in read() and
splitFrontmatter are no longer needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsdTn4ZMvVxxETgpvb1Li3
A summary should fit one 120-column line of the generated list (118
characters after the `- `), and a second indented line is fine. Past 236
characters docs-check reports a notice. Notices are a new kind of warning that
fails nothing, even under --strict. Past 512, up from 200, it is an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsdTn4ZMvVxxETgpvb1Li3
The anchor pattern now needs a word boundary before ADR, so an identifier
ending in ADR, such as LOADR(…), is not read as an anchor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsdTn4ZMvVxxETgpvb1Li3
An ADR link to the evidence root counted as a folder link, so one link to
../evidence/ marked every evidence file as linked. Only links strictly below
docs/evidence/, to a file or a subfolder, now count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrXVKz8zBJAvnxa6SKhJtB
scripts/docs-check.ts generates docs/README.md and the cross-cutting block in
AGENTS.md from ADR frontmatter, and fails when either is stale. It validates ADR
filenames, frontmatter, MADR headings and the Bad-because line, supersession
chains, DECISION(slug) anchors, docs/ paths named in source, relative links and
their fragments in every Markdown file, and evidence that no ADR links to.
--write regenerates; --strict also fails on warnings.
It runs on Node's own type stripping and needs no new dependency: yaml parses
frontmatter, marked's lexer finds links and headings, git ls-files lists files.
npm scripts: docs:gen and docs:check.
docs/adr-template.md and docs/decision-tags.yml are the template and the tag
vocabulary. tsconfig.json now type-checks scripts/**/*.ts, and vitest runs
scripts/**/*.test.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrXVKz8zBJAvnxa6SKhJtB
## Summary
The repository's Actions caches reached 9.57 GB of GitHub's 10 GB quota,
and the Rust caches had roughly doubled in ten days. When `Cargo.lock`
changes, rust-cache restores the previous entry as a fallback, cargo
builds the new dependency versions beside the old ones, and the save
keeps both, because its cleanup matches artifacts by crate name only.
Every dependency update stacked another generation onto every platform's
cache.
This change puts a hash of the Rust dependency set into each rust-cache
`key`. The key is part of rust-cache's restore prefix, so a dependency
change has nothing to fall back to: it builds cold once per platform,
and each saved entry holds a single generation. It also stops caching
Gradle, whose 843 MB entry only saved dependency downloads.
## Changes
- **`scripts/ci/cargo-deps-hash.js`** prints the first 16 hex characters
of a SHA-256 over `Cargo.lock`'s registry and git packages (dropping the
`aventura` workspace crate) and `src-tauri/Cargo.toml` without its
`[package]` version line. A release version bump leaves it unchanged; a
dependency version, feature or profile change does not.
- **`.github/actions/rust-cache`** (new composite action) runs the
script and calls `swatinem/rust-cache@v2` with `key: <key>-<hash>`,
`workspaces: '<working-directory>/src-tauri -> target'` and `save-if` on
`master` only. If the script fails, the step fails rather than saving
under a key without the hash. Inputs: `key`, `working-directory`
(default `.`) and `cache-directories`.
- **Every rust-cache step uses the action:** `build-desktop.yml`
(`<target>-<hash>`, with the Dev Drive path as `working-directory`),
`build-android.yml` (`android-v2-<hash>`, still persisting the generated
Kotlin), `build-ios.yml` (`ios-<hash>`) and `rust-lint.yml`'s Clippy job
(`clippy-<hash>`). `rust-lint.yml` also runs when the script or the
action changes.
- **Gradle is not cached.** `setup-gradle` keeps `cache-provider: basic`
and adds `cache-disabled: true`, so it still validates the wrapper and
writes the job summary.
- **`docs/development/ci-caching.md`** (new) records what each cache
holds and why it is configured that way: the diagnostic runs, how
rust-cache keys, restores and prunes, the options weighed against the
dependency hash, the Gradle provider comparison, and the decision to
keep `armv7`. `release.md` keeps the rules and links there, and its
generated-Kotlin explanation moves there. `ci.yml`'s comments,
`ios-build-target.md` and `docs/README.md` are updated to match.
### Effect
- The first `master` run after this merges, and the first after any
later dependency, feature or profile change, builds every Rust cache
cold. A PR that makes such a change also builds cold.
- Release runs keep hitting: the version bump changes neither this hash
nor rust-cache's own lock hash.
- The existing, oversized entries stay until GitHub evicts them as least
recently used. Deleting them by hand (`gh cache list --key v0-rust-`,
then `gh cache delete <id>`) frees the space right away.
- Android runs download the Gradle distribution and dependencies each
time.
## Testing
- New unit tests for `cargoDepsHash`: unchanged by an app version bump
and by CRLF line endings, changed by a registry package version and by a
dependency feature.
- `node scripts/ci/cargo-deps-hash.js` on this branch prints
`8cdb41fe24f6ce46`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01AhkosChqt73QngC3UqQB9C
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Android, desktop, iOS, and Rust lint workflows now use shared Rust
caching that reflects dependency changes.
* Android builds no longer cache Gradle data.
* CI workflow descriptions now reflect the updated cache behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary
The Rust crate is format-checked but never linted. This fixes the
crate's existing Clippy warnings and adds `cargo clippy -D warnings` to
the pre-push hook and to CI. To keep Rust and TypeScript linting
independent, `lint` is split into `lint:ts` and `lint:rust`, and hooks,
CI and the release script call the half they need.
## Changes
- **Clippy fixes.** In `backup.rs`, two nested `if let … { if … }`
blocks become let-chains (`collapsible_if`). In `migration_patch.rs`,
`apply_checksum_patch` takes `&Path` instead of `&PathBuf` (`ptr_arg`).
No behaviour change.
- **Scripts.** `package.json` adds `lint:ts` / `lint:ts:fix` (ESLint)
and `lint:rust` / `lint:rust:fix` (`cargo fmt` plus `cargo +1.99.0
clippy --all-targets --all-features -D warnings`). `lint` and `lint:fix`
run both halves through `scripts/pm.js`.
- **Pre-push hook.** `lefthook.yml` runs `lint:ts` and `lint:rust`
through `scripts/pm.js`, under the same globs as before.
- **CI.** `rust-lint.yml` gains a `clippy` job on Rust 1.99.0 with the
Linux WebKit/GTK build dependencies and a Rust cache.
`lint-and-typecheck.yml` runs `lint:ts`.
- **Release script.** `scripts/release.js` runs `lint:ts:fix`, so a
release never needs the Rust toolchain or rewrites Rust code.
- **Docs.** `CLAUDE.md`, `README.md` and `docs/development/release.md`
describe the split scripts, the hook commands, the Clippy job, and how
to bump the pinned toolchain.
### Changes to existing behaviour
- `npm run lint` and `npm run lint:fix` now also lint the Rust crate, so
they need `cargo` and the 1.99.0 toolchain with Clippy (`rustup
toolchain install 1.99.0 --component clippy`). `lint:ts` / `lint:ts:fix`
keep the old ESLint-only behaviour.
- The pre-push commands are renamed `lint` → `lint-ts` and `rust-fmt` →
`lint-rust`. Anything that skips or overrides them by name
(`LEFTHOOK_EXCLUDE`, `lefthook-local.yml`) needs the new names.
## Test plan
- [ ] `npm run lint:rust` passes
- [ ] The "Rust Lint" `Clippy` job runs on this PR and passes
- [ ] `npm run lint` runs ESLint, then rustfmt and Clippy
- [ ] Pushing a commit that touches only `.rs` files runs `lint-rust`
but not `lint-ts` or `check`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Mp6GGHh2aY397TuZXyUXKX
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Developer Tooling**
* TypeScript and Rust linting run separately, with Rust formatting and
Clippy checks included in the standard lint workflow.
* Rust lint checks also run in continuous integration, with warnings
treated as errors.
* Pre-push checks now run both TypeScript and Rust linting. Release
preparation runs TypeScript formatting and lint fixes.
* **Documentation**
* Updated build-script descriptions to reflect the TypeScript and Rust
lint commands and their fix options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary
`scripts/release.js` no longer cuts a `release/vX.Y.Z` branch and
fast-forwards the original branch afterwards. It now refuses to run from
any branch but `master`, commits the version bump there, tags it, and
pushes `master` and the tag in one atomic push. The version on `master`
is then always the version that was tagged, and the tag can never point
at a commit `master` lacks.
## Changes
**Release script (`scripts/release.js`)**
- Removes the `release/vX.Y.Z` branch and the best-effort merge-back
step, along with the `--no-merge-back` flag.
- Pre-flight fails unless the current branch is `master` (or reports a
detached HEAD), before anything is written.
- Pushes `master` and the tag with `git push --atomic`.
- Pre-flight also checks that the remote copy of the branch is an
ancestor of `HEAD`, using the SHA from the existing `ls-remote` call. A
remote with commits the checkout lacks fails up front, instead of
rejecting the push after the bump is committed and tagged.
- Rollback now runs `git reset --hard` to the starting commit and
deletes the tag, replacing the checkout-and-delete-branch cleanup.
**Docs and CI comments**
- `docs/development/release.md`: the "Cutting a New Release" steps,
preconditions and rollback description match the new flow.
- `.github/workflows/ci.yml` and the matching paragraph in `release.md`:
comments describing the bump push as a fast-forward now say it is a push
to `master`.
## Testing
Not run. `release.js` has no tests, and I did not run `npm run release
-- patch --dry-run`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01NH3m3rkE2Mm9Bz5Eonpfxz
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Releases are now created from `master`; release attempts from other
branches or detached checkouts are rejected.
* The version update and release tag are pushed together. Checks catch
existing tags and remote changes not present locally, and failed pushes
restore the branch to its starting point.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
**Summary**
Follow-up to the iOS build target (#541). It fixes problems found while
running the build on real iOS/iPadOS WebViews, and it adds the missing
app icon step. Every behavioural change is behind `isIos()` or lives in
an iOS-only file, so other platforms take the same code path as before.
**Changes**
*Platform detection and restore*
- **iPadOS detection:** an iPad's WKWebView reports a Mac user agent by
default, so `isIos()` and `isMobileDevice()` missed it. The update check
took the desktop path and open dialogs kept filters iOS can't map. A Mac
UA with `maxTouchPoints > 1` now counts as iOS, with tests in
`platform.test.ts`.
- **Backup restore:** the iOS file picker returns a `file://` URL that
the native restore can't `std::fs`-open. iOS now stages it through
`import_saf_to_temp`, as Android does.
- **Restore exit:** the restore flow shows a toast and waits before
`exit(0)`, so quitting doesn't look like a crash.
*Config and build*
- **Updater string:** the updater text now mentions iOS.
- **iOS config:** new `src-tauri/tauri.ios.conf.json` sets
`minimumSystemVersion` to 16.4, the frontend's WebKit floor for Tailwind
v4 `@property`, cascade layers and `color-mix()`. It also empties
`build.features`, so `tauri ios dev` no longer needs the manual
`--config` workaround.
- **App icon:** `tauri ios init` fills the Xcode AppIcon set with the
Tauri placeholder. `scripts/sync-ios-icons.sh` copies
`src-tauri/icons/ios` over it and fails if the catalog names an icon the
Aventuras set lacks. `build-ios-unsigned.sh` and `bootstrap-ios.yml`
both run it.
*UI and services*
- **Update dialog:** on iOS the dialog points to a sideloading tool
(AltStore, Sideloadly or TrollStore), since the `.ipa` is unsigned and
Safari can only save it.
- **TTS volume:** `HTMLMediaElement.volume` is a no-op on iOS. The
settings now note this and disable the volume override switch and slider
there.
- **ComfyUI:** WKWebView's App Transport Security can silently block raw
`fetch()` to a ComfyUI host outside the local-network exception.
- `fetchModelList` now uses Tauri's HTTP plugin on iOS and forwards
`timeoutMs`.
- The SDK's `fetchApi()` (checkpoints, samplers, prompt queueing, image
retrieval) is patched on the instance, iOS only, to use the same plugin.
*Docs*
- `docs/development/ios-build-target.md` and
`docs/development/release.md` are updated. That includes the new config
file and corrections to two stale claims.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Improved iOS Support**
* Backup restoration supports selected files and displays completion
instructions before the app closes or prompts you to restart.
* Image generation connections are more reliable.
* Update instructions explain how to install iOS downloads with a
sideloading tool.
* Volume controls are unavailable with an explanatory notice when the
selected provider does not support them on iOS.
* iOS app builds require iOS 16.4 or later and include synchronized app
icons.
* **Bug Fixes**
* Improved iPad recognition, including iPadOS devices that identify as
Mac computers.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
The generation activity report could not be relied on when something
went wrong:
- steps were swallowing the exceptions they produced, with no chance for
a user to see them
- a step whose call failed usually closed as finished, because the
services absorb their failures into a fallback;
- several requests behind one step — retries, the narrator's
empty-answer loop — showed as a single row;
- a step could stay *in progress* indefinitely while a retry loop ran
out of sight, and the narrator's own errors were swallowed and re-sent
as empty answers.
This makes the report readable in full and makes it say why each step
failed, request by request, without changing what a turn does.
## Changes
**Display**
- The timeline is uncapped, read by scrolling the story like the
reasoning block. Times are rounded to the nearest second, sit in a fixed
column, and are left blank for a step recorded after the fact without a
duration (an agent tool call, a cached selection) instead of reading a
default `0s`.
- The collapsed line wraps instead of being cut, and on a narrow card
shares its row with the story time.
- The latest turn's timeline opens and the previous one folds; a
reader's own choice sticks. Records are no longer hidden when a turn
ends, and the session keeps the last 20.
- Sending scrolls to the end of the story, and the story view no longer
jumps when the streaming card is replaced by the entry.
- The sparkle marks the model calls themselves, not the steps that
contain them.
**Failure reporting**
- A failed step carries its reason, worded once by
`describeActivityError`: status and provider message for an API error,
`Failed after N attempts, with no retries left: <last reason>` for
exhausted retries, a lost connection named as such rather than the HTTP
plugin's `error decoding response body`.
- Services that absorb a failure into a fallback — translation,
suggestions, action choices, timeline fill, Tier 3 selection, scene
analysis, the background image, the classifier's `_error` — still close
the step they serve as failed (`failStep`) and return the same fallback
as before.
- A failure is told once, on the deepest step that carries it; every
step above it is marked with a red ⚠. When the only failures beneath a
step are attempts its request got past, the ⚠ is in the text colour.
- A turn ends with an outcome (`turnOutcome`); only a halted turn says
*Failed* on the collapsed line. A halted turn's record moves to its
error entry, which is marked by `metadata.source` rather than recognised
by its text, and every other system entry gets edit, delete and
regenerate. The "Generation failed" banner above the input is removed:
it duplicated the error card.
- The error card for a turn without narration reads *The narration could
not be generated. Please try again.* — it no longer claims a number of
attempts.
**Requests and retries**
- `activityMiddleware` sits inside `retryOn429Middleware`: a request
that needs a second attempt gets a row per attempt (the first
backfilled) and a row per wait the retry middleware schedules. The SDK's
own retries show as attempts.
- The narrator's empty-answer loop reports *passes*, kept apart from the
transport attempts inside them. A single pass has no container; it is
grouped into `Pass 1` only once a second follows. An empty pass is a
failure.
- Within a pass, `Waiting for model` gives way to `Generating` as soon
as the response starts, so a slow first token and an empty answer are
both visible.
**Narrator errors**
- `streamText` reports a failed request as an `error` part rather than
by throwing. The narrator dropped it, so every narrator failure read as
an empty answer and was re-sent twice. The part is now rethrown, with a
provider's in-stream error object turned into an `Error` with its code
and message.
- A stream that fails after text has arrived keeps that text as the
narration and reports the failure without halting: the request was paid
for, and the reader can still regenerate. One that fails before any text
is passed again, like an empty answer; a request refused outright is
not.
**Phases**
- `Updating world` and `Saving translation` are steps of their own, so a
failed write shows where it happened. `Updating world` reads *partly
applied* after a salvaged classification and *fallback bookkeeping only*
after a failed one.
- Agent Mode's hand-off is a `Handed off · N images queued` step under
*Images*: the images finish after the turn, and the record does not
follow them.
- A phase switched off in settings is not reported; one switched on but
missing its profile is struck through with that reason.
**Stop**
- Once the narration is saved, Stop dims, and a tap explains with a
toast that the turn is finishing steps that cannot be interrupted.
Nothing after the narration observes the abort, so Stop used to free the
input while the turn still held the generation lease: Send was offered
and refused, the story could not be left, and the report went on
counting. "Generate a different response", which refuses Stop
throughout, now explains its disabled Stop the same way.
**Tooling**
- `scripts/fake-provider.mjs` (`npm run fake-provider`) is an
OpenAI-compatible chat and image API whose behaviour is chosen by the
requested model name: rate limits, 4xx and 5xx, hangs, empty, malformed
and partly valid answers, cut streams. Setup and the model list are in
`docs/development/testing.md`.
`docs/architecture/overview.md` describes the reporting model, the
narrator's passes and the Stop refusal.
## Testing
- New unit tests for `describeActivityError`, `turnOutcome`, the
recorder (including `groupChildren` and untimed steps), the tree
readings (`failuresShownBelow`, `failureMarks`, `stepsAboveLLMSteps`),
`trackPhase`, the retry and activity middleware, the narrator's chunk
translation, each phase's reporting, and `storyEntries`.
- `reportingDoesNotAlterTurn.test.ts` and `absorbedFailures.test.ts`
check that reporting changes nothing a turn does and that every absorbed
failure still returns its fallback.
- Every call a turn makes was run against the fake provider in the
desktop app, each in the failures it can meet; the display was also
checked on Android.
- No test renders a component; the timeline, the collapsed line and the
Stop button were checked in the running app.
## Known gaps
- The retrieval agent builds its model elsewhere and its retries are not
reported.
- Stop still cannot cancel work after the narration is saved; this only
stops it from claiming to.
- On Android a tap on Stop while the narration streams can fail to
register; that is separate from this change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Generation activity reports now show clearer step summaries, elapsed
times, failures, recoveries, and retry attempts. More activity history
is retained, and reports are visible by default.
* Generation errors include clearer explanations. Narration already
produced is preserved if a later step fails, and failed generations can
be retried from the story.
* The story view scrolls to the latest entry when requested during
generation.
* Added a fake AI provider for testing successful responses and failure
scenarios.
* Story creation no longer waits for background-image generation to
finish.
* **Bug Fixes**
* Stop controls now reflect when generation can no longer be
interrupted.
* Generation and translation failures are more consistently shown in
activity reports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tony Frazier <16245649+a-frazier@users.noreply.github.com>
## Summary
- Adds `scripts/package-manager.js`, which detects whether `aube` or
`npm` owns the current
`node_modules` (by checking for `node_modules/.aube-state`, then
confirming `aube` is still
on `PATH`) and builds the right `[cmd, args]` pair for
`spawnSync`/`execFileSync`, including
the Windows `npm.cmd` workaround.
- Adds `scripts/pm.js`, a thin CLI (`node scripts/pm.js <args...>`) that
runs that detected
package manager with the given arguments and forwards its exit status.
- Routes `lefthook.yml`'s `pre-push` `lint`/`check` commands and
`tauri.conf.json`'s
`beforeDevCommand`/`beforeBuildCommand` through `scripts/pm.js`, so they
use aube wherever
aube did the install and npm otherwise.
- `scripts/release.js` keeps the version bump on npm (npm owns
`package-lock.json`), but now
runs its `format`/`lint:fix` steps through the detected package manager.
When that manager
is aube, it adds `--no-install --frozen-lockfile` so aube doesn't
re-resolve the lockfile npm
just rewrote.
- Documents the new detection/routing behavior in
`docs/development/release.md`.
## Test plan
- [ ] `npm test` / `aube test` passes, including the new
`scripts/package-manager.test.js`
coverage for `detectPackageManager` and `commandFor` on both platforms.
- [ ] `npm run lint` / `aubr lint` and `npm run check` / `aubr check`
pass.
- [ ] Manually verify `npx tauri dev` still launches correctly on a
checkout with only npm
installed, and on one where aube performed the install.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01YHkuCEJRUTBnsZaUnhfR9N
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Development, build, pre-push checks, and release tasks now use the
package manager detected for the project, supporting both npm and Aube.
* Command execution accounts for platform differences, including
Windows, and the existing lint memory limit is preserved.
* These changes apply to development and release workflows; they do not
change application features or behavior for end users.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
# PR Summary
## What this does
Adds iOS as a build target. CI (`release.yml`, `pre-release.yml`,
`ci.yml`) now builds an
**unsigned iOS-device ARM64 `.ipa`** on `macos-15` — with **no Apple
certificates,
provisioning profiles, or signing secrets** used or required anywhere —
and uploads it as a
workflow artifact (and as a release asset when publishing), exactly like
the Android APK leg.
Existing platform builds (Windows, Debian/Fedora Linux, Android, macOS
Intel/ARM64) are
untouched: **no file under `src-tauri/` was modified** — no
`Cargo.toml`, no `Cargo.lock`, no
`tauri.conf.json`, no capabilities, no Rust sources. The app was audited
first and was already
almost iOS-ready (mobile entry point, `staticlib` crate type, all Rust
deps support
`aarch64-apple-ios`, iOS icons present, frontend mobile handling in
place).
## How the unsigned build works
1. `scripts/build-ios-unsigned.sh` patches
`src-tauri/gen/apple/project.yml` (idempotently) to
set `CODE_SIGNING_ALLOWED=NO`, `CODE_SIGNING_REQUIRED=NO`,
`CODE_SIGN_IDENTITY=""`,
`CODE_SIGN_STYLE=Manual` on the iOS target, then regenerates the Xcode
project.
This is required because neither the Tauri CLI's signing defaults nor
xcodebuild runner
args reach the signing validation in `xcodebuild archive`.
2. `tauri ios build --target aarch64 --archive-only` produces the
`.xcarchive` without the
CLI's signed-export phase.
3. The script merges iOS `Info.plist` keys (camera, local-network, ATS
local-networking,
export-compliance) into the built app, packages `Payload/Aventuras.app`
into a versioned
`.ipa`, and verifies it (arm64 binary, unsigned, correct layout) before
exiting 0.
## Changes
| File | Change |
| --- | --- |
| `scripts/build-ios-unsigned.sh` | **new** — full unsigned iOS pipeline
(macOS-only) |
| `.github/workflows/build-ios.yml` | **new** — reusable CI leg
mirroring `build-android.yml` |
| `.github/workflows/bootstrap-ios.yml` | **new** — one-shot workflow
that generates the `gen/apple` Xcode scaffold and smoke-tests the
unsigned build |
| `.github/workflows/release.yml` / `pre-release.yml` / `ci.yml` |
additive `build-ios` jobs + path triggers |
| `src/lib/utils/platform.ts` | additive `isIos()` |
| `src/lib/services/updater.ts` | iOS routes through the existing mobile
update check; picks the `.ipa` asset (Android/desktop behavior
unchanged) |
| `docs/development/release.md`, `README.md`,
`docs/architecture/overview.md`, `CLAUDE.md` | iOS documentation |
| `docs/development/ios-build-target.md` | **new** — full account:
audit, decisions, every CI failure and fix, limitations, verification |
## Verification
- Local: `npm run check` (0 errors), `npm test` (1890 passed), `npm run
lint` (0 errors),
`npm run build`, `cargo check`, YAML job-graph validation of every
touched workflow,
the `project.yml` patcher tested against a template mock, and every
Tauri CLI flag
verified against the actual `cli-darwin-arm64@2.11.4` binary.
- CI: the iOS leg runs end-to-end green on `macos-15` — scaffold
generation, Rust
cross-compile, unsigned archive, `.ipa` packaging, artifact upload.
Android stayed green
throughout; desktop legs were unaffected by this work.
## Notes / limitations (also documented in
`docs/development/ios-build-target.md`)
- The `.ipa` is **unsigned by design**: stock iOS will not install it —
it targets
sideloading tools (AltStore, Sideloadly, TrollStore) or a future signing
step.
- No in-app updater on iOS (the updater plugin supports neither mobile
platform); iOS users
get the release page / `.ipa` asset in the browser via the existing
mobile check.
- `src-tauri/gen/apple` is not yet committed; `build-ios.yml` self-heals
by running
`tauri ios init` on the runner when it's absent (~1–2 min overhead). Run
the
**"Bootstrap iOS scaffold"** workflow once and commit its artifact to
remove that.
- Local `tauri ios dev` needs `--config
src-tauri/tauri.release.conf.json` (devtools plugin
is desktop-only).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added unsigned iOS app builds for testing and release distribution.
- iOS builds are now included in CI and release workflows.
- iOS release checks link to a matching IPA when available, with a
fallback to the release page.
- Added iOS platform detection.
- **Documentation**
- Added iOS download and build guidance, including Mac and Xcode
requirements.
- **Bug Fixes**
- iOS builds now fail validation if required app metadata, executable
files, or expected IPA components are missing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Node 24 migration, plus a CI/CD overhaul that puts desktop and Android
builds behind one shared,
cached, and now release-safe set of workflows — used by pull-request
validation, `master`, and
both release tracks alike.
## What changed
**Node & Actions**
- Node 24, tracked via `.nvmrc` (`package.json`'s `devEngines` enforces
it; the README points at
`.nvmrc` instead of a hardcoded version)
- GitHub Actions bumped off versions that were 1-3 majors behind,
several still on the deprecated
Node 20 runtime
**Shared, safer release workflows**
- Desktop and Android build jobs now live in reusable workflows
(`build-desktop.yml`,
`build-android.yml`), called by `master`/PR validation and both
`release.yml` and
`pre-release.yml` with a `publish`/`prerelease` switch — previously each
release workflow
duplicated its own copy of the whole build matrix
- A `create-release` job creates the release once per run, so the build
matrix's parallel legs
stop racing to create duplicate draft releases
- A release-safety guard now refuses to run when the tag's release is
already published, or when
a pushed tag doesn't match `tauri.conf.json`'s version — protects a
shipped release's assets and
`prerelease` flag from a stray `workflow_dispatch` or job re-run
- Pre-releases are now created as a draft and only published once every
platform build succeeds,
matching how stable releases already worked
- Runners are pinned (`macos-15`, `windows-2025`) instead of tracking
`-latest`, and the Linux
desktop build runs inside an `ubuntu:22.04` container, independent of
the `ubuntu-latest` host's
own lifecycle
**CI now builds and caches on `master`**
- Every platform now builds on `master` (on push, a weekly schedule, and
manual dispatch) without
publishing, so tag-triggered release builds — which can only restore
caches from the tag itself
or the default branch — no longer start cold, and every push leaves a
downloadable per-platform
build
- Rust, Gradle and npm all get real caches now; the npm cache key
ignores the version-bump commit
every release makes, so releases hit the cache too
- `lint-and-typecheck.yml` now also runs `npm test`, which CI never did
before, and installs
dependencies through the same cached action as the rest of CI
**Build changes**
- Android builds an APK only (the AAB was built and discarded every run)
and drops the 32-bit
`x86` target; it uses the runner's preinstalled NDK instead of
downloading one, and drops the
Android SDK's no-longer-published `tools` package
- A release-mode Tauri config drops the `devtools` feature from release
builds
- A CI-only build gets a SHA-suffixed version instead of silently
reusing the last release's
version
**Fixes found along the way**
- A release-name typo (`Aventura` → `Aventuras`) that `tauri-action`
would otherwise have kept
reverting
- The CI version suffix can no longer produce an invalid semver
identifier
## Docs
`docs/development/release.md` substantially rewritten to cover the
shared-workflow architecture,
the release-safety guard, the caching setup, and the build-version
scheme.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added automated desktop and Android builds for Linux, Windows, macOS,
and Android.
- Added signed artifacts, draft releases, prereleases, and
platform-specific downloads.
- Added scheduled and manually triggered builds that create downloadable
artifacts without publishing a release.
- Added macOS Apple Silicon and Linux RPM packages, plus updated Android
and desktop asset naming.
- Added safeguards to prevent duplicate uploads to published releases.
- **Documentation**
- Updated build prerequisites and documented automated linting,
type-checking, tests, and builds.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added an Android-only **Incognito Keyboard** setting in Interface
preferences.
* The setting is saved, restored, reset with other preferences, and
applied immediately.
* When enabled, Android input fields prevent personalized keyboard
learning.
* On non-Android platforms, the setting is hidden and has no effect.
* **Reliability**
* Added automated checks to verify Incognito Keyboard support is
included in Android builds.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
`npm run release` cannot push for anyone without an HTTPS credential
helper:
```
Username for 'https://github.com': …
remote: Password authentication is not supported for Git operations.
```
`scripts/release.js` pushed to a hardcoded
`https://github.com/AventurasTeam/Aventuras.git`. GitHub no longer
accepts a password there, so the push fails outright — while the
contributor's working SSH remote sits unused two lines away. (Present
since `5fd37fcb`, February; it just needed someone to cut a release from
a clone without a helper.)
### The fix
The URL was not arbitrary — releasing to `origin` would happily tag a
**fork**, and hardcoding guaranteed the canonical repository. This keeps
that guarantee and drops the transport:
`pickReleaseRemote` scans `git remote -v` for a remote whose **push URL
points at the release repository** and hands git that remote's name, so
the push uses whatever transport and credentials that remote already
has. When nothing matches it falls back to the HTTPS URL, i.e. today's
behaviour.
Two details that matter:
- **Matching is on the repository path, never the remote name**, so a
fork cannot be selected however it is called. A test pins this.
- **`unkarelian/Aventuras` is accepted alongside
`AventurasTeam/Aventuras`.** GitHub redirects the pre-rename path, so a
remote predating the move still points here — and matching on the
repository *name* alone would sweep up every fork, which is exactly what
the owner check is for.
### Notes
- Extracted into `scripts/remote.js` rather than left inline:
`release.js` cuts a release on import, so it cannot be imported by a
test. 18 cases in `scripts/remote.test.js`, following the `version.js` /
`version.test.js` split from #416.
- The console now prints `upstream (git@github.com:...)` instead of a
bare remote name, so the repository about to be released to stays
visible.
- The failure hint for an unreachable remote now names the HTTPS
fallback as a likely cause.
## Release script
- Version arithmetic extracted into `scripts/version.js`, covered by
`scripts/version.test.js`
(17 cases). `vitest.config.ts` now includes `scripts/**/*.test.js` for
it.
- **Every precondition is checked before anything is written**: a clean
tree, a version that
moves forward, and a tag/branch that exists neither locally *nor on the
remote*. A failure
after that point deletes the branch and tag it created and returns to
the original branch.
- `--dry-run` runs the checks and stops. `--no-merge-back` skips the
fast-forward of the
branch the script was run from.
- Unknown flags are rejected rather than silently ignored.
- The rollback's `reset --hard` is guarded behind a **successful
checkout**: it moves the
*current* branch, so a failed checkout dragged the release branch onto
the start commit.
- Everything after the push is explicitly best-effort and is **not**
rolled back — the tag is
out and the release is building, so a protected branch or a race there
is a reason to print
two commands, not to unpick a published release.
- Only `X.Y.Z` and `X.Y.Z-pre.N` are accepted. Other pre-release
spellings are valid semver
but match neither workflow trigger, so they would tag and build nothing.
- Works on Windows again.
Note the `--` in `npm run release -- <bump>`: without it npm consumes
the flags before the
script sees them. Documented in the README.
## eslint
`eslint-plugin-boundaries` v7 renamed `rules` to `policies` and takes
the element type inside
the `disallow` target rather than in a second outer `to`. The config was
still written for the
v6 shape — it worked, but on the deprecated path.
Verified **warning-for-warning identical** across the repo: 189
warnings, same files, same
positions, before and after.
`mode: 'folder'` dropped where it is already the default.
## Android
`src-tauri/gen/android/app/tauri.build.gradle.kts` is autogenerated by
the Tauri CLI and was
being regenerated on every build, so it showed up as a dirty file
constantly. Untracked and
gitignored.
---
Base: `upstream/master`. `check`, `lint` and the full Vitest suite pass.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved release automation with validation, dry-run support, rollback
handling, and optional branch merge-back.
* Added support for stable and prerelease version validation,
comparison, and version bumping.
* **Bug Fixes**
* Added safeguards for repository state, remote references, network
availability, and Windows npm execution.
* **Documentation**
* Expanded release instructions with supported version formats, safety
checks, rollback behavior, and usage options.
* **Tests**
* Added comprehensive coverage for version handling and release-related
scenarios.
Updates scripts/release.js to remove the --offline flag from cargo
update and automatically run npm run format and npm run lint:fix before
committing release changes.