diff --git a/.crow/auto-apply-patches.yaml b/.crow/auto-apply-patches.yaml new file mode 100644 index 0000000..15cc1ee --- /dev/null +++ b/.crow/auto-apply-patches.yaml @@ -0,0 +1,65 @@ +# Auto-apply registry patches (issue #115, step 3 automation). +# Classifies `single_builds` failures and, for the top-N auto-proposable +# candidates by failure volume, writes the registry entries onto the reused +# `auto/registry-patch-proposals` branch and opens/updates a single PR. +# Nothing merges: the `trial-build-registry` pipeline is the merge gate, and a +# human reviews the PR. Novel source diffs / unknown signatures are never +# proposed. Global across platforms, so a single job -- no matrix. +# +# Needs a write token (REPO_RW_TOKEN) to push and FORGEJO_TOKEN to open the PR. +# Register the `auto-apply-patches` cron in the crow UI, or run manually: +# woodpecker-cli pipeline create --var task=auto-apply-patches --branch=main 7 +variables: + patch_limit: + description: 'Max candidates to propose per run (top by failure volume).' + default: '10' + +when: + - event: manual + evaluate: 'task == "auto-apply-patches"' + - event: cron + cron: auto-apply-patches + +skip_clone: true + +labels: + group: rpkgs-amd64 + +steps: + - name: 'Auto-apply registry patches' + image: reg.devxy.io/rpkgs/build-env-alpine:3.24 + pull: true + environment: + PGPASS: + from_secret: PGPASS + REPO_RO_TOKEN: + from_secret: REPO_RO_TOKEN + REPO_RW_TOKEN: + from_secret: REPO_RW_TOKEN + FORGEJO_TOKEN: + from_secret: FORGEJO_TOKEN + GIT_USER: devxy-bot + GIT_EMAIL: bot@devxy.io + PATCH_LIMIT: ${patch_limit} + R_VERSION: 4.5.3 + R_LIBS_USER: /mnt/cache/R-pkgs + commands: + - git clone -q https://pat-s:$$REPO_RO_TOKEN@git.devxy.io/devxy/build-cran-binaries.git . + - mkdir -p /mnt/cache/R-pkgs + - rm -rf /mnt/cache/R-pkgs/00LOCK-* + - /opt/R/$R_VERSION/bin/R -q -e 'pak::pak(c("httr2", "jsonlite"))' + - /opt/R/$R_VERSION/bin/Rscript local/propose-patches.R --open-pr --limit $PATCH_LIMIT + backend_options: + kubernetes: + resources: + requests: + memory: 1Gi + cpu: 2000m + limits: + memory: 2Gi + cpu: 2000m + tolerations: + - key: 'CI' + operator: 'Equal' + value: 'true' + effect: 'NoSchedule' diff --git a/.crow/trial-build-registry.yaml b/.crow/trial-build-registry.yaml new file mode 100644 index 0000000..1ac71e8 --- /dev/null +++ b/.crow/trial-build-registry.yaml @@ -0,0 +1,140 @@ +# Merge gate for the auto-patch PR (issue #115, step 3). +# For each platform, trial-builds every registry entry the auto-patch branch +# ADDS (vs main) in that platform's own `reg.devxy.io/rpkgs/build-env-*` image, +# with the registry applied. A row with no new entries for its platform is a +# fast no-op. The pipeline is green only if every new entry builds, so it gates +# the PR before merge. Nothing is uploaded/archived/recorded. +# +# The repo uses no `pull_request` triggers, so this runs manually against the +# branch (or on a cron); point it at the auto-patch branch via `patch_branch`: +# woodpecker-cli pipeline create --var task=trial-build-registry \ +# --var patch_branch=auto/registry-patch-proposals --branch=main 7 +variables: + patch_branch: + description: 'Branch whose new registry entries to trial-build.' + default: auto/registry-patch-proposals + +when: + - event: manual + evaluate: 'task == "trial-build-registry"' + - event: cron + cron: trial-build-registry + +skip_clone: true + +labels: + group: rpkgs-${ARCH} + +matrix: + include: + - OS: alpine-322 + ARCH: amd64 + R_VERSION: 4.5.3 + IMG: alpine:3.24 + - OS: alpine-322 + ARCH: arm64 + R_VERSION: 4.5.3 + IMG: alpine:3.24 + - OS: alpine-323 + ARCH: amd64 + R_VERSION: 4.5.3 + IMG: alpine:3.24 + - OS: alpine-323 + ARCH: arm64 + R_VERSION: 4.5.3 + IMG: alpine:3.24 + - OS: alpine-324 + ARCH: amd64 + R_VERSION: 4.5.3 + IMG: alpine:3.24 + - OS: alpine-324 + ARCH: arm64 + R_VERSION: 4.5.3 + IMG: alpine:3.24 + - OS: redhat-8 + ARCH: amd64 + R_VERSION: 4.4.3 + IMG: redhat:8 + - OS: redhat-8 + ARCH: arm64 + R_VERSION: 4.4.3 + IMG: redhat:8 + - OS: redhat-9 + ARCH: amd64 + R_VERSION: 4.4.3 + IMG: redhat:9 + - OS: redhat-9 + ARCH: arm64 + R_VERSION: 4.4.3 + IMG: redhat:9 + - OS: redhat-10 + ARCH: amd64 + R_VERSION: 4.5.3 + IMG: redhat:10 + - OS: redhat-10 + ARCH: arm64 + R_VERSION: 4.5.3 + IMG: redhat:10 + - OS: ubuntu-2204 + ARCH: amd64 + R_VERSION: 4.4.3 + IMG: ubuntu:jammy + - OS: ubuntu-2204 + ARCH: arm64 + R_VERSION: 4.4.3 + IMG: ubuntu:jammy + - OS: ubuntu-2404 + ARCH: amd64 + R_VERSION: 4.4.3 + IMG: ubuntu:noble + - OS: ubuntu-2404 + ARCH: arm64 + R_VERSION: 4.4.3 + IMG: ubuntu:noble + - OS: ubuntu-2604 + ARCH: amd64 + R_VERSION: 4.5.3 + IMG: ubuntu:resolute + - OS: ubuntu-2604 + ARCH: arm64 + R_VERSION: 4.5.3 + IMG: ubuntu:resolute + +steps: + - name: 'Trial-build new registry entries' + image: reg.devxy.io/rpkgs/build-env-${IMG} + pull: true + environment: + B2_S3_ACCESS_KEY: + from_secret: B2_S3_ACCESS_KEY + B2_S3_SECRET_KEY: + from_secret: B2_S3_SECRET_KEY + REPO_RO_TOKEN: + from_secret: REPO_RO_TOKEN + GITHUB_PAT: + from_secret: GITHUB_PAT + PLATFORM: ${OS} + ARCH: ${ARCH} + R_VERSION: ${R_VERSION} + R_LIBS_USER: /mnt/cache/R-pkgs + commands: + - git clone -q --branch ${patch_branch} https://pat-s:$$REPO_RO_TOKEN@git.devxy.io/devxy/build-cran-binaries.git . + - git fetch -q origin main + - mkdir -p /mnt/cache/R-pkgs + - rm -rf /mnt/cache/R-pkgs/00LOCK-* + - /opt/R/$R_VERSION/bin/Rscript local/install-bincraft.R + - /opt/R/$R_VERSION/bin/Rscript local/trial-build-registry.R origin/main + backend_options: + kubernetes: + resources: + requests: + memory: 2Gi + cpu: 2000m + limits: + memory: 4Gi + cpu: 2000m + tolerations: + - key: 'CI' + operator: 'Equal' + value: 'true' + effect: 'NoSchedule' diff --git a/local/patches/README.md b/local/patches/README.md index 992395f..455a96c 100644 --- a/local/patches/README.md +++ b/local/patches/README.md @@ -81,6 +81,19 @@ Run the trial build inside the failing platform's build-env image; it uploads/ar Rscript local/trial-build-patch.R ``` +#### Autonomous PR + trial-build gate + +`--open-pr` closes the loop: it writes the top-N candidates (by failure volume) onto the reused `auto/registry-patch-proposals` branch and opens/updates a single PR. +`.crow/auto-apply-patches.yaml` runs this on a cron (needs `FORGEJO_TOKEN` and a write-scoped `REPO_RW_TOKEN`). + +```bash +# Bounded batch; opens/updates one PR. +PGPASS=... FORGEJO_TOKEN=... REPO_RW_TOKEN=... Rscript local/propose-patches.R --open-pr --limit 10 +``` + +The merge gate is `.crow/trial-build-registry.yaml`: matrixed over the build-env images, each platform trial-builds only the entries the branch **adds** (`local/trial-build-registry.R`, which diffs the registry against `main`) and is green only if every new entry builds. +The repo uses no `pull_request` triggers, so this gate runs manually or on a cron against the auto-patch branch (`--var patch_branch=...`); wiring it to fire on the PR needs `event: pull_request` enabled on the forge. + `--write` and `--open-issue` also append to `local/patches/proposals-log.json`, a ledger of what was proposed. ### Feedback loop (step 4) diff --git a/local/proposal-tracking-lib.R b/local/proposal-tracking-lib.R index 5efc87c..80c4ab2 100644 --- a/local/proposal-tracking-lib.R +++ b/local/proposal-tracking-lib.R @@ -175,4 +175,37 @@ blocked_summary <- function(report, max_pkgs = 15L) { }) } +# Does a registry entry's `platforms` apply to a build on `os` (e.g. +# "ubuntu-2604")? Mirrors bincraft's token match: an entry applies if any of its +# platform tokens is "*", the OS codename, or the distro family ("ubuntu"). +entry_applies_to_os <- function(entry_platforms, os) { + toks <- as.character(unlist(entry_platforms)) + family <- sub("-.*$", "", os) # ubuntu-2604 -> ubuntu + any(toks %in% c("*", os, family)) +} + +# Registry entries present in `current` but not in `base` (matched on +# package|platforms|versions), optionally restricted to those that apply to a +# given `os`. Used by the trial-build gate to build only the entries a PR adds. +new_registry_packages <- function(current, base, os = NULL) { + key <- function(e) { + sprintf( + "%s|%s|%s", + e$package %||% "?", + paste(sort(as.character(unlist(e$platforms))), collapse = ","), + e$versions %||% "?" + ) + } + base_keys <- vapply(base %||% list(), key, character(1L)) + added <- Filter(function(e) !(key(e) %in% base_keys), current %||% list()) + if (!is.null(os)) { + added <- Filter(function(e) entry_applies_to_os(e$platforms, os), added) + } + unique(vapply( + added, + function(e) as.character(e$package %||% ""), + character(1L) + )) +} + `%||%` <- function(a, b) if (is.null(a)) b else a diff --git a/local/propose-patches.R b/local/propose-patches.R index 7ff195b..fb45ea9 100644 --- a/local/propose-patches.R +++ b/local/propose-patches.R @@ -15,10 +15,17 @@ # # Usage: # PGPASS=... Rscript local/propose-patches.R [--platform P] [--arch A] [--min N] +# [--limit N] # (default) print candidates + validation, take no action # --write append candidates to local/patches/registry.json and # the proposals ledger (commit + open a PR yourself) # --open-issue post/update a Forgejo tracking issue (needs FORGEJO_TOKEN) +# --open-pr write the entries, push the `auto/registry-patch-proposals` +# branch, and open/update a PR autonomously (needs +# FORGEJO_TOKEN, and REPO_RW_TOKEN to push in CI). The +# `trial-build-registry` pipeline is the merge gate. +# --limit N only act on the top-N candidates by failure volume +# (bounded batch; the rest are picked up on the next run) # --json PATH also write the machine-readable candidate list to PATH options(error = function() { @@ -58,6 +65,8 @@ if (is.na(min_count)) { } do_write <- "--write" %in% args do_issue <- "--open-issue" %in% args +do_pr <- "--open-pr" %in% args +limit <- suppressWarnings(as.integer(opt_val("--limit", NA_character_))) if (nchar(Sys.getenv("PGPASS")) == 0L) { stop("PGPASS env var is not set; a DB password is required.") @@ -65,6 +74,8 @@ if (nchar(Sys.getenv("PGPASS")) == 0L) { registry_file <- file.path(script_dir, "patches", "registry.json") ledger_file <- file.path(script_dir, "patches", "proposals-log.json") +# Branch the autonomous PR reuses, so re-runs update one PR instead of piling up. +pr_branch <- "auto/registry-patch-proposals" # --------------------------------------------------------------------------- # Query failing builds (same shape as failing-builds-report.R) @@ -118,7 +129,8 @@ registered_pkgs <- unique(vapply( report <- build_triage_report(failures, registered_pkgs) report <- Filter(function(r) r$build_count >= min_count, report) -# Flatten auto-proposable groups into candidate records. +# Flatten auto-proposable groups into candidate records, carrying the group's +# build volume so we can prioritise the highest-impact fixes. candidates <- list() for (r in report) { if (is.null(r$proposed_entries)) { @@ -130,6 +142,7 @@ for (r in report) { signature = r$signature, confidence = r$confidence, tier = r$tier, + build_count = r$build_count, entry = r$proposed_entries[[pkg]] ) } @@ -173,6 +186,22 @@ if (length(candidates) == 0L) { q(status = 0) } +# Prioritise by failure volume, then apply --limit so one run tackles a bounded +# batch (the rest are picked up on the next run). +candidates <- candidates[order( + -vapply(candidates, function(c) c$build_count %||% 0L, integer(1L)) +)] +deferred <- 0L +if (!is.na(limit) && limit >= 0L && length(candidates) > limit) { + deferred <- length(candidates) - limit + candidates <- utils::head(candidates, limit) + cat(sprintf( + "\nLimiting to top %d candidate(s) by failure volume; %d deferred to a later run.\n", + limit, + deferred + )) +} + cat(sprintf( "\n%d candidate registry %s:\n", length(candidates), @@ -386,8 +415,163 @@ if (do_write) { cat(sprintf("Opened tracking issue #%d.\n", created$number)) } save_ledger(merge_ledger(load_ledger(), new_ledger_records)) +} else if (do_pr) { + forgejo_token <- Sys.getenv("FORGEJO_TOKEN") + if (nchar(forgejo_token) == 0L) { + stop("--open-pr requires FORGEJO_TOKEN (to open the PR).") + } + suppressPackageStartupMessages(library(httr2, quietly = TRUE)) + forgejo_base <- "https://git.devxy.io/api/v1" + repo <- "devxy/build-cran-binaries" + + # Write the entries + ledger, then commit them onto the reused auto branch. + jsonlite::write_json( + merged, + registry_file, + auto_unbox = TRUE, + pretty = TRUE, + null = "null" + ) + save_ledger(merge_ledger(load_ledger(), new_ledger_records)) + + git <- function(...) { + st <- system2("git", c(...), stdout = TRUE, stderr = TRUE) + if (!identical(attr(st, "status"), NULL)) { + stop(sprintf( + "git %s failed:\n%s", + paste(..., collapse = " "), + paste(st, collapse = "\n") + )) + } + invisible(st) + } + git("config", "user.name", Sys.getenv("GIT_USER", "devxy-bot")) + git( + "config", + "user.email", + Sys.getenv("GIT_EMAIL", "bot@devxy.io") + ) + git("checkout", "-B", pr_branch) + git( + "add", + file.path(script_dir, "patches", "registry.json"), + ledger_file + ) + git( + "commit", + "-m", + sprintf( + "feat(patches): auto-propose %d registry %s from classified failures", + length(candidate_entries), + if (length(candidate_entries) == 1L) "entry" else "entries" + ) + ) + # Push with a write token when provided (CI); otherwise rely on origin creds. + rw_token <- Sys.getenv("REPO_RW_TOKEN") + push_target <- if (nchar(rw_token) > 0L) { + sprintf( + "https://%s:%s@git.devxy.io/%s.git", + Sys.getenv("GIT_USER", "devxy-bot"), + rw_token, + repo + ) + } else { + "origin" + } + git("push", "-f", push_target, sprintf("HEAD:refs/heads/%s", pr_branch)) + + pr_title <- sprintf( + "feat(patches): auto-proposed registry patches (%s)", + now + ) + body_lines <- c( + sprintf( + "_Auto-generated %s by `local/propose-patches.R --open-pr` from classified `single_builds` failures._", + now + ), + "", + sprintf( + "Adds %d known-lever registry %s (top by failure volume%s).", + length(candidate_entries), + if (length(candidate_entries) == 1L) "entry" else "entries", + if (deferred > 0L) { + sprintf("; %d deferred to a later run", deferred) + } else { + "" + } + ), + "", + "**Merge gate:** the `trial-build-registry` pipeline builds each new entry in its target build-env image; merge only once it is green.", + "Novel source diffs and unknown signatures are never auto-proposed.", + "", + "| package | signature | platforms |", + "| --- | --- | --- |" + ) + for (c in candidates) { + body_lines <- c( + body_lines, + sprintf( + "| %s | %s | %s |", + c$package, + c$signature, + toString(as.character(c$entry$platforms)) + ) + ) + } + new_body <- paste(body_lines, collapse = "\n") + + # One PR per reused branch: update if open, else create. + pulls_url <- sprintf( + "%s/repos/%s/pulls?state=open&limit=50", + forgejo_base, + repo + ) + open_pulls <- httr2::request(pulls_url) |> + httr2::req_headers(Authorization = paste("token", forgejo_token)) |> + httr2::req_perform() |> + httr2::resp_body_json(simplifyVector = FALSE) + match_idx <- which(vapply( + open_pulls, + function(p) identical(p$head$ref, pr_branch), + logical(1L) + )) + if (length(match_idx) > 0L) { + num <- open_pulls[[match_idx[1]]]$number + httr2::request(sprintf("%s/repos/%s/pulls/%d", forgejo_base, repo, num)) |> + httr2::req_headers( + Authorization = paste("token", forgejo_token), + `Content-Type` = "application/json" + ) |> + httr2::req_body_json(list(title = pr_title, body = new_body)) |> + httr2::req_method("PATCH") |> + httr2::req_perform() + cat(sprintf("\nUpdated auto-patch PR #%d (branch %s).\n", num, pr_branch)) + } else { + created <- httr2::request(sprintf( + "%s/repos/%s/pulls", + forgejo_base, + repo + )) |> + httr2::req_headers( + Authorization = paste("token", forgejo_token), + `Content-Type` = "application/json" + ) |> + httr2::req_body_json(list( + title = pr_title, + head = pr_branch, + base = "main", + body = new_body + )) |> + httr2::req_perform() |> + httr2::resp_body_json() + cat(sprintf( + "\nOpened auto-patch PR #%d (branch %s).\n", + created$number, + pr_branch + )) + } } else { cat( - "\nDry run: no changes made. Re-run with --write or --open-issue to act.\n" + "\nDry run: no changes made. Re-run with --write, --open-issue, or --open-pr to act.\n" ) } diff --git a/local/tests/test-proposal-tracking-lib.R b/local/tests/test-proposal-tracking-lib.R index 02a29f1..05cd0e9 100644 --- a/local/tests/test-proposal-tracking-lib.R +++ b/local/tests/test-proposal-tracking-lib.R @@ -162,6 +162,57 @@ test_that("blocked_summary lists each dependency and its dependent count", { expect_true(b[[1L]]$packages_truncated) }) +test_that("entry_applies_to_os matches codename, family, and wildcard", { + expect_true(entry_applies_to_os(list("ubuntu-2604"), "ubuntu-2604")) + expect_true(entry_applies_to_os(list("ubuntu"), "ubuntu-2604")) # family + expect_true(entry_applies_to_os(list("*"), "ubuntu-2604")) + expect_true(entry_applies_to_os(list("alpine", "ubuntu-2604"), "ubuntu-2604")) + expect_false(entry_applies_to_os(list("alpine-324"), "ubuntu-2604")) + expect_false(entry_applies_to_os(list("ubuntu-2404"), "ubuntu-2604")) # other codename +}) + +test_that("new_registry_packages returns only added entries for the platform", { + base <- list( + list( + package = "RcppParallel", + platforms = list("alpine", "ubuntu-2604"), + versions = "*" + ) + ) + current <- list( + base[[1L]], # unchanged -> not "new" + list(package = "BFpack", platforms = list("ubuntu-2604"), versions = "*"), + list( + package = "someAlpinePkg", + platforms = list("alpine-324"), + versions = "*" + ) + ) + # For ubuntu-2604: only the newly-added BFpack (RcppParallel is unchanged, + # someAlpinePkg does not apply to this OS). + expect_identical( + new_registry_packages(current, base, os = "ubuntu-2604"), + "BFpack" + ) + # For alpine-324: the alpine package is new and applies. + expect_identical( + new_registry_packages(current, base, os = "alpine-324"), + "someAlpinePkg" + ) + # Without an OS filter, both additions are returned. + expect_setequal( + new_registry_packages(current, base), + c("BFpack", "someAlpinePkg") + ) + # A changed platform set on the same package counts as a new entry. + widened <- list(list( + package = "RcppParallel", + platforms = list("*"), + versions = "*" + )) + expect_identical(new_registry_packages(widened, base), "RcppParallel") +}) + test_that("retirement_candidates flags entries whose package no longer fails", { entries <- list( list(package = "RcppParallel"), diff --git a/local/trial-build-registry.R b/local/trial-build-registry.R new file mode 100644 index 0000000..f649b62 --- /dev/null +++ b/local/trial-build-registry.R @@ -0,0 +1,145 @@ +#!/usr/bin/env Rscript + +# Merge gate for the auto-patch PR (issue #115, step 3): for every registry +# entry the PR ADDS that applies to this platform, trial-build the package with +# the registry applied, in this platform's own build-env image. Nothing is +# uploaded, archived, or written to the metadata DB. +# +# Exit 0 only if every new entry's package builds; exit 1 if any fails, so it +# gates the PR. A platform with no new entries is a fast no-op. +# +# Usage (inside a build-env image): +# PLATFORM=ubuntu-2604 Rscript local/trial-build-registry.R [base_ref] +# base_ref git ref to diff the registry against (default: origin/main) + +options(error = function() { + cat("ERROR:", geterrmessage(), "\n", file = stdout()) + q(status = 1) +}) + +suppressPackageStartupMessages({ + library(jsonlite, quietly = TRUE) + library(bincraft, quietly = TRUE) +}) + +script_path <- local({ + a <- commandArgs(trailingOnly = FALSE) + f <- sub("^--file=", "", a[grepl("^--file=", a)]) + if (length(f) == 1L && nzchar(f)) normalizePath(f) else NA_character_ +}) +script_dir <- if (is.na(script_path)) "local" else dirname(script_path) +source(file.path(script_dir, "proposal-tracking-lib.R")) + +args <- commandArgs(trailingOnly = TRUE) +base_ref <- if (length(args) >= 1L) { + args[[1L]] +} else { + Sys.getenv("BASE_REF", "origin/main") +} +os <- Sys.getenv("PLATFORM", "") +if (!nzchar(os)) { + stop("PLATFORM env var is not set (e.g. ubuntu-2604).") +} + +patches_dir <- file.path(script_dir, "patches") +registry_file <- file.path(patches_dir, "registry.json") +current <- if (file.exists(registry_file)) { + jsonlite::fromJSON(registry_file, simplifyVector = FALSE) +} else { + list() +} +# Read the registry at base_ref. Fail loud if the ref or file can't be read: +# silently treating the base as empty would trial-build the WHOLE registry +# instead of just the entries the branch adds. +registry_rel <- "local/patches/registry.json" +ref_ok <- suppressWarnings(system2( + "git", + c("rev-parse", "--verify", "--quiet", sprintf("%s^{commit}", base_ref)), + stdout = TRUE, + stderr = FALSE +)) +if (!is.null(attr(ref_ok, "status"))) { + stop(sprintf("base ref %s does not resolve to a commit.", base_ref)) +} +in_base <- suppressWarnings(system2( + "git", + c("ls-tree", base_ref, "--", registry_rel), + stdout = TRUE, + stderr = FALSE +)) +file_in_base <- length(in_base) > 0L && any(nzchar(in_base)) +base_json <- suppressWarnings(system2( + "git", + c("show", sprintf("%s:%s", base_ref, registry_rel)), + stdout = TRUE, + stderr = FALSE +)) +show_ok <- is.null(attr(base_json, "status")) +if (file_in_base && !show_ok) { + stop(sprintf( + "could not read %s at %s; refusing to build the whole registry.", + registry_rel, + base_ref + )) +} +base <- if (show_ok && length(base_json) > 0L) { + jsonlite::fromJSON(paste(base_json, collapse = "\n"), simplifyVector = FALSE) +} else { + list() # file genuinely absent at base -> every entry is new +} + +pkgs <- new_registry_packages(current, base, os = os) +if (length(pkgs) == 0L) { + cat(sprintf( + "No new registry entries apply to %s; nothing to trial-build.\n", + os + )) + q(status = 0) +} + +cat(sprintf( + "Trial-building %d new registry %s on %s (vs %s):\n %s\n", + length(pkgs), + if (length(pkgs) == 1L) "entry" else "entries", + os, + base_ref, + toString(pkgs) +)) + +results <- vapply( + pkgs, + function(pkg) { + cat(sprintf("\n=== trial build: %s ===\n", pkg)) + tryCatch( + { + bincraft::build_binary_package( + pkg, + tag_limit = 1L, + patches = patches_dir, + archive = FALSE, + upload = FALSE, + store_build_metadata = FALSE + ) + TRUE + }, + error = function(e) { + cat(sprintf("FAILED %s: %s\n", pkg, conditionMessage(e))) + FALSE + } + ) + }, + logical(1L) +) + +failed <- pkgs[!results] +cat(sprintf( + "\n%d/%d passed on %s.%s\n", + sum(results), + length(results), + os, + if (length(failed) > 0L) sprintf(" Failed: %s", toString(failed)) else "" +)) +if (length(failed) > 0L) { + q(status = 1) +} +q(status = 0)