Core and desktop comments describe the code to a stranger (#6224)
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 12s
CI & Build / Web typecheck and unit tests (push) Successful in 8s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Android / Core and FFI clippy and tests (push) Successful in 1m3s
CI & Build / integration (push) Successful in 1m37s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m14s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m3s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m42s
Android / Build the server image (push) Successful in 1s

Milestone 546 "Bring the repo to the comment standard (rule 32) and
compose defaults (rule 198)", step #6224 "Comment standard sweep: Rust
core and desktop client".

Doc comments and comments across inkwell-core, the Tauri app and
desktop/packaging now say what the code does and the non-obvious why in
the present tense. Gone: milestone tags (M10.x, M11.3, M13, M304, M315,
M314, milestone 325), issue and Scribe-note numbers, family-idea and
family-rule citations, the run numbers and commit hashes behind past
bugs, and the narration of what used to be (the stable channel before it
held its own bundles, CARGO_PKG_VERSION as the shown version, the
MANIFEST_TAG split, the store's missing tests, the old PKGBUILD). The
schema's migration notes keep what each step does and why, without
"until now". The forge is called the forge rather than Forgejo.

Hazards stay as facts: the dev-rolling tag that must not shadow the
branch, the manifest matched on this build's version, the APK's fixed
name surviving the prune, the LIKE guard in v9, -wal/-shm moving with
the renamed store, panic = abort ending the app.

34 files, comments only: a comment-stripping diff of every changed file
against HEAD is identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:30:37 -04:00
co-authored by Claude Opus 5.5
parent 0a631219ee
commit 01b53118b9
34 changed files with 300 additions and 384 deletions
@@ -6,7 +6,7 @@
# libwayland-* into usr/lib. On many end-user systems (Arch/CachyOS, NVIDIA,
# Wayland) those bundled copies clash with the running kernel driver + Mesa and
# abort with "Could not create default EGL display: EGL_BAD_PARAMETER" -> a black
# window (issue 2021). They load before any renderer choice, so the runtime env
# window. They load before any renderer choice, so the runtime env
# fallbacks (WEBKIT_DISABLE_DMABUF_RENDERER etc.) cannot rescue it. Per the
# AppImage excludelist these libraries MUST come from the host. We strip exactly
# that graphics/display subset -- keeping webkit/gtk/javascriptcore bundled for
+11 -14
View File
@@ -3,8 +3,8 @@
# Build a PREBUILT Arch package (.pkg.tar.zst) from the binary the desktop CI job
# already compiled — no makepkg, no Arch container, no second Rust build.
#
# WHY prebuilt: the source PKGBUILD this replaces required every user to install
# rust + nodejs + npm and compile for minutes before they could open the app.
# WHY prebuilt: a source PKGBUILD would make every user install rust + nodejs +
# npm and compile for minutes before they could open the app.
# Distribution means shipping a package people can just install, so the one-command
# installer (install.sh) can `pacman -U` a real native package on Arch/CachyOS
# instead of falling back to the AppImage.
@@ -14,8 +14,8 @@
# SONAME at runtime, and those sonames are identical on both distros; SQLite is
# compiled in (rusqlite "bundled"). glibc symbol versioning is forward-compatible,
# and we build on Debian's older glibc and run on Arch's newer one — the safe
# direction. Using the host's graphics stack is also exactly what keeps issue 2021
# (bundled-lib EGL_BAD_PARAMETER black window) fixed.
# direction. Using the host's graphics stack is also what avoids the bundled-lib
# EGL_BAD_PARAMETER black window.
#
# A .pkg.tar.* is just a tar whose FIRST entry is .PKGINFO, so we assemble it
# directly rather than nesting an Arch container over the docker socket.
@@ -33,10 +33,9 @@ BINARY="$REPO_ROOT/target/release/inkwell"
OUT_DIR="${1:-$REPO_ROOT/target/release/bundle/arch}"
PKGNAME="inkwell"
# The names this package used to ship under: `thoughtsync` until the app became
# Inkwell (Scribe note 5071), `thoughtsync-desktop` before that. pacman needs both
# conflict and replace to retire them: without them a `pacman -U` of the renamed
# package installs ALONGSIDE the old one (issue 2075).
# Earlier names of this package: `thoughtsync` and `thoughtsync-desktop`. pacman
# needs both conflict and replace to retire them: without them a `pacman -U`
# installs ALONGSIDE the old one.
REPLACES=(thoughtsync thoughtsync-desktop)
PKGREL=1
PKGDESC="Inkwell desktop — local-first Keep-style thought capture"
@@ -58,11 +57,9 @@ DEPENDS=(webkit2gtk-4.1 gtk3)
# Single source of truth for the version: the SAME helper the bundle build uses.
#
# It used to read tauri.conf.json directly, which was right until dev builds began
# overriding the version on the command line (M10.9) — the file still says 0.1.0, so
# the pacman package came out stamped 0.1.0 around a binary reporting 0.1.132. A
# package that lies about its version is exactly what makes a later "which build is
# this?" question unanswerable.
# Not tauri.conf.json: dev builds override the version on the command line and the
# file says 0.1.0, so reading it would stamp the package with a version its binary
# doesn't report.
# `|| true` so a miss falls through to the explicit error below rather than
# aborting on pipefail with no explanation.
# The ORDERING KEY: pacman compares this, and it must match the filename the
@@ -127,7 +124,7 @@ INSTALLED_SIZE="$(du -sb "$STAGE" | cut -f1)"
# pacman uses .MTREE for `pacman -Qkk` file verification; it is NOT required to
# install, and generating it needs bsdtar (libarchive-tools), which the Debian CI
# image may not carry. Emit it when we can, skip loudly when we can't, rather
# than adding an apt install to the job (rule 5: the image is the toolchain).
# than adding an apt install to the job (the image is the toolchain).
MTREE_ENTRY=()
if command -v bsdtar >/dev/null 2>&1; then
( cd "$STAGE" && LANG=C bsdtar -czf .MTREE --format=mtree \
+12 -14
View File
@@ -3,9 +3,8 @@
# Verify the .deb that `cargo tauri build` produced is a correct, installable
# native package — BEFORE the one-command installer starts handing it to people.
#
# WHY: install.sh prefers the .deb on every Debian/Ubuntu machine, but until now
# nothing had ever inspected it, let alone installed it. Its Depends line was
# entirely whatever tauri inferred, unread. The failure mode we're guarding is a
# WHY: install.sh prefers the .deb on every Debian/Ubuntu machine, and its Depends
# line is whatever tauri infers. The failure mode we're guarding is a
# user running install.sh and getting either an apt resolution error or an app
# that installs and then won't launch because a library it needs was never
# declared.
@@ -14,10 +13,9 @@
# 1. Print the control file + contents — the generated metadata becomes ground
# truth in the build log instead of an assumption.
# 2. Naming: the binary is /usr/bin/inkwell and the generated .desktop
# entry's StartupWMClass matches it. The app used to identify itself three
# different ways depending on install channel (issue 2075); this is what
# keeps the .deb — the only channel whose entry Tauri generates for us —
# from drifting away from the two we write by hand.
# entry's StartupWMClass matches it. This keeps the .deb — the only channel
# whose entry Tauri generates for us — from drifting away from the two we
# write by hand.
# 3. dpkg-shlibdeps: the canonical Debian answer for "what does this ELF
# actually need". Compared against what the package declares.
# 4. Every declared dependency resolves to a real package in apt (catches a
@@ -28,7 +26,7 @@
#
# Check 5 is opportunistic on purpose: the build image is not guaranteed to carry
# a docker CLI, and adding one at job time would violate "the image is the
# toolchain" (rule 5). Checks 1-4 are self-contained and always run.
# toolchain". Checks 1-4 are self-contained and always run.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -74,10 +72,10 @@ echo " (binary: ${BIN#"$WORK/root"})"
# purpose — this literal IS the contract the three install channels are held to.
#
# The control file's `Package:` field is held to it too. tauri-bundler derives it
# as kebab-case(productName) with no override, which split "ThoughtSync" into
# `thought-sync` (issue 2075). "Inkwell" has no hump to split, so the package and
# the command finally agree, and a productName that brought the split back would
# also break the `conflicts`/`replaces` retiring the old package.
# as kebab-case(productName) with no override, so a productName with a hump
# ("ThoughtSync") splits into `thought-sync`. "Inkwell" has none, so the package
# and the command agree; a productName that brought a split back would also break
# the `conflicts`/`replaces` retiring the old package.
CANON="inkwell"
package="$(dpkg-deb -f "$DEB" Package 2>/dev/null || true)"
@@ -89,8 +87,8 @@ installed_bin="${BIN#"$WORK/root"}"
note_fail "binary is at $installed_bin, expected /usr/bin/$CANON (mainBinaryName in tauri.conf.json)."
# Tauri generates this entry from the binary name, so a mismatch means the config
# and the bundler have diverged — exactly the drift that made the app group under
# a different taskbar icon depending on how it was installed.
# and the bundler have diverged — the drift that groups the app under a different
# taskbar icon depending on how it was installed.
entry="$(find "$WORK/root/usr/share/applications" -name '*.desktop' -print -quit 2>/dev/null || true)"
if [ -z "$entry" ]; then
note_fail "no .desktop entry in the package — the app would not appear in any menu."
+14 -19
View File
@@ -16,12 +16,8 @@
# stable (default) — the rolling build from every merge to `main`.
# dev — the rolling build from every green push to `dev`.
# Both are fixed-tag releases: the tag never moves and the assets are pruned to the
# current build, so the tag alone names the newest one. `stable` only became one in
# M314 step 3, when `main` started publishing — before that it was a manifest-only
# pointer at whatever `v*` tag somebody had last cut, and this script carried a
# fallback that chased the `v*` release its manifest named. That came out once
# `main` had published to `stable` for real (`b6673c6`); the two channels are the
# same shape now and nothing here should special-case one of them again.
# current build, so the tag alone names the newest one. The two channels are the
# same shape, and nothing here special-cases either.
# Pick one with `--channel dev` or `INKWELL_CHANNEL=dev`. Through a pipe the options go
# after a `--`: curl -fsSL <url> | sh -s -- --channel dev
#
@@ -36,7 +32,7 @@
# * everything else (Fedora/openSUSE/…) -> the de-bundled AppImage,
# installed user-locally (no sudo). The AppImage's graphics libs are
# stripped in CI (see debundle-graphics.sh), so it uses the host GPU stack
# and renders where a stock Tauri AppImage would black-window (issue 2021).
# and renders where a stock Tauri AppImage would black-window.
#
# POSIX sh (dash-safe) so `curl … | sh` works everywhere. Dependency-light and
# auditable on purpose — read it before you pipe it.
@@ -122,9 +118,8 @@ if [ -n "$server" ]; then
fi
# --- architecture gate ------------------------------------------------------
# Only x86_64 is built today; arm64 will be added when the CI matrix grows. The
# release-asset naming carries the arch, but since only one arch ships now we
# match by file extension below and just guard the arch here.
# Only x86_64 is built. The release-asset naming carries the arch, but with one
# arch shipping we match by file extension below and just guard the arch here.
arch="$(uname -m)"
case "$arch" in
x86_64 | amd64) : ;;
@@ -148,7 +143,7 @@ fetch() {
# The download against the digest its server published, BEFORE it reaches pacman,
# dpkg or a menu entry: a truncated file installs as something broken rather than
# failing. The forge path publishes no digest per bundle, so it passes an empty one
# and this checks nothing there; that is the forge path exactly as it always was.
# and this checks nothing there.
verify() {
[ -n "$2" ] || return 0
if have sha256sum; then
@@ -231,8 +226,8 @@ say "Finding the latest Inkwell build on the $channel channel…"
# channel — which is exactly what an installer wants and what the in-app updater
# already reads.
# The channel's RELEASE TAG, which is not always its name: `dev` publishes on the
# tag `dev-rolling`, because a tag called `dev` shadowed the branch of the same name
# and broke `git push origin dev` (Scribe #2184). Same mapping as
# tag `dev-rolling`, because a tag called `dev` would shadow the branch of the same
# name and break `git push origin dev`. Same mapping as
# packaging/channel-tag.sh — inlined because this script is fetched alone through a
# pipe and has nothing to source.
case "$channel" in
@@ -269,9 +264,9 @@ revoke_token() {
}
# Tell the app which channel it was installed from. The installer is the only thing
# that knows, and without this the app kept its own `stable` default and a dev install
# checked the stable feed — which advertises an OLDER version — reporting "up to date"
# forever (issue 2183).
# that knows; without this the app keeps its own `stable` default, and a dev install
# would check the stable feed, which advertises an OLDER version, and report "up to
# date" forever.
#
# A plain file rather than a write into the app's SQLite store: shell has no business
# knowing that schema, and a file it can't misread is the narrowest possible contract.
@@ -282,7 +277,7 @@ revoke_token() {
# both sides hardcode it, so a change to the identifier has to change both.
#
# From a server, the sibling `install-server` is written instead: which server the app
# came from, for the updater to follow (milestone 325, step 6). The channel marker is
# came from, for the updater to follow. The channel marker is
# left alone on that path, because a server's channel is whatever its image was built
# with and nothing here can know it.
record_channel() {
@@ -323,8 +318,8 @@ native_update_note() {
# --- native pacman path (Arch/CachyOS/Manjaro) ------------------------------
# Preferred over the AppImage on Arch: pacman pulls webkit2gtk-4.1 itself and the
# app then runs against the host graphics stack, which is what keeps the
# EGL_BAD_PARAMETER black window (issue 2021) from coming back. It also means the
# app then runs against the host graphics stack, which is what avoids the
# EGL_BAD_PARAMETER black window. It also means the
# app is tracked by the package manager and uninstalls cleanly.
if have pacman && [ -n "$pkg_url" ]; then
say "Arch-family system detected — installing the native pacman package"
+22 -24
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Publish the built desktop bundles as assets on a Fabled-Git (Forgejo) Release.
# Publish the built desktop bundles as assets on a Fabled-Git Release.
#
# WHY: CI Actions artifacts are ephemeral, per-run, and auth-gated — useless as a
# distribution/fetch target. The install script (install.sh) and the in-app
@@ -9,9 +9,8 @@
# and each asset has a permanent browser_download_url.
#
# WHEN: CI-only, and ONLY on a `v*` tag build (the workflow gates this step with
# `if: startsWith(github.ref, 'refs/tags/v')`). Cutting the tag is the operator's
# action (rule 2) — this script never creates a tag, it only publishes a Release
# for a tag that already exists.
# `if: startsWith(github.ref, 'refs/tags/v')`). This script never creates a tag; it
# only publishes a Release for a tag that already exists.
#
# The build + de-bundle steps run first; this consumes their output:
# target/release/bundle/appimage/*.AppImage (de-bundled)
@@ -20,7 +19,7 @@
# target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
#
# Instance-agnostic: server + repo come from the runner's github.* context
# (Forgejo populates them for compatibility), so nothing is hardcoded to one host.
# (the forge populates them for compatibility), so nothing is hardcoded to one host.
#
# Idempotent: re-running for the same tag reuses the existing Release and
# replaces same-named assets, so a re-run (or workflow_dispatch retry) is safe.
@@ -34,7 +33,7 @@ set -euo pipefail
# The release to publish to. Defaults to the pushed tag (the versioned, stable
# case). The channel publish steps call this with the channel's fixed tag —
# `stable`, or `dev-rolling` for the dev channel (packaging/channel-tag.sh) — to
# maintain a rolling release whose tag never moves, because Forgejo has no
# maintain a rolling release whose tag never moves, because the forge has no
# `/releases/latest/download/<asset>` route for an updater to point at.
RELEASE_TAG="${RELEASE_TAG:-$GITHUB_REF_NAME}"
RELEASE_PRERELEASE="${RELEASE_PRERELEASE:-false}"
@@ -57,13 +56,13 @@ shopt -s nullglob
# can see the other's bundles. The release is created once and reused (409 path).
# The `.sig` files are the updater's whole trust story — a bundle published without
# its signature is one the app will refuse, so they ship together or not at all.
# They only exist when the build ran with a signing key (M10.9); nullglob drops
# them silently otherwise, which is the correct behaviour for an unsigned build.
# They only exist when the build ran with a signing key; nullglob drops them
# silently otherwise, which is the correct behaviour for an unsigned build.
# The Android client publishes here too, from its own job and its own workspace
# — the same nullglob arrangement that already lets the Linux and Windows jobs
# share one release. Its two files are staged under android/dist by the workflow:
# a STABLY NAMED apk (a fixed name is the whole point of the fixed `dev` tag —
# Forgejo has no /releases/latest/download route) and the sidecar the server reads
# — the same nullglob arrangement that lets the Linux and Windows jobs share one
# release. Its two files are staged under android/dist by the workflow: a STABLY
# NAMED apk (a fixed name is the whole point of the fixed `dev` tag — the forge
# has no /releases/latest/download route) and the sidecar the server reads
# its version out of, because an APK keeps that in a binary manifest.
ASSETS=(
"$BUNDLE_ROOT"/appimage/*.AppImage
@@ -77,8 +76,7 @@ ASSETS=(
)
# nullglob drops PATTERNS that match nothing — it does nothing for a path with no
# wildcard in it, which stays in the array as a literal and reaches curl as a file
# that isn't there (exit 26). The Android entries above are exactly that shape, and
# adding them broke the desktop publish that had been working. Filter on existence
# that isn't there (exit 26). The Android entries above are that shape. Filter on existence
# instead, which is what the array actually means and covers every entry rather
# than only the ones that happen to contain a `*`.
present=()
@@ -120,20 +118,20 @@ first_id() { grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE
# — otherwise someone following the instructions here lands on a tagged build and
# wonders why the version they were sent isn't what they got.
#
# Both CHANNELS are rolling pointer releases (M314 step 3): `dev` republishes on
# Both CHANNELS are rolling pointer releases: `dev` republishes on
# every green push to dev, `stable` on every merge to main. Each says so, because a
# release that prunes its own assets behaves differently from a versioned one and a
# reader deserves to know which they are looking at.
# Keyed on the TAG, which for the dev channel is `dev-rolling`, not `dev` — a tag
# named `dev` shadowed the branch (Scribe #2184). CHANNEL_LABEL is the name a person
# reads in the release title, so the rename does not leak into it.
# named `dev` would shadow the branch. CHANNEL_LABEL is the name a person reads in
# the release title, so the tag name doesn't leak into it.
if [ "$TAG" = "dev-rolling" ]; then
CHANNEL_LABEL='dev (rolling)'
INSTALL_TAIL='sh -s -- --channel dev'
# Backticks BARE, not `\``. The heredoc below is unquoted, so there the backslash
# is the shell's — it suppresses command substitution and never reaches the JSON.
# Here single quotes already do that job, so a backslash would survive into the
# body as `\``, which is not a legal JSON escape: Forgejo answers 422.
# body as `\``, which is not a legal JSON escape: the forge answers 422.
CHANNEL_NOTE='\n\nThis is the rolling **dev** channel: republished on every green push to `dev`, and pruned to the current build.'
elif [ "$TAG" = "stable" ]; then
CHANNEL_LABEL='stable'
@@ -155,11 +153,11 @@ JSON
# An explicit body, replacing the install instructions above.
#
# Used by the RELEASE lane, whose job is a changelog rather than artifacts (M314
# step 7). It goes through this script rather than making its own API calls so that
# the create-or-PATCH-on-409 path is shared: a fixed-tag release that only ever
# POSTs keeps whatever text its FIRST build wrote, which is #2182 exactly, and
# re-implementing that correctly in a second place is how it comes back.
# Used by the RELEASE lane, whose job is a changelog rather than artifacts. It goes
# through this script rather than making its own API calls so that the
# create-or-PATCH-on-409 path is shared: a fixed-tag release that only ever POSTs
# keeps whatever text its FIRST build wrote, and a second copy of that path is one
# more place to get it wrong.
#
# CONTRACT: already JSON-escaped, without surrounding quotes. The caller knows
# whether it has a JSON encoder; this script cannot assume python3 is on PATH in
@@ -190,7 +188,7 @@ existing="$(api GET "$API/releases/$RELEASE_ID/assets")"
# --- upload each asset ------------------------------------------------------
for asset in "${ASSETS[@]}"; do
name="$(basename "$asset")"
# If an asset with this name already exists, delete it first (Forgejo rejects
# If an asset with this name already exists, delete it first (the forge rejects
# a duplicate name). Match the "id" that precedes this asset's "name".
old_id="$(printf '%s' "$existing" \
| grep -oE "\"id\"[[:space:]]*:[[:space:]]*[0-9]+[^}]*\"name\"[[:space:]]*:[[:space:]]*\"$name\"" \
+10 -18
View File
@@ -30,15 +30,8 @@ set -euo pipefail
# wrong build, and there is nothing downstream that could catch it.
: "${DISPLAY_VERSION:?DISPLAY_VERSION is required (the human-readable version)}"
# The manifest is published to the release that HOLDS the bundles. There is no
# second place any more.
#
# There used to be: `MANIFEST_TAG` let the manifest live on a `stable` pointer
# release while the bundles sat on a versioned `v*` one, because the app can only
# read a URL that never changes and a versioned tag is not that. M314 step 3 made
# `stable` a rolling release that holds its own bundles, exactly like `dev`, so the
# split had nothing left to bridge — and a parameter that can only ever be passed
# its own default is a branch nobody exercises and a comment that goes stale.
# The manifest is published to the release that HOLDS the bundles: both channels
# are rolling releases that hold their own bundles, so there is no second place.
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
AUTH=(-H "Authorization: token $GITHUB_TOKEN")
@@ -66,12 +59,12 @@ platform_entry() {
local target="$1" pattern="$2" bundle sig_name
# Matched on THIS build's version, not just the file extension.
#
# The rolling dev release accumulates every build's assets, and picking the first
# extension match returned the OLDEST one — so the manifest advertised the new
# version while pointing at an old binary. The client would install the older
# build, still be told the newer version was available, and update forever. The
# signature check couldn't catch it either: the old bundle's signature is
# perfectly valid FOR THE OLD BUNDLE.
# The rolling dev release accumulates every build's assets, and the first
# extension match would be the OLDEST one: the manifest would advertise the new
# version while pointing at an old binary, and the client would install the older
# build, still be told the newer one was available, and update forever. The
# signature check can't catch it: the old bundle's signature is valid FOR THE OLD
# BUNDLE.
bundle="$(printf '%s\n' "$names" | grep -F "_${APP_VERSION}_" | grep -E "$pattern" | head -1 || true)"
# A hard failure, not a skip: reaching here means this platform's build didn't
# upload, and the whole point is to never advertise a bundle that isn't there.
@@ -125,7 +118,7 @@ cat "$work/latest.json"
# the one `publish-release.sh` created or refreshed moments earlier, so it is
# guaranteed to exist by the time this runs.
# Replace rather than duplicate: Forgejo rejects a second asset with the same name,
# Replace rather than duplicate: the forge rejects a second asset with the same name,
# and these files are rewritten on every publish by design.
replace_asset() {
local path="$1" name="$2" escaped old_id
@@ -191,8 +184,7 @@ if [ "${PRUNE_OLD_ASSETS:-false}" = "true" ]; then
# The Android client is on that list for a second reason too: it is built
# by a DIFFERENT workflow with its own run number, so its version never
# matches $APP_VERSION here and a version-stamped name would be pruned on
# every desktop push regardless. That is exactly what happened on run
# 4098, which swept the APK run 4092 had just published.
# every desktop push regardless.
case "$asset_name" in
latest.json|inkwell-desktop.json|inkwell.apk|inkwell-android.json) continue ;;
*"$APP_VERSION"*) continue ;;