5 Commits
Author SHA1 Message Date
Worph e6a9ab9e45 CI: push to ipcr-push-85-17-246-67.ip.nsl.sh (the box's cert no longer covers nip.io) [skip ci] 2026-11-04 14:31:35 +00:00
Manielle 02bff131ff CI accepts four-part tags (v1.2.3.1) for fixes
Publish image / publish (push) Successful in 1m50s
Test / cargo test (push) Successful in 5m59s
2026-10-25 14:22:06 +01:00
Manielle 560effe226 clippy: a char array for the subtitle separators in main_title (manual_pattern_char_comparison)
Test / cargo test (push) Successful in 5m29s
Publish image / publish (push) Failing after 15m37s
2026-10-19 20:49:39 +00:00
Manielle e14797e57b ci: no debug info and two jobs at most, so the test link fits in the runner's memory
Test / cargo test (push) Failing after 3m48s
The test job's linker was killed with signal 9 twice (run 705): cargo test links
several test binaries at once, each with full debug info. CARGO_PROFILE_DEV_DEBUG=0
and CARGO_BUILD_JOBS=2 keep the link within the job's memory; the tests are the
same.
2026-10-19 20:45:18 +00:00
Manielle efba2f2825 gutenberg: one bind() for the search and redeem paths
Test / cargo test (push) Failing after 2m47s
compute_outcomes still passed the identifier-only lookup's Option<String> to
into_discovery_record, which now takes the binding (CI: E0308). Both paths now
call bind(): the identifier, else the title match for an etext Open Library
lists under no work, so a redeemed record is bound exactly as its search record
was.
2026-10-19 19:38:22 +00:00
5 changed files with 41 additions and 21 deletions
+6 -3
View File
@@ -2,10 +2,10 @@
# folder exists, and GitHub never reads it.
#
# Publishes to IPCR on watch.nsl.sh instead of ghcr.io:
# push ipcr-push-85-17-246-67.nip.io/metadec.eth/<repo>:<tag> docker login as metadec.eth with the
# push ipcr-push-85-17-246-67.ip.nsl.sh/metadec.eth/<repo>:<tag> docker login as metadec.eth with the
# IPCR_PUSH_TOKEN secret (the metadec.eth namespace token from the IPCR dashboard)
# pull ipcr-watch.nsl.sh/ipns/metadec.eth/metadec.eth/<repo>:<tag> anonymous
# The push goes to ipcr-push-85-17-246-67.nip.io, the box's own address, not ipcr-push-watch.nsl.sh: Cloudflare fronts
# The push goes to ipcr-push-85-17-246-67.ip.nsl.sh, the box's own address, not ipcr-push-watch.nsl.sh: Cloudflare fronts
# *.watch.nsl.sh and refuses any request over 100 MB (413), which a large image layer is.
# That host's certificate is signed by the box's private Mesh Router CA, given to buildkit from
# the IPCR_CA_PEM repo variable. docker login would make the job's Docker daemon check the
@@ -27,7 +27,7 @@ on:
tags: ['v*']
env:
REGISTRY: ipcr-push-85-17-246-67.nip.io
REGISTRY: ipcr-push-85-17-246-67.ip.nsl.sh
IMAGE_NAME: metadec.eth/${{ github.event.repository.name }}
jobs:
@@ -71,6 +71,9 @@ jobs:
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
# A fix to release 1.2.3 is tagged v1.2.3.1: four numbers are not semver, so
# the pattern above makes no tag for it; this one does (image tag 1.2.3.1).
type=match,pattern=v(\d+\.\d+\.\d+\.\d+),group=1
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,format=short
+6
View File
@@ -11,6 +11,12 @@ jobs:
rust:
name: cargo test
runs-on: ubuntu-latest
# The job ran out of memory linking the test binaries (ld killed with
# signal 9, run 705 twice): no debug info, and at most two links at once.
# What is tested does not change.
env:
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_BUILD_JOBS: "2"
steps:
- uses: actions/checkout@v4
# rust-toolchain.toml pins the channel (with clippy); rustup installs it on
+3
View File
@@ -43,6 +43,9 @@ jobs:
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
# A fix to release 1.2.3 is tagged v1.2.3.1: four numbers are not semver, so
# the pattern above makes no tag for it; this one does (image tag 1.2.3.1).
type=match,pattern=v(\d+\.\d+\.\d+\.\d+),group=1
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,format=short
+25 -17
View File
@@ -44,6 +44,26 @@ struct Binding {
by_title: bool,
}
/// One etext's binding: the identifier Open Library records, else — only for an
/// etext it lists under no work — the title match. Shared by the search path and
/// the redeem path so the two cannot bind the same book differently.
async fn bind(
client: &OpenLibraryClient,
id: u64,
title: &str,
author: &str,
deadline: std::time::Duration,
) -> Result<Option<Binding>, GatewayError> {
Ok(match client.gutenberg_etext(id, deadline).await? {
Etext::One(work) => Some(Binding { work, by_title: false }),
Etext::Several => None,
Etext::Unlisted => client
.title_work(title, author, deadline)
.await?
.map(|work| Binding { work, by_title: true }),
})
}
/// Canonical Gutendex base URL. Overridable via [`GutenbergPlugin::with_base_url`]
/// for tests + private mirrors.
const DEFAULT_BASE_URL: &str = "https://gutendex.com";
@@ -451,19 +471,7 @@ impl GutenbergPlugin {
.collect();
stream::iter(wanted)
.map(|(id, title, author)| async move {
// The identifier first; a title match only for an etext Open
// Library lists under no work at all.
let lookup = async {
let deadline = openlibrary_client::BINDING_DEADLINE;
Ok::<_, GatewayError>(match client.gutenberg_etext(id, deadline).await? {
Etext::One(work) => Some(Binding { work, by_title: false }),
Etext::Several => None,
Etext::Unlisted => client
.title_work(&title, &author, deadline)
.await?
.map(|work| Binding { work, by_title: true }),
})
};
let lookup = bind(client, id, &title, &author, openlibrary_client::BINDING_DEADLINE);
match tokio::time::timeout(BINDING_TIMEOUT, lookup).await {
Ok(Ok(binding)) => binding,
Ok(Err(e)) => {
@@ -628,9 +636,9 @@ impl FeederPlugin for GutenbergPlugin {
// Not user-facing, so the binding lookup may queue for the full search
// deadline. A failure still leaves the record unbound, never fails the
// compute.
let author = book.authors.first().map(|a| a.name.clone()).unwrap_or_default();
let work = match self.openlibrary.as_ref() {
Some(ol) => ol
.gutenberg_work(book.id, openlibrary_client::SEARCH_DEADLINE)
Some(ol) => bind(ol, book.id, &book.title, &author, openlibrary_client::SEARCH_DEADLINE)
.await
.unwrap_or_else(|e| {
debug!(
@@ -704,8 +712,8 @@ impl FeederPlugin for GutenbergPlugin {
/// Convert a `GutendexBook` into our wire-level `DiscoveryRecord`.
///
/// `openlibrary_work` is the binding from [`OpenLibraryClient::gutenberg_work`]:
/// `Some` only when Open Library records exactly one work for this etext.
/// `openlibrary_work` is the binding from [`bind`]: the one work Open Library
/// records for this etext, or, when it records none, the title match.
fn into_discovery_record(
book: GutendexBook,
cover_cid: Option<String>,
@@ -769,7 +769,7 @@ fn etext_of(body: &Value, gutenberg_id: u64) -> Etext {
pub fn main_title(raw: &str) -> String {
let lower = raw.to_lowercase();
let head = lower
.split(|c: char| matches!(c, ';' | ':' | '(' | '['))
.split([';', ':', '(', '['])
.next()
.unwrap_or_default();
let head = head.find(", or").map_or(head, |i| &head[..i]);