Skip to content

Apps fixes#400

Merged
krokicki merged 18 commits into
mainfrom
apps-fixes
Jun 30, 2026
Merged

Apps fixes#400
krokicki merged 18 commits into
mainfrom
apps-fixes

Conversation

@krokicki

@krokicki krokicki commented Jun 27, 2026

Copy link
Copy Markdown
Member

A batch of fixes and refinements to the Apps feature, covering private-repo support over SSH, consistent app naming, accurate error messages, pinned/canonical GitHub URLs that fix the false "not in your library" state, and assorted UI polish.

Changes

Private repository support (SSH)

  • Accept SSH GitHub URLs (git@github.com:owner/repo.git and ssh://git@github.com/owner/repo) in addition to HTTPS, in both the backend parser and the frontend.
  • When an HTTPS clone fails for auth/access reasons (private or nonexistent repos look identical to an unauthenticated client), automatically retry over SSH as the current user.
  • SSH is run with BatchMode=yes and StrictHostKeyChecking=accept-new so the worker never hangs on an interactive prompt.
  • The Add App dialog documents that private repos are accessed over SSH using the server's configured SSH key.

Accurate clone errors and tag-safe pulls

  • Distinguish "revision not found" (remote reachable, branch/tag mistyped) from "can't access repository" and surface the right message for each, instead of burying both inside a generic "Failed to clone or scan repo" wrapper. The endpoint maps the worker's generic 500 to a 400 for these repo problems while preserving other codes (e.g. 503 worker-dead).
  • Pulls now fetch the ref and reset --hard FETCH_HEAD instead of origin/<branch>, so updating an app pinned to a tag or commit (not a branch) works correctly. The fetch carries the SSH env so private-repo origins don't prompt.

Canonical, pinned GitHub URLs

  • New canonical_github_url (backend) / canonicalGithubUrl (frontend) that fold URLs to a single canonical https form — stripping .git, trailing slashes, and a redundant /tree/main, and converting SSH to https.
  • App, listing, and job URLs are canonicalized on write and on lookup, so the same repo always resolves to one stored representation.
  • The launch page now matches the user's library by canonical URL identity rather than exact string, fixing apps (e.g. ones added from the catalog) wrongly showing as "not in your library."
  • The revision is resolved once, at add time, in the per-user worker (which holds the user's SSH credentials — the shared server process cannot resolve a private repo's default branch) and baked into the stored URL: a repo whose default is e.g. master is stored as .../tree/master, so it dedups against an explicit /tree/master and the URL always names the revision actually cloned. This closes the bare-vs-default-branch dedup hole where the same app could be added twice.
  • The branch column records the user's requested revision ("" = took the default at add time) rather than the resolved one; the Revision shown in the dialogs is parsed from the URL. Once added, a revision is fixed — update just re-pulls the stored URL and refreshes the manifest without re-resolving or moving the row. To follow a changed default branch, delete and re-add the app.
  • Legacy apps migrated from user_preferences have a NULL branch (their default was never recorded). These are left to track the current default (stored URL returned unchanged, git resolves the default) rather than being wrongly pinned to main; they get pinned the next time the app is re-added.

App naming

  • Pixi apps are now named from the pixi.toml project name (falling back to the git repo name, then the directory) instead of repo/branch, which produced poor names like SmartSPIMGlancer/HEAD since a detached-HEAD clone's leaf directory is the revision.
  • Jobs and the launch page prefer the user's saved/custom app name (e.g. one chosen when adding from the catalog) over the raw manifest name. The internal job work_dir still uses the manifest name as a stable path identifier.
  • Adding an app from the catalog now preserves the publisher's custom name/description rather than overwriting them with the manifest's values.

UI refinements

  • Renamed the user-facing "Branch" label to "Revision" (with a "Tag or branch name" helper) to reflect that tags and commits are valid.
  • Job Execution box: show the app and entry point together (<app name> — <entry point>), add a separate repository link labeled owner/repo with (branch) appended only for non-default branches, and use a 40/60 (Status/Execution) card layout.
  • Show the user's custom app name on the launch and job pages.
  • Show an empty-state message ("There are no parameters to set for this app.") when an app has no parameters.
  • Add a share/unshare button to My Apps cards; when not shared, it opens the info dialog straight into the share form.
  • Add a confirmation dialog when canceling/stopping a job from the jobs list, matching the job detail page (shared CancelJobDialog component).

Tests

  • Backend: tests/test_apps.py, tests/test_apps_endpoints.py, tests/test_catalog_endpoints.py, and tests/test_url_migration.py.
  • Frontend: frontend/src/__tests__/unitTests/appUrls.test.ts.

Migration note

Running this branch applies two migrations:

  • b8e4f1a92c37 canonicalizes existing app/listing/job GitHub URLs in place, dropping rows that would collide with an already-canonical row under the UNIQUE(owner, url, manifest_path) constraint.
  • c1f9a4e7b2d8 bakes the resolved revision (from the old branch column) into existing URLs and derives the requested revision from the URL shape, de-duplicating any rows that collapse together. NULL-branch legacy rows are left untouched (and registered as collision winners so known-app rewrites that fold onto them are dropped rather than violating the unique constraint).

Both downgrades are no-ops since canonicalization is lossy.

@StephanPreibisch @JaneliaSciComp/fileglancer

krokicki and others added 18 commits June 26, 2026 15:04
Adding a shared catalog listing to "my apps" discarded the publisher's
custom name/description and used the freshly-fetched manifest's values
instead. The add_from_catalog endpoint already loaded the listing, so
capture and persist its name/description while still fetching the
manifest for current capabilities/branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a share/unshare icon button to each My Apps card, between the info
and trash buttons, styled like the catalog's ghost IconButton. When the
app is shared it unshares directly; when not shared it opens the app
info dialog straight into the share form (via a new startInShareView
prop) so the name/description can still be customized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The name a user customizes when adding an app from the catalog (or
sharing) now appears instead of the raw manifest name. submit_job
labels jobs with the user's saved app name (falling back to the
manifest name), so the jobs list and job detail pages reflect it, and
the launch page/form prefer the installed app's name. The internal job
work_dir still uses the manifest name as a stable path identifier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The launch form's Parameters tab now displays "There are no parameters
to set for this app." when the entry point defines no parameters,
instead of rendering an empty panel. The all-hidden case is unchanged
(handled by the existing "Show hidden" toggle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The job detail Execution card now labels "App" as "<app name> — <entry
point>" and moves the GitHub repository to its own "Repository" row
that links to and displays the repo URL (hidden when the URL isn't
parseable). Replaces the previous setup where the app name linked to
the repo and the entry point had a separate row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show the repository link as "owner/repo" with "(branch)" appended only
for non-default branches, instead of the full URL. Lay out the Status
and Execution cards as 40%/60% (a 5-column grid spanning 2 and 3) on
md+ screens; InfoCard now forwards an optional className.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relabel the branch field/rows as "Revision" in the add app dialog (with
helper text "Tag or branch name") and the app/listing info dialogs. Code
identifiers and the manifest's branch field are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adding an app from a private GitHub repo failed because the clone ran
over HTTPS with prompts disabled. Now the add dialog accepts both HTTPS
and SSH (git@github.com / ssh://) URLs, and the backend clone/ls-remote
tries HTTPS first, then retries over SSH as the user on an auth/access
error — using BatchMode + accept-new so it never hangs. When both
transports fail, surface a clear message naming the repo/revision and
the SSH-key requirement instead of a nested "500: Git command failed".

- manifest.py: _parse_github_url accepts SSH forms; _clone_repo and
  _resolve_default_branch do HTTPS->SSH fallback; _run_git gains
  extra_env; auth-error detection + user-facing error helpers.
- server.py: add_user_app re-raises the worker's HTTPException so its
  clean message isn't double-wrapped.
- frontend: parseGithubUrl accepts SSH; new isGithubRepoUrl/buildAppUrl;
  AddAppDialog validation/help text updated for HTTPS or SSH.
- tests: backend SSH parsing + clone fallback; frontend parser helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pixi adapter named apps "<repo>/<branch>", and git rev-parse on a
detached-HEAD clone returns "HEAD" — producing names like
"SmartSPIMGlancer/HEAD". Use the pixi project's declared name first,
falling back to the git repo name, then the directory name. The app's
branch/revision is still tracked separately on the UserApp record.

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

- A mistyped tag produced a misleading "can't access / maybe private"
  message (HTTPS auth-fails, SSH reaches the repo and reveals the real
  "ref not found"). _clone_repo now detects ref-not-found at both the
  HTTPS and SSH stages and reports the revision plainly, and add_user_app
  surfaces it as 400 instead of a scary 500.
- Pulling a cached tag/SHA failed with "ambiguous argument
  'origin/<ref>'" because origin/<ref> only exists for branches. The
  update path now resets to FETCH_HEAD (valid for branches, tags and
  SHAs) and passes the SSH env so SSH-origin private repos don't prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The launch page decided "installed" by exact string match against a URL
rebuilt into canonical form, so any stored app whose URL carried a
".git" suffix, trailing slash, or "/tree/main" wrongly showed as not in
the library — common for shared apps, whose URL is whatever the sharer
entered.

Fix it at the root by giving every GitHub URL one canonical form:
- new fileglancer/giturls.py (standalone, no app deps) with
  canonical_github_url, mirroring the frontend helper
- database.py canonicalizes on write (upsert_user_app, create_app_listing,
  create_job) and on lookup (get_user_app, delete_user_app,
  get_app_listing_for_app), so any cosmetic variant resolves to one row
  (also fixes the submit_job custom-name lookup)
- migration b8e4f1a92c37 rewrites existing user_apps/app_listings/jobs
  URLs, deduping rows that collapse under the unique constraint
- frontend AppLaunch matches installed apps via canonicalGithubUrl

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
os.geteuid() doesn't exist on Windows, breaking test_pull_resets_to_fetch_head_not_origin_branch. Fall back to 'n/a' when unavailable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a confirmation dialog when canceling/stopping a job from the jobs
list page, matching the existing dialog on the job detail page. Extract
the shared dialog into a CancelJobDialog component used by both pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the revision once when an app is added and bake it into the
stored canonical URL: a repo whose default is e.g. "master" is stored
as ".../tree/master" rather than a bare URL, so it dedups against an
explicit "/tree/master" and the URL always names the revision actually
cloned. This closes the bare-vs-default-branch dedup hole where the same
app could be added twice.

The branch column now records the user's requested revision ("" = took
the default at add time) instead of the resolved one, and the Revision
shown in the app/listing dialogs is parsed from the URL.

The revision is fixed at add time: update just re-pulls the stored URL
and refreshes the manifest, without re-resolving the default branch or
moving the row to a new URL. To follow a changed default branch, delete
and re-add the app.

Includes a migration that bakes the resolved revision (from the old
branch column) into existing URLs and derives the requested revision
from the URL shape, de-duplicating any rows that collapse together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
add_user_app resolved the default branch in the shared server process,
which has no access to a user's SSH key — so for a private repo with a
non-main default the resolution failed and fell back to "main", storing
a bare URL instead of e.g. ".../tree/master" and defeating the dedup and
pin-at-add-time guarantees.

Resolve the branch in the per-user worker (which holds the user's SSH
credentials) and return it from discover_manifests, then build the
canonical URL from that worker-resolved branch via the new pure
canonical_app_url helper. The server no longer makes a network call to
resolve branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Treat stored bare app URLs as the fixed main revision for clone/fetch/update paths so they do not follow later default-branch moves. Preserve the folded storage URL for dedupe/UI compatibility while using explicit operational URLs.

Co-authored-by: Codex <codex@openai.com>
A bare stored URL whose branch column is NULL is a legacy app migrated
from user_preferences, whose default branch was never recorded. Treating
it as the fixed "main" revision (as the prior pass did) breaks apps whose
repo defaults to e.g. "master", since "main" may not exist.

clone_url_for_stored_app now takes the row's branch: NULL means "unknown
default", so the stored URL is returned unchanged and git resolves the
current default (the row's historical behavior); "" or an explicit name
means pinned, so the revision is made explicit. The migration leaves
NULL-branch rows untouched instead of rewriting them to main. Such rows
get pinned the next time the app is re-added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register unchanged null-branch legacy rows as collision winners before rewriting known app URLs, so rows that bake to the same URL are dropped instead of violating the unique constraint.

Co-authored-by: Codex <codex@openai.com>
@krokicki krokicki merged commit d03fd58 into main Jun 30, 2026
5 checks passed
@krokicki krokicki deleted the apps-fixes branch June 30, 2026 18:31
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.

1 participant