Koopa r release
koopa R package release — cloud-native S3 + CloudFront, acidgenomics profile, koopa app r publish/reindex/archive/clean-orphan-binaries/publish-docs, sonoma-arm64 binaries, pre-release AcidDevTools::check() gate, pkgdown NEWS.md/reference-index silent-failure traps, CloudFront wildcard invalidation false-"Completed" gotcha. Use when releasing, publishing, reindexing, or archiving an Acid Genomics R package, cleaning up orphaned binaries or retired doc prefixes, when a pkgdown site's Changelog renders empty, or when a CDN edge serves stale content after invalidation.From its SKILL.md
npx -y skills add acidgenomics/koopa --skill koopa-r-releaseAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 11 stars11 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
27.6 KB, as published. Nobody here has run it
koopa R Package Release
Architecture: Cloud-Native S3
R packages are hosted at https://r.acidgenomics.com. S3 is the source of truth —
no local git repo or drat checkout required. Tarballs are uploaded directly; PACKAGES
manifests are regenerated by streaming only the DESCRIPTION file from each tarball
(ETag == MD5 for single-part uploads).
- S3 bucket: see
_BUCKETinlang/python/src/koopa/cran.py - CloudFront distribution: set via
AWS_CLOUDFRONT_DISTRIBUTION_ID_Rin<koopa-root>/.env(required; raises if unset) - AWS profile:
acidgenomics - CloudFront ID env var:
AWS_CLOUDFRONT_DISTRIBUTION_ID_R(required); falls back toAWS_CLOUDFRONT_DISTRIBUTION_ID— both loaded from<koopa-root>/.envif not already in the environment - Implementation:
lang/python/src/koopa/cran.py(mirrorspypi.py)
S3 layout
src/contrib/
<Pkg>_<ver>.tar.gz # current source tarballs (one per package)
PACKAGES / PACKAGES.gz / PACKAGES.rds
Archive/<Pkg>/<Pkg>_<old>.tar.gz # superseded versions
bin/macosx/sonoma-arm64/contrib/4.6/
<Pkg>_<ver>.tgz # current arm64 binaries (one per package)
PACKAGES / PACKAGES.gz / PACKAGES.rds
bin/macosx/big-sur-arm64/contrib/4.{3,4,5,6}/
PACKAGES # empty skeleton — suppresses R client warnings
# (no binaries; R falls back to source silently)
bin/windows/contrib/<Rver>/
PACKAGES # empty skeleton
Only sonoma-arm64/contrib/4.6/ carries real binaries. All other binary paths hold
empty PACKAGES skeletons. x86_64 and big-sur-x86_64 paths are not served at all
(deleted from S3).
Commands
koopa app r publish <package-dir>
Build source + macOS arm64 binary, upload both to S3, regenerate PACKAGES manifests
for src/contrib and sonoma-arm64/contrib/4.6, invalidate CloudFront, then create
an annotated git tag v<Version> and push it to origin. Tag message format:
<Package> v<Version> (<Date>) — sourced from DESCRIPTION (Date: falls back to
today). Tag step is a no-op when the package dir is not a git repo, and skipped with
a notice when the tag already exists locally (the push still runs to recover a prior
partial failure).
koopa app r publish ~/git/personal/r-syntactic # with R CMD check
koopa app r publish ~/git/personal/r-syntactic --no-check # skip check (faster)
koopa app r publish ~/git/personal/r-syntactic --no-deploy # build only, no upload
koopa app r publish ~/git/personal/r-syntactic --no-tag # skip git tag + push
koopa app r publish-from-github <repo>
Clone a GitHub repo into a temp dir and publish it. Used for packages not checked
out locally. Default org: acidgenomics.
koopa app r publish-from-github r-acidcli
koopa app r publish-from-github r-acidcli --org acidgenomics --no-check
Dep install: missing Imports/Depends are auto-installed into the system
R library before building the binary. BiocManager repos are included when available.
Note: pointillism 0.8.0 has been updated for Seurat 5 compatibility and
the binary build now succeeds.
koopa app r reindex [--no-invalidate]
Regenerate all PACKAGES manifests from current S3 contents. Discovers active binary
prefixes automatically. Writes empty skeleton PACKAGES for _SKELETON_BINARY_PREFIXES
(suppress R client warnings on big-sur-arm64 paths). Also regenerates and uploads the
landing page at r.acidgenomics.com/index.html (see below); koopa app r publish
does the same as part of its src/contrib reindex step.
koopa app r reindex # reindex everything + invalidate CloudFront
koopa app r reindex --no-invalidate
Wildcard invalidation can report Completed while an edge still serves stale
content. reindex()'s own invalidation uses glob paths
(/bin/macosx/sonoma-arm64/contrib/*/PACKAGES*). Confirmed live: after
deleting a stale binary and re-running reindex, aws cloudfront get-invalidation returned Completed within seconds, but the PACKAGES file
served by the edge still had the old ETag and a climbing Age header for
several minutes afterward. Don't trust the invalidation status alone —
compare ETag between the CDN response and aws s3api head-object on the
same key. If they still differ, issue a second invalidation with the exact
path (no glob) for that key; that resolved it immediately when the wildcard
invalidation didn't.
Landing page
s3://<r-bucket>/index.html is generated, not hand-authored — _generate_landing()
in cran.py builds it from the Package/Description fields already parsed while
reindexing src/contrib (no extra S3 calls). It shares its HTML skeleton (head, nav,
breadcrumb, Google site search, footer) with the Python landing page via
render_landing() in landing.py.
Packages are grouped by the _CATEGORIES list in cran.py — a list[tuple[str, list[str]]] (order matters), not a dict. Adding a new package that should appear
in a specific section requires adding its lowercased name to that section's list.
A package present in src/contrib but absent from every category still appears —
under an "Infrastructure" fallback section — with a warning printed to stderr, so a
new package can never silently vanish from the page; it just lands somewhere less
specific until categorized.
Docs hrefs on the page use _DOCS_PATH ("{name}/", matching publish-docs's
layout above). Footer license is Apache 2.0, matching every package's DESCRIPTION.
koopa app r deploy [--no-invalidate]
Alias for reindex — kept for CLI symmetry and AcidDevTools compatibility.
koopa app r archive [--no-invalidate]
Reconciles superseded versions across both src/contrib/ and every active
sonoma-arm64 binary prefix. Source: superseded tarballs are S3-mv'd from
src/contrib/ to src/contrib/Archive/<Pkg>/. Binaries: superseded .tgz
files are S3-rm'd outright, not archived, since binary contrib dirs mirror
CRAN convention (current version only, no Archive/) and nothing consumes an
archived binary. Regenerates manifests for whichever side actually changed.
koopa app r archive
publish() also calls the binary-pruning step (_delete_superseded_binaries())
directly for the package it just published, so a normal release never leaves a
stale .tgz behind. Order matters there: the delete runs after the new
binary uploads successfully, because the R bucket has S3 versioning disabled,
so a failed upload must leave the old binary in place rather than lose both.
archive_src() (the function behind this command) is the batch/backfill path
for whatever accumulated before this existed, or from a --no-deploy history.
Confirmed live before the fix: AcidDevTools_0.7.10.tgz sat next to 0.7.11.tgz
until manually deleted, and the binary PACKAGES manifest carried two entries
each for pipette and Cellosaurus at once.
koopa app r clean-orphan-binaries [--no-invalidate]
Deletes a binary .tgz only when its package name is entirely absent from
src/contrib (i.e. the source package was fully retired). It does not
catch a superseded version of a still-published package: that's archive's
job (above). clean_orphan_binaries()'s docstring excludes this case
explicitly.
koopa app r publish-docs <package-dir> [--no-invalidate]
Build a package's pkgdown site and sync it to s3://<r-bucket>/<name>/, served at
https://r.acidgenomics.com/<name>/. <name> is the lowercased Package field from
DESCRIPTION. src/contrib/ and bin/ are never touched — the sync is scoped to the
package's own prefix. Rejects a package name colliding with a reserved top-level
prefix (bin, css, extdata, images, packages, src, testdata).
koopa app r publish-docs ~/git/personal/r-syntactic
Implementation: publish_docs() in cran.py, mirroring pypi.py's publish_docs().
Legacy path: docs used to live at https://r.acidgenomics.com/packages/<name>/.
That prefix collided with nothing on the artifact side (R tarballs live under
src/contrib/ and bin/, unlike Python's /packages/ which holds wheels), so the
/packages/ docs prefix was purely historical. It's now /<name>/, matching
python.acidgenomics.com's docs layout. An S3 website RoutingRule on the R bucket
301-redirects any /packages/<name>/... request to /<name>/... — set HostName to
r.acidgenomics.com and Protocol to https explicitly; an explicit RoutingRules
redirect defaults to the bucket's own S3-website hostname over plain HTTP if you omit
them, unlike S3's automatic trailing-slash redirect which stays host-relative.
Superseded: AcidDevTools::pkgdownDeployToAws() (in r-aciddevtools) is
.Deprecated() in favor of this command — its old default bucketDir pointed at
s3://r.acidgenomics.com/packages/, a legacy bucket holding zero live objects (the
real origin is the account-scoped r-<acct>-us-east-1-an bucket), so every deploy
through it was silently writing into a void. AcidDevTools::publish() now shells out
to koopa app r publish-docs directly instead of calling it.
Don't forget the GitHub "Website" field. Each r-<pkg> repo has a
homepage field (shown as "Website" on the repo page) that's independent of
anything in the repo content — a URL migration like this one won't touch it
unless you update it separately:
gh api -X PATCH repos/acidgenomics/r-<pkg> -f homepage="https://r.acidgenomics.com/<pkg>/"
# verify:
gh api repos/acidgenomics/r-<pkg> --jq '.homepage'
The Python side (py-<pkg>) uses the same pattern with
https://python.acidgenomics.com/<pkg>/.
Multi-account gh gotcha: the PATCH 404s (not 403 — GitHub returns 404 on
a repo write without permission, not a clear auth error) if the active gh
account lacks write access to the acidgenomics org. Check first:
gh auth status # look for "Active account: true" next to the right user
If the active account is wrong, gh auth switch --hostname github.com --user <owner>
— this changes the machine-wide active account for every future gh command,
not just the current one. Confirm with the user before switching if it's not
obviously already their intent.
pkgdown silent-failure traps
Found across the full 24-package fleet in one sweep (2026-07-29). All four either produce an empty page with no error, or abort the build in a way that doesn't point at the actual root cause:
1. NEWS.md must never open with a top-level # heading. If line 1 is
# Release notes followed by ## <Pkg> X.Y.Z (...) release headings, pkgdown
parses the whole file as one h1-rooted section, calls tweak_section_levels()
to demote every heading (release headings become h3), then matches versions
only against h2. It finds none, drops every section, and emits no version headings found as a warning rather than an error. The page still builds and
deploys, just with an empty Changelog. Fix: delete the # line (and the
blank line after it) so the file starts directly at ## <Pkg> .... Verify
before deploying:
e <- loadNamespace("pkgdown")
pkg <- pkgdown::as_pkgdown(".")
nrow(e$data_news(pkg)) # must be > 0, with no accompanying warning
2. news: one_page: false must be a YAML mapping, not a list.
# wrong: silently ignored, one_page always defaults to TRUE
news:
- one_page: false
# right
news:
one_page: false
pkgdown::check_pkgdown() reports "No problems found" either way, so this
never surfaces as a lint failure. Confirm the fix took effect by checking
docs/news/ for per-minor-version pages (news-X.Y.html) instead of a
single index.html.
3. pkgdown 2.2+ hard-errors when _pkgdown.yml's reference: index omits
any exported topic (internal S4 classes, show, dots, etc.) not marked
@keywords internal. Older pkgdown (2.0.x-2.1.x) only warned, so a package
last deployed under an older version can carry a stale, incomplete
reference: index that only breaks on the next upgrade. build_site()'s
error message names the missing topics directly; add a title: Internal
section listing them.
4. Deployed docs paths are case-sensitive, because the S3 prefix is
always the lowercased Package field. /AcidBase/news/ 404s; /acidbase/news/
is 200. Always spot-check a deploy with the lowercase slug, and check content
inside <main> specifically: the page's own table-of-contents sidebar also
contains an <h2>On this page</h2>, which will falsely read as "has content"
in a naive grep -c '<h2' check.
5. goalie::bapply()'s own parameter is useNames, not USE.NAMES — a
caller that (understandably, since it wraps vapply()) passes USE.NAMES =
doesn't hit an "unused argument" error; it silently falls through bapply()'s
own ... and collides with the USE.NAMES = useNames that bapply() passes
to vapply() internally, aborting with formal argument "USE.NAMES" matched by multiple actual arguments. This breaks any code path that reaches the
call — including, non-obviously, a pkgdown vignette that calls a completely
unrelated-looking function (e.g. Cellosaurus(), via cacheUrl() →
initDir() → bapply() in AcidBase), so the error surfaces at build_site()
time pointing at the vignette chunk, not at the actual bad call site several
frames down. Found live in AcidBase::initDir() and three call sites in
pipette::export-methods.R; grep any package that calls bapply() for this:
grep -rn "bapply(" -A6 R/*.R | grep -B6 "USE\.NAMES"
Fix is a rename, not a logic change: USE.NAMES = <flag> → useNames = <flag> at the call site. Reinstall the fixed package
(devtools::install(upgrade = FALSE)) before re-testing a downstream
package that depends on it — R doesn't pick up source changes in an
installed dependency without a reinstall.
S4 list-like classes (CompressedCharacterList/CharacterList/etc.) are slow to index in a loop
Found while investigating why Cellosaurus() (168,970 rows) took 4+ minutes,
versus py-cellosaurus's pandas equivalent finishing in under 9 seconds for
the same dataset. Profiling (Rprof, plus manual Sys.time() bracketing of
each pipeline stage — Rprof doesn't see time spent inside mclapply's
forked children, only the parent's IPC overhead, so bracket stage timings
directly rather than trusting a flat profile alone) isolated two real,
independently-fixable bottlenecks, plus one new unrelated bug surfaced along
the way. All three verified with identical() against the pre-fix output on
the real, full dataset — a synthetic/small-N benchmark is not sufficient
here; see the second finding below for why.
1. AcidPlyr::rbindToDataFrame() did two named-list scans per cell.
Building the transposed DFrame used a nested Map(Map(...)): for every
(row, column) pair it checked j %in% names(x[[i]]) then looked up
x[[i]][[j]] by name — two linear scans per cell, ~2.9M cells for this
dataset. Fixed by doing one match() per row (not per cell) against the
full column set, then a single vectorized row[idx] to align all columns at
once — 10.8s → 8.4s on the real data (measured in isolation, i.e. calling
rbindToDataFrame() directly on the already-parsed entry list, not via the
full Cellosaurus() pipeline). Two correctness traps hit while doing
this, both caught by identical() against the original, not by
inspection:
Map(i = seq_along(x), ...)'sUSE.NAMES = TRUEdoes not name the result wheniis an unnamed integer vector —Map/mapplyonly names-by-value when the varying argument is itself a character vector. A naive rewrite that iteratesX = x(the named list) instead ofX = seq_along(x)silently changes output names on nested-list columns.row[idx]on an atomic vector row coerces an unmatched (NA) position to that vector's type (e.g.NA_integer_), not a plain logicalNA— but the original code's fallback is always plainNAregardless of row type. Must overwrite:out[is.na(idx)] <- list(NA)after the vectorized index, for both the list-row and atomic-row branches.- 2.
[[on aCompressedCharacterList/CharacterList(IRanges S4 classes) costs ~1 ms/call, because it goes through S4 dispatch and slot-based range extraction every time, not a simple offset lookup..batchFormatNestedCols()called[[i]]on six such columns inside a per-row loop nested insidemclapply— for 168,970 rows that's ~1M[[calls, and since it's insidemclapply, every forked worker re-pays this cost independently rather than sharing one paid-once cost. This single function accounted for 257 ofCellosaurus()'s ~290 total seconds. Fix: convert each column to a plain baselistwithas.list()once, before themclapplyloop —list[[i]]is O(1) with zero dispatch. 257s → 38s (6.8x) on the real dataset, verifiedidentical(). This class of bug is invisible in a small/synthetic benchmark — the per-call dispatch overhead is roughly constant, so it only dominates runtime once the row count is large enough that dispatch cost outweighs the actual per-row work; a 5,000-row synthetic test showed no meaningful difference, but a 168,970-row real dataset showed a 6.8x gap. When benchmarking anything that indexes an IRanges S4 list-like class in a loop, test at (or extrapolate from) the real data's actual scale, not a small stand-in. - New bug found, not yet fixed:
AcidBase::download()(download.R:60-66) hardcodesmode = "wb"when forwarding toutils::download.file(), but also forwards...after it.pipette:::.localOrRemoteFile()(import-methods.R:563-568) passes its ownmode = mode(computed from the file extension,"w"for text or"wb"for binary) through that same..., so any text-file download (mode = "w") collides with the hardcodedmode = "wb"and aborts withformal argument "mode" matched by multiple actual arguments. Surfaced as aCellosaurustest failure (test-currentCellosaurusVersion.R) with a call stack that looks Cellosaurus-specific but the actual defect is inAcidBase. Same root cause class as thebapply()/USE.NAMESbug above: a function that hardcodes an argument and forwards...risks the caller supplying that same argument by name through.... Not fixed in this session — either drop the hardcodedmode = "wb"and require callers to pass it, or have.localOrRemoteFile()stop passingmodeexplicitly and letdownload()always write binary (check whether any caller actually relies on text mode first).
Local Clone Setup
All 24 active packages are cloned under ~/git/personal/r-<pkgname> (HTTPS origin).
The canonical set is derived from S3 — not from gh repo list. The repo names
are the .tar.gz filenames stripped of version, lowercased, and prefixed with r-.
Branch convention: develop = active development; main = release-ready.
Always work on develop; switch to main only to publish (step 3 of the release flow).
To sync / bootstrap all clones (zsh-safe — for repo in $repos doesn't word-split):
aws --profile acidgenomics s3 ls \
s3://r-<account-id>-us-east-1-an/src/contrib/ \
| awk '/\.tar\.gz$/ {print $NF}' \
| sed -E 's/_[0-9].*\.tar\.gz$//' \
| awk '{print "r-" tolower($0)}' \
| sort -u \
| while IFS= read -r repo; do
target=~/git/personal/"$repo"
if [ -d "$target/.git" ]; then
git -C "$target" fetch --quiet origin develop
git -C "$target" checkout --quiet develop
git -C "$target" pull --quiet --ff-only origin develop
else
git clone --quiet -b develop \
"https://github.com/acidgenomics/$repo.git" "$target"
fi
done
Verify afterwards:
aws --profile acidgenomics s3 ls s3://r-<account-id>-us-east-1-an/src/contrib/ \
| awk '/\.tar\.gz$/ {print $NF}' | sed -E 's/_[0-9].*\.tar\.gz$//' \
| awk '{print "r-" tolower($0)}' | sort -u \
| while IFS= read -r repo; do
branch=$(git -C ~/git/personal/"$repo" rev-parse --abbrev-ref HEAD 2>/dev/null)
printf '%s\t%s\n' "$repo" "${branch:-MISSING}"
done
Expected: 24 lines, all develop.
End-to-End Release (packages in ~/git/personal)
- Bump
Version:andDate:inDESCRIPTION; addNEWS.mdentry. - Commit on
develop, open PR develop→main, merge. - Publish from main (builds, uploads, tags, and pushes the tag in one step):
git -C ~/git/personal/r-<pkg> checkout main && git pull koopa app r publish ~/git/personal/r-<pkg> --no-check - Verify (smoke test below).
The tag v<ver> is created and pushed to origin automatically by publish. Use
--no-tag to opt out. No drat git commit needed — S3 is the source of truth.
Relicensing (AGPL-3 → Apache-2.0)
Done as of 2026-07-29 — verified live by grepping License: for every
Package: entry in src/contrib/PACKAGES: all 24 active packages report
License: Apache License (>= 2), including every name on the older
AGPL-3-holdouts list this section used to carry (AcidCLI, AcidDevTools,
AcidExperiment, AcidGSEA, AcidGenerics, AcidMarkdown, AcidPlots, AcidRoxygen,
AcidSingleCell, AcidTest, Chromium, DESeqAnalysis, EggNOG, PANTHER, WormBase,
basejump). The landing-page footer (generated by _generate_landing()) also
now correctly says Apache 2.0, matching. If a future package somehow reverts,
the pattern is:
License: Apache License (>= 2)in DESCRIPTION (CRAN shipped-template form)LICENSE.mdat repo root (usethis::use_apache_license()output)- Delete the old plain
LICENSEfile - Update
.Rbuildignore:^LICENSE\.md$(not^LICENSE$) - Add
License changes:section to NEWS.md - Bump patch version (so the new Apache-2 tarball differs from the published AGPL one)
DESCRIPTION Conventions
Version: X.Y.Z— manual bump; no bumpverDate: YYYY-MM-DD— update to release dateLicense: Apache License (>= 2)— do NOT use| file LICENSELICENSE.md— Apache-2.0 Markdown (usethis output); no plainLICENSE.Rbuildignore:^LICENSE\.md$
NEWS.md Format
## <Pkg> X.Y.Z (YYYY-MM-DD)
Major changes:
Minor changes:
Bug fixes:
License changes:
Omit empty sections. Top heading = current release.
Version-bump Rule
If Version: in DESCRIPTION matches what's already in src/contrib/PACKAGES,
bump the patch before publishing — S3 overwrites but the PACKAGES index must have
a new version to be useful. koopa app r archive will move the old one to Archive/.
Verification Smoke Test
tmp=$(mktemp -d)
TMP_LIB="$tmp" Rscript -e '
lib <- Sys.getenv("TMP_LIB")
install.packages("goalie", repos = "https://r.acidgenomics.com", lib = lib)
cat("OK", as.character(packageVersion("goalie", lib.loc = lib)), "\n")
'
rm -rf "$tmp"
Expected: binary pulled from sonoma-arm64/contrib/4.6/ with no warning.
install.packages() blocked in an agent session: guard-installs.sh (a
PreToolUse hook) rejects this smoke test when run from inside Claude Code —
by design, installs require explicit user action. Substitute an HTTP-only
check that proves the same thing without installing anything:
curl -s "https://r.acidgenomics.com/src/contrib/PACKAGES" | grep -A2 "^Package: goalie"
curl -sI "https://r.acidgenomics.com/src/contrib/goalie_<ver>.tar.gz" # 200 + correct content-type
Surface the real install.packages() command to the user to run themselves
if the full round-trip is needed.
Pre-Release Quality Gate
Run before publish (replaces manual R CMD check):
AcidDevTools::check(
path = "~/git/personal/r-<pkg>",
lints = TRUE,
urls = FALSE, # skip — requires pandoc, slow
cran = FALSE, # skip — has issues with requireNamespace
biocCheck = TRUE # auto-skips if no biocViews or dir name != pkg name
)
Gate order inside check(): lint → URL check → rcmdcheck() → BiocCheck().
All must pass. The function errors on any lint; it does NOT error on URL failures.
Always run with LANG=LC_ALL=en_US.UTF-8 to avoid Greek-letter translation
warnings from S4 method dispatch during load_all.
See skill acid-r-package for the full dev conventions (tooling, lintr, roxygen2 8.x).
AcidDevTools Integration
AcidDevTools::publish() calls koopa app r publish --no-check internally, and
(when pkgdown = TRUE) shells out to koopa app r publish-docs before that. The
check step remains in R; build/upload/reindex/pkgdown-deploy are all koopa.
Source: ~/git/personal/r-aciddevtools/R/publish.R
Active Package List (src/contrib as of 2026-07-29)
24 active source packages; 24 with sonoma-arm64 binaries. All Apache 2.0 — see Relicensing above.
Archived / no longer maintained (source removed from src/contrib, so absent
from the generated landing page automatically): cBioPortalAnalysis,
DepMapAnalysis.
Docs-only cleanup: acidshiny/, bb8/, and koopa (the R package, not
this repo) had orphaned pkgdown doc prefixes on the bucket with no matching
source package — deleted (108 objects). bcbiobase/, bcbiornaseq/, and
bcbiosinglecell/ docs were deliberately kept live despite being in the
same no-source-package state: they're published, possibly-cited packages
(bcbioBase, bcbioRNASeq, bcbioSingleCell), and there's no redirect to offer a
reader who lands on an old link. Before deleting any orphaned doc prefix,
check whether the package was ever CRAN/Bioconductor-published or likely
cited in a paper — if so, default to keeping the docs even though the
package itself is retired.
_SKELETON_BINARY_PREFIXES (in cran.py)
Add a new entry whenever a new R minor version ships, before publishing any packages for that version. Empty PACKAGES prevent R client warnings on macOS arm64 machines running the old R version.
_SKELETON_BINARY_PREFIXES: list[str] = [
"bin/macosx/big-sur-arm64/contrib/4.3",
"bin/macosx/big-sur-arm64/contrib/4.4",
"bin/macosx/big-sur-arm64/contrib/4.5",
"bin/macosx/big-sur-arm64/contrib/4.6",
# Add "bin/macosx/sonoma-arm64/contrib/4.7" when R 4.7 ships
]
Comparison to Python Publish
| Concept | Python | R |
|---|---|---|
| Command | koopa app python publish | koopa app r publish |
| Docs command | koopa app python publish-docs | koopa app r publish-docs |
| Module | koopa/pypi.py | koopa/cran.py |
| Build | uv build | R CMD build + R CMD INSTALL --build |
| Index format | PEP 503 HTML (hand-rolled) | CRAN PACKAGES DCF |
| Manifest regen | list S3 + SHA256 via download | stream DESCRIPTION only; ETag = MD5 |
| Version mgmt | bumpver | manual in DESCRIPTION |
| Index URL | https://python.acidgenomics.com/ | https://r.acidgenomics.com/ |
| Docs URL | https://python.acidgenomics.com/<name>/ | https://r.acidgenomics.com/<name>/ |
| Artifact prefix | /packages/ (wheels/sdists) | /src/contrib/, /bin/ (tarballs) |
| Dep install on binary build | N/A (pure Python) | auto via install.packages() |
Both sites now follow the same rule: artifacts under reserved prefixes, docs at
/<name>/. Python's /packages/ prefix holds real artifacts (PEP 503 convention,
matching pypi.org) and must not be repurposed for docs. R has no artifacts at
/packages/ — that prefix is retired entirely.