Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
ccf8b30
feat(apps): seamless container/service runnables
krokicki Jul 1, 2026
69dd372
feat(apps): choose which apps to add from a multi-app repo
krokicki Jul 1, 2026
6675403
fix(apps): black text and drop manifest path in app picker
krokicki Jul 1, 2026
b581b75
fix(jobs): truncate Command to one line in Execution box
krokicki Jul 2, 2026
bbd37aa
fix(jobs): show Recent output whenever stdout has content
krokicki Jul 2, 2026
9830976
fix(apps): don't orphan jobs submitted while the poll loop is stopping
krokicki Jul 2, 2026
c3eefac
docs: reword comments to describe behavior, not history
krokicki Jul 2, 2026
879e3be
feat(apps): readiness-gated auto_url + FG_SERVICE_TOKEN + service_url…
krokicki Jul 2, 2026
335add4
feat(apps): surface "Downloading container image…" during service sta…
krokicki Jul 2, 2026
1e21e12
fix(apps): pull with --disable-cache to avoid duplicate SIF storage
krokicki Jul 2, 2026
6773a06
fix(apps): keep the Apps tabs visible on the job detail page
krokicki Jul 2, 2026
b9c70f0
fix(apps): drop the redundant "Back to Jobs" button on job detail
krokicki Jul 2, 2026
2e218f4
feat(apps): create_if_missing directory params
krokicki Jul 2, 2026
43a5705
feat(files): home default + toolbar for the folder-select dialog
krokicki Jul 2, 2026
04b88b4
fix(apps): don't fail pre-submit validation for create_if_missing dirs
krokicki Jul 2, 2026
549cb2b
feat(files): tighten the folder-select dialog layout
krokicki Jul 2, 2026
38f27f3
feat(files): added path display/input box to path selection dialog
krokicki Jul 2, 2026
ca71ff1
feat(apps): app detail page, card banner with actions menu, tabbed la…
krokicki Jul 2, 2026
234b32f
feat(apps): catalog listing detail page and card banner with actions …
krokicki Jul 2, 2026
1d6a360
feat(apps): adopt shared page header on the job detail page
krokicki Jul 2, 2026
f3511f4
feat(apps): show entry points on the catalog listing detail page
krokicki Jul 2, 2026
2f98f72
feat(apps): recent jobs on app detail, link job detail back to its app
krokicki Jul 2, 2026
dd35f20
refactor(apps): drop duplicate app name from the launch form
krokicki Jul 2, 2026
754fc76
refactor: rename DataLinksActionsMenu to CardActionsMenu
krokicki Jul 2, 2026
fd85618
feat(apps): pin apps to commits and run jobs from immutable snapshots
krokicki Jul 3, 2026
c2e0b98
fix(alembic): make commit-pinning migration idempotent
krokicki Jul 3, 2026
f342d1a
fix(apps): shrink version commit-sha font to match surrounding text
krokicki Jul 3, 2026
75e725b
feat(apps): add card/table view toggle to My Apps
krokicki Jul 3, 2026
b3a2d7b
fix(apps): drop update-available badge from My Apps table mode
krokicki Jul 3, 2026
c7d8b8c
fix(apps): name Nextflow pipelines by repo only, not org/repo
krokicki Jul 3, 2026
8b0fbbd
feat(apps): add card/table view toggle to App Catalog
krokicki Jul 3, 2026
5a5b273
feat(apps): replace create_if_missing with exists on path params
krokicki Jul 3, 2026
29011c9
fix(apps): open Browse dialog in the right folder for file params
krokicki Jul 3, 2026
f5e0c75
fix(apps): preselect the current file value when reopening Browse
krokicki Jul 3, 2026
59ebdb4
feat(apps): validate file/folder type of path params
krokicki Jul 3, 2026
dcfa5f5
Use standard app icon
krokicki Jul 3, 2026
ffb56f1
feat(apps): align My Apps and App Catalog table columns
krokicki Jul 3, 2026
dbb83ae
Add app metadata to detail pages
krokicki Jul 3, 2026
f68c20d
fix(apps): tolerate serialized exists default on non-path params
krokicki Jul 3, 2026
b944dba
Polish app detail metadata labels
krokicki Jul 3, 2026
86b2884
Simplify catalog app detail metadata
krokicki Jul 3, 2026
86e4e4c
fix(apps): keep Shared on header on one line, show date without time
krokicki Jul 3, 2026
e6b39d9
Show repository names as text with tooltips
krokicki Jul 3, 2026
b4d9308
Link app manifest paths to GitHub files
krokicki Jul 3, 2026
8eec21a
Align launch page header with app detail
krokicki Jul 3, 2026
2bd04d0
Soften app card hover highlights
krokicki Jul 3, 2026
a5666df
Add unshare button to listing detail
krokicki Jul 3, 2026
97d50ae
Allow editing catalog listings after sharing
krokicki Jul 3, 2026
d4c84b3
Normalize app detail metadata typography
krokicki Jul 3, 2026
5ef9486
Align app launch and job headers
krokicki Jul 3, 2026
97548a8
Strip Fileglancer secrets from per-user worker environment
krokicki Jul 4, 2026
257ef53
Pin separate code repos at app add time, not first launch
krokicki Jul 4, 2026
0e30bca
Tokenize scheduler extra_args and allow LSF resource strings
krokicki Jul 4, 2026
b2874c4
Bind container paths from defaults and env-tab file params too
krokicki Jul 4, 2026
711522c
Give git-heavy worker actions a longer IPC timeout
krokicki Jul 4, 2026
195f4f6
Actually stop local-executor jobs on cancel
krokicki Jul 4, 2026
c21ee38
Cap inline job-file reads to avoid OOM on large logs
krokicki Jul 4, 2026
18af4ef
Preserve env_parameters when relaunching from the Jobs table
krokicki Jul 4, 2026
340eaf0
Install only the current app from the launch page
krokicki Jul 4, 2026
ac59153
Report cancel/stop failures on the job detail page
krokicki Jul 4, 2026
f829707
Warn that apps execute code as the user at add time
krokicki Jul 4, 2026
5817742
Validate manifest repo_url and require at least one runnable
krokicki Jul 4, 2026
a5ae4a3
Show unknown job status neutrally instead of "Failed"
krokicki Jul 4, 2026
d3ccadf
Refetch job logs once when the job goes terminal
krokicki Jul 4, 2026
14f63da
Confirm before deleting a job
krokicki Jul 4, 2026
acdd5ab
Confirm before unsharing a catalog listing
krokicki Jul 4, 2026
e6c2a65
Associate parameter labels with inputs; label catalog search
krokicki Jul 4, 2026
e167c4b
Small launch/add UX guards
krokicki Jul 4, 2026
c93d828
Kill the whole local-job process tree on cancel, and confirm it
krokicki Jul 4, 2026
b8247c7
Allowlist the worker environment instead of only stripping FGC_*
krokicki Jul 4, 2026
3a317cd
Preserve cluster extra args quoting
krokicki Jul 4, 2026
0394517
Show the trust notice on direct catalog "Add" too
krokicki Jul 4, 2026
d494c21
Show sharer, date, and a linked GitHub source in the add dialog
krokicki Jul 4, 2026
8fd2f50
Namespace parameter input IDs to avoid cross-namespace collisions
krokicki Jul 4, 2026
f40dafe
Treat unknown job statuses as active
krokicki Jul 4, 2026
e0b17fe
Delete job work dirs with job records
krokicki Jul 4, 2026
b8cef92
Give up polling jobs stuck in UNKNOWN after a 24h cutoff
krokicki Jul 4, 2026
65fdede
Expand Apptainer cache tilde paths
krokicki Jul 4, 2026
74d49f0
Show fetch failures for app and job files
krokicki Jul 4, 2026
1b451cb
Export job extra args at top level
krokicki Jul 4, 2026
7c7c578
Clean up jobs after submit-time failures
krokicki Jul 4, 2026
79c3025
Add happy-path coverage for job submission and the jobs API
krokicki Jul 4, 2026
0d52538
Show non-field errors in the launch form error banner
krokicki Jul 4, 2026
58af14d
Batch service-URL reads in the jobs listing into one worker action
krokicki Jul 4, 2026
db57272
Clean up small apps-review items
krokicki Jul 4, 2026
cbece10
Remove historical and outdated code comments in Apps code
krokicki Jul 4, 2026
14c1ed3
Fix Windows CI test failures
krokicki Jul 4, 2026
3d010fb
Fix Nextflow leading-dash parameter values
krokicki Jul 4, 2026
db381d6
Fix Nextflow boolean parameter emission
krokicki Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/config.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ session_cookie_secure: true
# extra_paths: # Paths appended to $PATH in every job script
# - /opt/nextflow/bin # and used when verifying tool requirements
# - /opt/pixi/bin
# worker_env_passthrough: # Extra env var names (exact) or prefixes
# - MYSITE_LICENSE_SERVER # (ending in '_') to pass to per-user workers,
# - MYSITE_ # on top of the built-in allowlist. The worker
# # env is an allowlist so no server secret leaks
# # to the user; add only non-secret vars your
# # scheduler or tools need. FGC_* is never passed.
# unknown_timeout_hours: 24 # Give up polling a job stuck in UNKNOWN after
# # this many hours and mark it FAILED (the
# # scheduler can no longer report it). 0 disables.

# Cluster configuration
# Mirrors py-cluster-api ClusterConfig - all fields are optional
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""add status_updated_at column to jobs

Revision ID: a7e2f9d31c04
Revises: f4a1d8c62e97
Create Date: 2026-07-04 00:00:00.000000

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'a7e2f9d31c04'
down_revision = 'f4a1d8c62e97'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column('jobs', sa.Column('status_updated_at', sa.DateTime(), nullable=True))


def downgrade() -> None:
op.drop_column('jobs', 'status_updated_at')
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""add commit pinning to user_apps and jobs

Revision ID: f4a1d8c62e97
Revises: c1f9a4e7b2d8
Create Date: 2026-07-02 00:00:00.000000

user_apps.commit_sha pins each app to the exact commit it was added or last
updated at; jobs run from an immutable per-SHA snapshot of that commit rather
than the mutable branch clone. code_commit_sha is the equivalent pin for a
manifest's separate code repo (repo_url), when one is declared.
jobs.commit_sha records the commit whose code the job actually executed, and
jobs.code_repo_url the repo that commit belongs to when it isn't the app repo.

All columns are nullable: legacy rows have no pin and are backfilled the next
time the app is launched or updated.
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'f4a1d8c62e97'
down_revision = 'c1f9a4e7b2d8'
branch_labels = None
depends_on = None


def _missing(table: str, column: str) -> bool:
inspector = sa.inspect(op.get_bind())
return column not in {c["name"] for c in inspector.get_columns(table)}


def upgrade() -> None:
# Guarded adds: this revision briefly existed without jobs.code_repo_url,
# so a dev database migrated during that window is stamped at this head
# while missing the column. Re-running after `alembic stamp c1f9a4e7b2d8`
# converges any such state without erroring on the columns that do exist.
if _missing('user_apps', 'commit_sha'):
op.add_column('user_apps', sa.Column('commit_sha', sa.String(), nullable=True))
if _missing('user_apps', 'code_commit_sha'):
op.add_column('user_apps', sa.Column('code_commit_sha', sa.String(), nullable=True))
if _missing('jobs', 'commit_sha'):
op.add_column('jobs', sa.Column('commit_sha', sa.String(), nullable=True))
if _missing('jobs', 'code_repo_url'):
op.add_column('jobs', sa.Column('code_repo_url', sa.String(), nullable=True))


def downgrade() -> None:
if not _missing('jobs', 'code_repo_url'):
op.drop_column('jobs', 'code_repo_url')
if not _missing('jobs', 'commit_sha'):
op.drop_column('jobs', 'commit_sha')
if not _missing('user_apps', 'code_commit_sha'):
op.drop_column('user_apps', 'code_commit_sha')
if not _missing('user_apps', 'commit_sha'):
op.drop_column('user_apps', 'commit_sha')
10 changes: 10 additions & 0 deletions fileglancer/apps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
_ensure_repo_cache,
clone_url_for_stored_app,
discover_app_manifests,
ensure_repo_snapshot,
fetch_app_manifest,
gc_repo_snapshots,
get_app_branch,
get_remote_head,
get_remote_heads,
canonical_app_url,
get_or_load_manifest,
refresh_cached_manifest,
set_worker_exec,
validate_commit_sha,
)
from fileglancer.apps.command import ( # noqa: F401
_TOOL_REGISTRY,
build_command,
build_requirements_check,
collect_creatable_dirs,
collect_path_parameters,
expand_user_path,
merge_requirements,
Expand All @@ -24,7 +30,10 @@
)
from fileglancer.apps.jobs import ( # noqa: F401
_build_container_script,
_build_service_url_publisher,
_container_bind_paths,
_container_sif_name,
_SERVICE_PORT_HELPER,
cancel_job,
start_job_monitor,
stop_job_monitor,
Expand All @@ -34,4 +43,5 @@
get_job_file_content,
get_job_file_paths,
get_service_url,
get_service_phase,
)
119 changes: 99 additions & 20 deletions fileglancer/apps/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,19 +268,26 @@ def validate_path_for_shell(path_value: str) -> str | None:
return None


def validate_path_in_filestore(path_value: str, fsps: list, check_access: bool = True) -> str | None:
def validate_path_in_filestore(path_value: str, fsps: list, check_access: bool = True,
expected_type: str | None = None) -> str | None:
"""Validate a path within an allowed file share.

Always performs syntax checks and confirms the path resolves within an
allowed file share; both checks are euid-independent. When check_access is
True, additionally verifies the path exists and is readable.

The exists/readable check reflects the *calling process's* identity, so it
is only meaningful when this runs as the target user (i.e. in the setuid
worker). Callers running on the root server must pass check_access=False —
otherwise validation reflects root's access, not the user's (false-pass on
local FS where root bypasses perms, false-reject on root-squash NFS) — and
defer the access check to the worker (see submit_job).
When expected_type is 'file' or 'directory' and the path exists, its type
must match — a folder pasted into a file parameter (or vice versa) is
rejected here rather than failing at job runtime. A missing path is not
type-checked (an exists=false output has no type yet).

The exists/readable/type checks reflect the *calling process's* identity,
so they are only meaningful when this runs as the target user (i.e. in the
setuid worker). Callers running on the root server must pass
check_access=False — otherwise validation reflects root's access, not the
user's (false-pass on local FS where root bypasses perms, false-reject on
root-squash NFS) — and defer the access check to the worker (see
submit_job).

Returns an error message string if invalid, or None if valid.
"""
Expand All @@ -304,14 +311,23 @@ def validate_path_in_filestore(path_value: str, fsps: list, check_access: bool =
if result is None:
return "Path is not within an allowed file share"

if not check_access:
return None

fsp, subpath = result

from fileglancer.filestore import Filestore
filestore = Filestore(fsp)
return filestore.validate_path(subpath)
if check_access:
from fileglancer.filestore import Filestore
filestore = Filestore(fsp)
error = filestore.validate_path(subpath)
if error:
return error

if expected_type in ("file", "directory") and os.path.exists(expanded):
is_dir = os.path.isdir(expanded)
if expected_type == "file" and is_dir:
return "Path is a folder, but a file is required"
if expected_type == "directory" and not is_dir:
return "Path is a file, but a folder is required"

return None


# --- Command Building ---
Expand Down Expand Up @@ -375,7 +391,8 @@ def _validate_parameter_value(param: AppParameter, value, session=None, username
str_val = expand_user_path(str_val, username)
if session is not None:
fsps = db.get_file_share_paths(session)
error = validate_path_in_filestore(str_val, fsps, check_access=check_access)
error = validate_path_in_filestore(str_val, fsps, check_access=check_access,
expected_type=param.type)
else:
error = validate_path_for_shell(str_val)
if error:
Expand All @@ -399,6 +416,15 @@ def _flatten_param_items(items) -> list:
return result


def _is_nextflow_run_command(command: str) -> bool:
"""Return true when the base command appears to be ``nextflow run``."""
try:
argv = shlex.split(command)
except ValueError:
argv = command.strip().split()
return len(argv) >= 2 and argv[0] == "nextflow" and argv[1] == "run"


def build_command(entry_point: AppEntryPoint, parameters: dict,
env_parameters: dict = None, session=None, username=None,
check_access: bool = True) -> str:
Expand Down Expand Up @@ -456,18 +482,38 @@ def build_command(entry_point: AppEntryPoint, parameters: dict,

# Start with the base command
parts = [entry_point.command]
nextflow_run = _is_nextflow_run_command(entry_point.command)

# Pass 1: Flagged args in declaration order
for p, value in effective:
if p.flag is None:
continue
validated = _validate_parameter_value(p, value, session=session, username=username,
check_access=check_access)
# Nextflow pipeline params (`--foo`) are parsed differently from normal
# CLI switches: a separate leading-dash value becomes another param, and
# omitting a false boolean leaves any pipeline default in effect. Apply
# the Nextflow-safe forms at command-build time too, so cached
# auto-generated manifests from older Fileglancer versions are fixed
# without requiring users to update/re-add the app.
is_nextflow_pipeline_param = nextflow_run and p.flag.startswith("--")
value_separator = "equals" if is_nextflow_pipeline_param else p.value_separator
boolean_style = "value" if is_nextflow_pipeline_param else p.boolean_style
if p.type == "boolean":
if value is True:
if boolean_style == "value":
bool_value = "true" if value is True else "false"
if value_separator == "equals":
parts.append(f"{p.flag}={bool_value}")
else:
parts.append(f"{p.flag} {bool_value}")
elif value is True:
parts.append(p.flag)
else:
parts.append(f"{p.flag} {shlex.quote(validated)}")
quoted = shlex.quote(validated)
if value_separator == "equals":
parts.append(f"{p.flag}={quoted}")
else:
parts.append(f"{p.flag} {quoted}")

# Pass 2: Positional args in declaration order
for p, value in effective:
Expand All @@ -488,12 +534,13 @@ def build_command(entry_point: AppEntryPoint, parameters: dict,


def collect_path_parameters(entry_point: AppEntryPoint, parameters: dict,
env_parameters: dict = None) -> list[tuple[str, str, str]]:
env_parameters: dict = None) -> list[tuple[AppParameter, str]]:
"""Collect effective file/directory parameter values needing path validation.

Mirrors build_command's effective-value computation (user-provided merged
with defaults, across both the env and pipeline namespaces) but returns only
file/directory parameters as (param_key, param_name, raw_value) tuples.
file/directory parameters as (param, raw_value) tuples, so callers can key
validation off the parameter's type and exists flag.

Raw (un-expanded) values are returned so that authoritative validation can
run in the setuid worker, where '~' and access checks resolve as the target
Expand All @@ -504,7 +551,7 @@ def collect_path_parameters(entry_point: AppEntryPoint, parameters: dict,
param_flat = _flatten_param_items(entry_point.parameters)
groups = ((env_flat, env_parameters), (param_flat, parameters))

result: list[tuple[str, str, str]] = []
result: list[tuple[AppParameter, str]] = []
for flat, values in groups:
for param in flat:
if param.type not in ("file", "directory"):
Expand All @@ -515,5 +562,37 @@ def collect_path_parameters(entry_point: AppEntryPoint, parameters: dict,
value = param.default
else:
continue
result.append((param.key, param.name, str(value)))
result.append((param, str(value)))
return result


def collect_creatable_dirs(entry_point: AppEntryPoint, parameters: dict,
env_parameters: dict = None) -> list[tuple[str, str]]:
"""Collect effective directory values for params with exists=false.

Mirrors collect_path_parameters' effective-value logic (user value else
default, across both the env and pipeline namespaces) but returns only
directory params whose path need not exist yet (exists=false), as
(param_name, raw_value) tuples. Raw (un-expanded) values are returned so
the setuid worker resolves '~' as the target user. See submit_job.
"""
env_parameters = env_parameters or {}
env_flat = _flatten_param_items(entry_point.env_parameters)
param_flat = _flatten_param_items(entry_point.parameters)
groups = ((env_flat, env_parameters), (param_flat, parameters))

result: list[tuple[str, str]] = []
for flat, values in groups:
for param in flat:
if param.type != "directory" or param.exists:
continue
if param.key in values:
value = values[param.key]
elif param.default is not None:
value = param.default
else:
continue
if value == "":
continue
result.append((param.name, str(value)))
return result
Loading
Loading