Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3f29059
Apollo release package: fix jbrowseOrganismList, add design spec
Aug 21, 2026
e6d0a68
Apollo spec: record faToTwoBit resolution on cedar
Aug 21, 2026
e1ee52e
Apollo spec: replace release-68 diff with live-site equivalence
Aug 21, 2026
3ac106a
Apollo: add implementation plan
Aug 21, 2026
eed177e
fix(jbrowse): repair organismList history query and Postgres key casing
Aug 22, 2026
aa8bb30
feat(apollo): add Portal module for the UniDB organism list
Aug 22, 2026
e2c564f
fix(apollo): coerce Portal flags and build numbers numerically
Aug 22, 2026
6424d56
test(apollo): make the build_number ordering assertion discriminate
Aug 22, 2026
3ec2195
feat(apollo): add Apollo module for the live prod roster
Aug 22, 2026
00acbf9
Apollo: an update must never change an organism's visibility
Aug 22, 2026
0eeeabc
test(apollo): pin the non-numeric build_number skip path
Aug 22, 2026
d2229d4
feat(apollo): add roster overlay parser and seeded overlay file
Aug 22, 2026
4df20cd
test(apollo): assert the unparseable-directory warning, rename a loos…
Aug 22, 2026
57c5413
refactor(apollo): collect Portal warnings instead of writing to stderr
Aug 22, 2026
4cd7d6d
feat(apollo): add reconciliation with add/prune/rename invariant
Aug 22, 2026
50fdc30
Apollo plan: report the redundant_overlay bucket
Aug 22, 2026
76d8803
fix(apollo): trim whitespace from directory, report a non-JSON body a…
Aug 22, 2026
3e3c4fd
fix(apollo): sharper overlay parse errors, cite both lines, hermetic …
Aug 22, 2026
a0c870c
test(apollo): cover hidden publicMode, rename dies, rename-shadowed o…
Aug 22, 2026
69db985
feat(apollo): detect taxonomic renames by assembly identity
Aug 22, 2026
d55a18a
feat(apollo): add URL absolutization with a mandatory post-condition …
Aug 22, 2026
242ee5e
fix(apollo): report unreadable indexes instead of reading them as a c…
Aug 22, 2026
cb80d44
polish(apollo): tighten base validation, a loose assertion, and excep…
Aug 22, 2026
69e4c98
Apollo spec: record the measured jbrowse script baseline, and two def…
Aug 22, 2026
8337a23
fix(jbrowse): remove the dead refSeqs cache path that killed jbrowseR…
jbrestel Aug 22, 2026
648d9bd
feat(apollo): carry the internal organism abbrev alongside the public…
jbrestel Aug 22, 2026
1b93b69
Apollo spec: detect renames from the database, not the genome
Aug 22, 2026
d026a81
Apollo: derive refSeqs.json from the .fai; jbrowseRefSeqs is orphaned
Aug 22, 2026
f80cfb0
Apollo spec: Apollo owns its fasta copy; organismSpecific needs a str…
Aug 22, 2026
5c43b5e
Merge master: refSeqs cache removal and organismSpecific integer fix
jbrestel Aug 22, 2026
b5434f5
feat(apollo): generate the per-organism Apollo package
Aug 22, 2026
5e41435
Apollo spec: record provenance and settle apollo_gene_tracks.conf
Aug 22, 2026
f8d27ef
fix(apollo): close the spec-review gaps in Generate
Aug 22, 2026
8deb571
feat(apollo): emit Apollo update, rename, prune and add commands
Aug 22, 2026
8688436
Apollo: command-file verification counts non-comment lines; flag groo…
Aug 22, 2026
49621e7
feat(apollo): render the reconciliation report
Aug 22, 2026
6742712
test(apollo): pin the organism-name quoting guards
Aug 22, 2026
eb60a59
fix(apollo): count every annotated prune, not just the unapproved ones
Aug 22, 2026
0c4a7f6
feat(apollo): add the createApolloReleasePackage CLI
Aug 22, 2026
3d4e4ff
test(apollo): cover the assembly merge-decline and positive narrowing
Aug 22, 2026
c3aeefb
fix(apollo): print collected warnings before the gates that die
Aug 22, 2026
2ec66d6
fix(apollo): refuse to --generate an empty package, even under --force
Aug 22, 2026
95f1030
docs(apollo): trim the commentary to the load-bearing why
Aug 22, 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
249 changes: 249 additions & 0 deletions Model/bin/createApolloReleasePackage
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
#!/usr/bin/perl

use strict;
use warnings;

# Added only when GUS_HOME is set: unconditional interpolation warns at COMPILE
# time, before any option is looked at, so a new user's first output would be
# Perl noise instead of the usage text.
use lib (defined $ENV{GUS_HOME} && length $ENV{GUS_HOME}
? "$ENV{GUS_HOME}/lib/perl" : ());

use File::Path qw(make_path);

# Wiring only. Every rule with a wrong answer worth catching lives in
# ApolloRelease::Cli or the module that owns it, because a Perl script cannot be
# `use`d by a test without running its main() -- the tool this replaces kept its
# thresholds and rename detection in a script nothing ever exercised.
#
# The step order below is the whole safety story: nothing is read before it can
# be validated, nothing is written before a human decision is settled.

my $CLI = 'ApiCommonModel::Model::ApolloRelease::Cli';

# Parsed before anything loads from GUS_HOME: --help and a missing option must
# not require credentials, a GUS_HOME or a database.
my $opt = eval { _requireCli(); $CLI->parseOptions(@ARGV) };
_fail($@) unless $opt;

if ($opt->{phase} eq 'help') {
print $CLI->usage();
exit 0;
}

# Reported as one clean line rather than a Perl die with file and line: these
# messages are read by a release engineer, not by whoever wrote the module.
my $status = eval { main($opt) };
_fail($@) unless defined $status;
exit $status;

sub main {
my ($opt) = @_;

# --- 2. preflight --------------------------------------------------------
#
# Run in BOTH phases: --report is the rehearsal for --generate, so a report
# that succeeds and is then followed by a --generate dying at startup wastes
# the curation-team round trip the two-phase split exists to protect.
$CLI->assertEnvironment(\%ENV, !$opt->{apollo_roster});
$CLI->assertPreviousRelease($opt->{previous_release});

my $outDir = $CLI->outputDir($opt);

my %generateOpts = (
outDir => "$outDir",
base => $opt->{base_url},
project => $opt->{project},
build => $opt->{build},
wsDir => $opt->{ws_dir},
gusHome => $ENV{GUS_HOME},
);
$CLI->assertGenerateConfig(\%generateOpts);

_generate()->assertToolsAvailable();

# --- 3. inputs, all read-only -------------------------------------------
my $portal = _portal()->loadFromCommand($opt->{project});

# BEFORE assertPortalSane, deliberately. Portal.pm collects its warnings
# rather than writing to stderr, so surfacing them is this script's job -- and
# held until after the sanity gates they were discarded on exactly the runs
# that needed them. A gate that dies must not swallow the evidence for its
# own message.
_printWarnings('portal', [_portal()->warnings()]);

$CLI->assertPortalSane($portal);

my ($live, $apolloSource) = _loadApollo($opt);
$CLI->assertApolloSane($live, $apolloSource);

my $overlayPath = "$ENV{GUS_HOME}/data/ApiCommonModel/Model/apollo/roster-overlay.txt";
my $overlay = _overlay()->parseFile($overlayPath);

# --- 5. renames ----------------------------------------------------------
my $resolved = $CLI->resolveRenames($portal, $live, {
previous_release => $opt->{previous_release},
ws_dir => $opt->{ws_dir},
project => $opt->{project},
build => $opt->{build},
});

# Before the gate below for the same reason: a declined merge explains why an
# organism became a prune candidate, and so why a bucket came up empty.
_printWarnings('renames', $resolved->{warnings});
_printRenameProvenance($resolved);

# --- 6. reconcile --------------------------------------------------------
my $result = _reconcile()->reconcile($portal, $live, $overlay, $resolved->{renames});
$CLI->assertUpdateBucketSane($result, $opt->{force});

print "Apollo roster read from $apolloSource\n";
print "roster overlay read from $overlayPath\n\n";

my $report = _report()->render($result, {build => $opt->{build},
environment => $opt->{environment}});
print $report;

# After the report and independent of the decision gate: an approved annotated
# prune raises no pending decision, so it is the case that runs unread.
my $atStake = $CLI->annotatedPruneWarning($result);
print "\nAT STAKE: $atStake" if $atStake;

# --- --report stops here, having changed nothing -------------------------
if ($opt->{phase} eq 'report') {
print "\n--report changed nothing. Re-run with --generate to build the package\n"
. "into $outDir once the decisions above are recorded in the roster overlay.\n";
return 0;
}

# --- 7. the gate ---------------------------------------------------------
$CLI->assertGenerationAllowed($result, $opt->{force});

# --- 8. generate ---------------------------------------------------------
my $roster = $CLI->generationRoster($result, $opt->{organisms});

# Before make_path below, deliberately: a refused run must not leave a
# half-created release directory for the next person to find.
$CLI->assertRosterNonEmpty($roster);

if (@{$opt->{organisms}}) {
print "\nPARTIAL PACKAGE: --organism narrowed generation to "
. scalar(@$roster) . " organism(s).\n"
. "The report above covers the whole roster; the command files below cover\n"
. "only what was generated.\n";
}

my $commandDir = "$outDir/updateCommands";
make_path("$outDir/data", "$outDir/twoBit", $commandDir);

print "\ngenerating " . scalar(@$roster) . " organism(s) into $outDir\n";

my $generated = _generate()->generateAll($roster, sub {
my ($organism) = @_;
return _generate()->generateOrganism($organism, \%generateOpts);
});

_printWarnings('generate', [_generate()->warnings()]);

# From the NARROWED result so the commands describe what was actually built:
# full-roster commands beside a partial package would repoint an Apollo
# organism at a directory nobody generated.
my $written = _commands()->writeCommandFiles(
$CLI->narrowResult($result, $opt->{organisms}), $commandDir,
build => $opt->{build});

_write("$outDir/report.txt", $report);
_write("$outDir/report.tsv", _report()->renderTsv($result));

printf("\n%d succeeded, %d failed\n",
scalar @{$generated->{succeeded}}, scalar @{$generated->{failed}});
print " $written->{curl}\n $written->{groovy}\n";
print " $outDir/report.txt\n $outDir/report.tsv\n";

return 0 unless @{$generated->{failed}};

# Printed in full: the run is hours long and the organisms are independent, so
# failures accumulate rather than stopping it. Exiting zero here is how a
# package ships with holes in it.
print STDERR "\nFAILED organisms:\n";
print STDERR " $_: $generated->{errors}{$_}\n" for @{$generated->{failed}};

return 1;
}

# --- helpers ---

sub _loadApollo {
my ($opt) = @_;

# A saved findAllOrganisms response goes through the same normalise() seam, so
# an offline rehearsal exercises every rule the live path does but the fetch.
return (_apollo()->loadFromFile($opt->{apollo_roster}), $opt->{apollo_roster})
if $opt->{apollo_roster};

# Labelled from Apollo::apiUrl, never a second copy of the same default: the
# report must name the host actually read.
return (_apollo()->loadFromApi(), _apollo()->apiUrl());
}

sub _printWarnings {
my ($label, $warnings) = @_;

return unless $warnings && @$warnings;

print "\n" . scalar(@$warnings) . " $label warning(s):\n";
print " $_\n" for @$warnings;
print "\n";
}

# A rename is the one action that touches curated annotations, so say which
# mechanism decided it: "the database says so" and "the sequences match" carry
# very different weight.
sub _printRenameProvenance {
my ($resolved) = @_;

my @from = sort keys %{$resolved->{renames}};
return unless @from || @{$resolved->{unresolved}};

print "\nrename resolution\n";
printf(" %-24s -> %-24s (%s)\n",
$_, $resolved->{renames}{$_}, $resolved->{mechanism}{$_}) for @from;
print " unresolved orphan(s), left as prune candidates: "
. join(', ', @{$resolved->{unresolved}}) . "\n"
if @{$resolved->{unresolved}};
print "\n";
}

sub _write {
my ($path, $content) = @_;
open(my $fh, '>:raw', $path) or die "Cannot write $path: $!\n";
print $fh $content;
close $fh or die "Cannot close $path: $!\n";
return 1;
}

sub _fail {
my ($error) = @_;
chomp(my $message = $error || 'failed for no stated reason');
print STDERR "createApolloReleasePackage: $message\n";
exit 2;
}

# Lazy and by name so --help and an option error never touch GUS_HOME. Each
# wrapper returns the class name, so callers read as normal method calls.
sub _requireCli { _load('Cli') }
sub _portal { _load('Portal') }
sub _apollo { _load('Apollo') }
sub _overlay { _load('Overlay') }
sub _reconcile { _load('Reconcile') }
sub _report { _load('Report') }
sub _generate { _load('Generate') }
sub _commands { _load('Commands') }

sub _load {
my ($name) = @_;
my $class = "ApiCommonModel::Model::ApolloRelease::$name";
eval "require $class; 1" or die $@;
return $class;
}

11 changes: 6 additions & 5 deletions Model/bin/jbrowseOrganismList
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ use Data::Dumper;

my ($projectName) = @ARGV;

my $jbrowseUtil = ApiCommonModel::Model::JBrowseUtil->new({projectName => $projectName, organismAbbrev => $organismAbbrev});
my $jbrowseUtil = ApiCommonModel::Model::JBrowseUtil->new({projectName => $projectName});
my $dbh = $jbrowseUtil->getDbh();

my $sql = "select distinct o.public_abbrev as organism_abbrev
, o.abbrev as internal_abbrev
, o.name_for_filenames
, o.strain_abbrev
, o.IS_REFERENCE_STRAIN
Expand All @@ -33,7 +34,7 @@ my $historySql = "select h.build_number, o.public_abbrev, h.genome_source, h.gen
where h.dataset_presenter_id = dd.dataset_presenter_id
and dd.name like '%primary_genome_RSRC'
and h.annotation_version is not null
and o.taxon_id = nt.taxon_id";
and dd.taxon_id = o.taxon_id";


my $sh = $dbh->prepare($sql);
Expand All @@ -60,11 +61,11 @@ print encode_json($result);

sub addHistoryToOrganism {
my ($h, $orgs) = @_;
my $publicAbbrev = $h->{PUBLIC_ABBREV};
my $publicAbbrev = $h->{public_abbrev};
foreach(@$orgs) {

if($_->{ORGANISM_ABBREV} eq $publicAbbrev) {
push @{$_->{HISTORY}}, $h;
if($_->{organism_abbrev} eq $publicAbbrev) {
push @{$_->{history}}, $h;
return;
}
}
Expand Down
43 changes: 43 additions & 0 deletions Model/data/apollo/roster-overlay.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Roster overlay for the Apollo release package.
#
# The roster is seeded from LIVE PROD APOLLO. This file records the human
# decisions layered on top of it. Every line needs a reason: this file is the
# only written record of why an organism is or is not in Apollo.
#
# add <organismAbbrev> # who approved, when, why
# remove <organismAbbrev> # who approved, when, why

# --- Host genomes -------------------------------------------------------
# Apollo curates pathogens. The previous script excluded these implicitly,
# via an @databases array that omitted HostDB and SchistoDB. On the UniDB
# portal that filter does not exist, so the exclusion has to be stated.
#
# All 11 seeded 2026-08-21 to reproduce b68 behaviour; each line names its
# organism rather than repeating that provenance.

remove hsapREF # host genome: Homo sapiens
remove mmusC57BL6J # host genome: Mus musculus C57BL/6J
remove rnorBNNHsdMcwi # host genome: Rattus norvegicus BN/NHsdMcwi
remove btauHereford # host genome: Bos taurus Hereford
remove clupfamiliarisSID07034 # host genome: Canis lupus familiaris
remove ggalbGalGal1 # host genome: Gallus gallus bGalGal1
remove cpor2N # host genome: Cavia porcellus 2N
remove mfasREF # host genome: Macaca fascicularis
remove mmulAG07107 # host genome: Macaca mulatta AG07107
remove mmyomMyoMyo1 # host genome: Myotis myotis mMyoMyo1
remove dmeliso-1 # host genome: Drosophila melanogaster iso-1

# --- Model fungi --------------------------------------------------------
# In FungiDB, qualify on the criteria, never been in Apollo. Seeded to match
# existing behaviour, but nobody has consciously decided this -- raise with the
# curation team.

remove scerS288C # model organism, never in Apollo; decision unconfirmed
remove spom972h # model organism, never in Apollo; decision unconfirmed

# --- Deliberately NOT listed here ---------------------------------------
# hcapNAm1 (Histoplasma mississippiense NAm1) is a FungiDB pathogen that
# qualifies and is absent from Apollo. It is left in the add-candidate bucket
# for the curation team rather than suppressed here. Model/t/overlay.t pins
# that it appears in neither direction, so adding it here without revisiting
# this comment fails the build.
Loading