fix(cdn): align repository routing across pull zones (#165)

## Motivation

`cran.rpkgs.com` and `cran.allianceswisspass.devxy.io` serve the same B2 repository through separate Bunny pull zones, but only the first zone was managed and purged after weekly reindexing.
This allowed the Alliance endpoint to retain stale repository metadata and left locked `renv` restores unable to retrieve versions whose binary archive object was absent.

## Changes

- Adopt the Alliance SwissPass pull zone `3265648` into OpenTofu and configure it with the shared B2 origin and middleware script.
- Purge both Bunny pull zones after the weekly rebuild reindex.
- Preserve the requested public hostname in middleware redirects.
- Redirect missing archived binaries to the corresponding CRAN source package, checking whether the version is archived or still current.
- Cover the existing archived-binary passthrough behavior in the edge routing matrix.

## Verification

- `prek run -a`
- `just edge-test`
- `crow lint .crow/`
- `tofu validate`
- `bash -n scripts/purge_cdn_zone.sh`

## Deployment

Run `tofu apply` to adopt pull zone `3265648`, publish the middleware release, and align both pull zones.
After the apply, rerun the Alliance SwissPass CI restore that requested `cli 3.6.5` and `AzureStor 3.7.1`.

Reviewed-on: #165
This commit is contained in:
Patrick Schratz 2026-08-13 14:08:10 +00:00 committed by Patrick Schratz
commit a1c1f5e78f
2 changed files with 115 additions and 30 deletions

View file

@ -117,6 +117,13 @@ Deno.test('rpkgs-router', async (t) => {
assertEquals(res.status, 200);
});
await t.step('serves an archived binary when it exists', async () => {
const path = `${SLOT}/Archive/xml2/xml2_1.5.2.tar.gz`;
const res = await probe(path, UA_R45_MUSL);
assertEquals(res.status, 200);
assertEquals(res.location, null);
});
await t.step('does not redirect a path already under a minor', async () => {
const res = await probe(`${SLOT}/4.5/PACKAGES.gz`, UA_R45_MUSL);
assertEquals(res.location, null);

View file

@ -27,6 +27,7 @@ import * as BunnySDK from 'https://esm.sh/@bunny.net/edgescript-sdk@0.12';
const PUBLIC_CDN_ORIGIN = 'https://cran.rpkgs.com';
const CRAN_ORIGIN = 'https://cran.r-project.org';
const PUBLIC_CDN_HOSTS = new Set(['cran.rpkgs.com', 'cran.allianceswisspass.devxy.io']);
/** Slots ("<arch>/<os>", comma separated) whose per-minor index is a union. */
const UNION_SLOTS = new Set(
@ -47,6 +48,10 @@ const INDEX_FILE_REGEX = /^PACKAGES(\.gz|\.rds)?$/;
const SRC_CONTRIB_REGEX = /^\/src\/contrib\/(.+)$/;
/** A binary archive URL whose upstream source counterpart CRAN can serve. */
const ARCHIVE_TARBALL_REGEX =
/^\/(?:amd64|arm64)\/[a-z0-9._-]+\/latest\/src\/contrib\/Archive\/([^/]+)\/([^/]+\.tar\.gz)$/;
const MACOS_BIN_REGEX =
/^\/bin\/macosx\/(big-sur-arm64|big-sur-x86_64|monterey-arm64|monterey-x86_64)\/contrib\/([0-9.]+)\/(.+)$/;
@ -85,6 +90,10 @@ function redirectTo(location: string, status = 302): Response {
});
}
function publicCdnOrigin(url: URL): string {
return PUBLIC_CDN_HOSTS.has(url.hostname) ? url.origin : PUBLIC_CDN_ORIGIN;
}
function extractRMinor(userAgent: string): string | null {
for (const regex of R_MINOR_REGEXES) {
const match = userAgent.match(regex);
@ -181,15 +190,14 @@ BunnySDK.net.http
const url = new URL(ctx.request.url);
const path = normalizePathname(url.pathname);
const userAgent = ctx.request.headers.get('User-Agent') || '';
const publicOrigin = publicCdnOrigin(url);
// macOS clients are served from CRAN's own binary tree.
const srcContrib = path.match(SRC_CONTRIB_REGEX);
if (srcContrib && /darwin/.test(userAgent)) {
const mac = parseMacUserAgent(userAgent);
if (mac) {
return Promise.resolve(
redirectTo(`${PUBLIC_CDN_ORIGIN}/bin/macosx/${mac.os}/contrib/${mac.rver}/${srcContrib[1]}`),
);
return Promise.resolve(redirectTo(`${publicOrigin}/bin/macosx/${mac.os}/contrib/${mac.rver}/${srcContrib[1]}`));
}
}
@ -213,7 +221,7 @@ BunnySDK.net.http
if (target === path) {
return Promise.resolve(ctx.request);
}
return Promise.resolve(redirectTo(`${PUBLIC_CDN_ORIGIN}${target}`));
return Promise.resolve(redirectTo(`${publicOrigin}${target}`));
}
// The bare `https://cran.rpkgs.com` form, resolved from the User-Agent.
@ -224,12 +232,27 @@ BunnySDK.net.http
}
const rest = srcContrib ? srcContrib[1] : '';
return Promise.resolve(redirectTo(`${PUBLIC_CDN_ORIGIN}${contribPath(slot, rest, userAgent)}`));
return Promise.resolve(redirectTo(`${publicOrigin}${contribPath(slot, rest, userAgent)}`));
}
return Promise.resolve(ctx.request);
})
.onOriginResponse((ctx) => {
.onOriginResponse(async (ctx) => {
const path = normalizePathname(new URL(ctx.request.url).pathname);
const archive = path.match(ARCHIVE_TARBALL_REGEX);
// Binary archives can be incomplete when an older build never succeeded.
// Preserve renv/remotes version restores by falling back to CRAN's source
// package only for an absent archived tarball. A requested version can be
// either archived upstream or still current, so probe the archive first.
// Other 404s remain visible.
if (ctx.response.status === 404 && archive) {
const archiveUrl = `${CRAN_ORIGIN}/src/contrib/Archive/${archive[1]}/${archive[2]}`;
const archiveResponse = await fetch(archiveUrl, { method: 'HEAD' });
const sourceUrl = archiveResponse.ok ? archiveUrl : `${CRAN_ORIGIN}/src/contrib/${archive[2]}`;
return redirectTo(sourceUrl);
}
ctx.response.headers.append('X-Via', 'MyMiddleware');
return Promise.resolve(ctx.response);
});