From ca336c2720486d48fcc0fb76e84b6e1ebdb033c4 Mon Sep 17 00:00:00 2001 From: pat-s Date: Mon, 29 Jun 2026 12:11:03 +0000 Subject: [PATCH 1/4] fix(ci): detect existing R versions via public object HEAD probe (#8) The existing-version check listed the bucket with an anonymous curl, but Backblaze B2 requires authentication for the list-bucket API, so it returned AccessDenied and r-versions-existing.txt was always empty -> every version was rebuilt despite already existing in s3. Signing the request is not workable here: the B2 credentials are scoped to the plugin-s3 image in crow and are not injected into this plain alpine step (curl saw --user ":"). Individual artifacts are public-read, so probe each expected object URL with an anonymous HEAD instead of listing the bucket. The filename mirrors what nfpm produces (version/release pinned to 1): r-_1_.{apk,deb} for alpine/ubuntu and R--1-1..rpm for el*. Verified against the live bucket for all three packager families. Reviewed-on: https://git.devxy.io/devxy/r-builds/pulls/8 --- .crow/build.yaml | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.crow/build.yaml b/.crow/build.yaml index b3b6c0e..c980b4b 100644 --- a/.crow/build.yaml +++ b/.crow/build.yaml @@ -158,28 +158,27 @@ steps: - name: Check which R versions already exist in s3 image: reg.devxy.io/docker.io/library/alpine:3.23 privileged: true - environment: - AWS_ACCESS_KEY_ID: - from_secret: B2_S3_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY: - from_secret: B2_S3_SECRET_KEY commands: - ip link set dev eth0 mtu 1280 2>/dev/null || true - for i in 1 2 3 4 5; do apk add -q --no-cache curl && break; sleep 5; done - | - # Backblaze B2 requires authentication for the list-bucket API (anonymous - # GET works only for individual public-read objects), so the request must be - # SigV4-signed with the same credentials used for the upload step. Without - # this the listing returns AccessDenied, r-versions-existing.txt stays empty, - # and every version is rebuilt even though it already exists. - LISTING=$(curl -s --aws-sigv4 "aws:amz:eu-central-003:s3" \ - --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \ - "https://s3.eu-central-003.backblazeb2.com/devxy-r-builds?prefix=${PLATFORM_ID}/" | \ - grep -oE '[^<]+' | sed 's/<[^>]*>//g') + # Backblaze B2's list-bucket API requires authentication, but the upload + # credentials are scoped to the plugin-s3 image and aren't available here. + # Individual objects are public-read, so probe each expected artifact URL + # with an anonymous HEAD instead of listing the bucket. The filename mirrors + # what nfpm produces in builder/package.${PLATFORM} (version/release pinned + # to 1): r-_1_.{apk,deb} for alpine/ubuntu, R--1-1..rpm + # for el*. + case "${PLATFORM}" in + alpine-*) FILE="r-VER_1_${ARCH_ID}.apk" ;; + ubuntu-*) FILE="r-VER_1_${ARCH_ID}.deb" ;; + *) FILE="R-VER-1-1.${ARCH_ID}.rpm" ;; + esac + BASE="https://s3.eu-central-003.backblazeb2.com/devxy-r-builds/${PLATFORM_ID}" : > r-versions-existing.txt for VERSION in $(cat r-versions-to-build.txt); do - if printf '%s\n' "$LISTING" | \ - awk -v arch="${ARCH_ID}" -v ver="$VERSION" 'index($0, arch) && index($0, ver) { found=1 } END { exit !found }'; then + URL="${BASE}/$(echo "$FILE" | sed "s/VER/${VERSION}/")" + if [ "$(curl -s -o /dev/null -w '%{http_code}' -I "$URL")" = "200" ]; then echo "$VERSION" >> r-versions-existing.txt fi done From 12aa310b8d4f2e403c75fbb442456387c649be8e Mon Sep 17 00:00:00 2001 From: pat-s Date: Mon, 29 Jun 2026 13:00:26 +0200 Subject: [PATCH 2/4] fix(ci): detect existing R versions via public object HEAD probe The existing-version check listed the bucket with an anonymous curl, but Backblaze B2 requires authentication for the list-bucket API, so it returned AccessDenied and r-versions-existing.txt was always empty -> every version was rebuilt despite already existing in s3. Signing the request is not workable here: the B2 credentials are scoped to the plugin-s3 image in crow and are not injected into this plain alpine step. Individual artifacts are public-read, so probe each expected object URL with an anonymous HEAD instead. The filename mirrors what nfpm produces (version and release pinned to 1): r-_1_.{apk,deb} for alpine/ubuntu and R--1-1..rpm for el*. Verified against the live bucket for all three packager families. Shell variables use bare $name rather than ${name}: crow runs its own ${VAR} substitution over the commands before the shell executes, so a ${VAR} naming a shell var (BASE, VERSION) was blanked to empty -- only matrix vars resolve at that stage. Build the filename with printf to avoid the ${VERSION}_1 brace requirement entirely. --- .crow/build.yaml | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/.crow/build.yaml b/.crow/build.yaml index b3b6c0e..18f126f 100644 --- a/.crow/build.yaml +++ b/.crow/build.yaml @@ -158,28 +158,29 @@ steps: - name: Check which R versions already exist in s3 image: reg.devxy.io/docker.io/library/alpine:3.23 privileged: true - environment: - AWS_ACCESS_KEY_ID: - from_secret: B2_S3_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY: - from_secret: B2_S3_SECRET_KEY commands: - ip link set dev eth0 mtu 1280 2>/dev/null || true - for i in 1 2 3 4 5; do apk add -q --no-cache curl && break; sleep 5; done - | - # Backblaze B2 requires authentication for the list-bucket API (anonymous - # GET works only for individual public-read objects), so the request must be - # SigV4-signed with the same credentials used for the upload step. Without - # this the listing returns AccessDenied, r-versions-existing.txt stays empty, - # and every version is rebuilt even though it already exists. - LISTING=$(curl -s --aws-sigv4 "aws:amz:eu-central-003:s3" \ - --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \ - "https://s3.eu-central-003.backblazeb2.com/devxy-r-builds?prefix=${PLATFORM_ID}/" | \ - grep -oE '[^<]+' | sed 's/<[^>]*>//g') + # Backblaze B2's list-bucket API requires authentication, but the upload + # credentials are scoped to the plugin-s3 image and aren't available here. + # Individual objects are public-read, so probe each expected artifact URL + # with an anonymous HEAD instead of listing the bucket. The filename mirrors + # what nfpm produces in builder/package.${PLATFORM} (version/release pinned + # to 1): r-_1_.{apk,deb} for alpine/ubuntu, R--1-1..rpm + # for el*. Shell variables must use bare $name, not ${name}: crow performs + # its own ${VAR} substitution on these commands before the shell runs, so + # ${VAR} for a shell var would be blanked out (only matrix vars resolve there). + case "${PLATFORM}" in + alpine-*) FMT="r-%s_1_%s.apk" ;; + ubuntu-*) FMT="r-%s_1_%s.deb" ;; + *) FMT="R-%s-1-1.%s.rpm" ;; + esac + BASE="https://s3.eu-central-003.backblazeb2.com/devxy-r-builds/${PLATFORM_ID}" : > r-versions-existing.txt for VERSION in $(cat r-versions-to-build.txt); do - if printf '%s\n' "$LISTING" | \ - awk -v arch="${ARCH_ID}" -v ver="$VERSION" 'index($0, arch) && index($0, ver) { found=1 } END { exit !found }'; then + URL="$BASE/$(printf "$FMT" "$VERSION" "${ARCH_ID}")" + if [ "$(curl -s -o /dev/null -w '%{http_code}' -I "$URL")" = "200" ]; then echo "$VERSION" >> r-versions-existing.txt fi done From 8eacc0ab47fc45a93e0658d2d8193966fbd36e0c Mon Sep 17 00:00:00 2001 From: pat-s Date: Mon, 29 Jun 2026 14:15:55 +0200 Subject: [PATCH 3/4] fix(ci): use bare shell vars in existing-version check (crow ${VAR} substitution) The merged HEAD-probe check still rebuilt everything: crow runs its own ${VAR} substitution over the commands before the shell executes, so the shell variables ${BASE} and ${VERSION} were blanked to empty (only matrix vars resolve at that stage). Every probed URL was therefore malformed and returned non-200, leaving r-versions-existing.txt empty. Use bare $name for shell variables and build the filename with printf, which avoids the ${VERSION}_1 brace requirement entirely. Matrix vars (${PLATFORM}, ${PLATFORM_ID}, ${ARCH_ID}) keep braces since crow resolves those correctly. Verified URL construction against the live bucket for apk, deb and rpm. --- .crow/build.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.crow/build.yaml b/.crow/build.yaml index c980b4b..18f126f 100644 --- a/.crow/build.yaml +++ b/.crow/build.yaml @@ -168,16 +168,18 @@ steps: # with an anonymous HEAD instead of listing the bucket. The filename mirrors # what nfpm produces in builder/package.${PLATFORM} (version/release pinned # to 1): r-_1_.{apk,deb} for alpine/ubuntu, R--1-1..rpm - # for el*. + # for el*. Shell variables must use bare $name, not ${name}: crow performs + # its own ${VAR} substitution on these commands before the shell runs, so + # ${VAR} for a shell var would be blanked out (only matrix vars resolve there). case "${PLATFORM}" in - alpine-*) FILE="r-VER_1_${ARCH_ID}.apk" ;; - ubuntu-*) FILE="r-VER_1_${ARCH_ID}.deb" ;; - *) FILE="R-VER-1-1.${ARCH_ID}.rpm" ;; + alpine-*) FMT="r-%s_1_%s.apk" ;; + ubuntu-*) FMT="r-%s_1_%s.deb" ;; + *) FMT="R-%s-1-1.%s.rpm" ;; esac BASE="https://s3.eu-central-003.backblazeb2.com/devxy-r-builds/${PLATFORM_ID}" : > r-versions-existing.txt for VERSION in $(cat r-versions-to-build.txt); do - URL="${BASE}/$(echo "$FILE" | sed "s/VER/${VERSION}/")" + URL="$BASE/$(printf "$FMT" "$VERSION" "${ARCH_ID}")" if [ "$(curl -s -o /dev/null -w '%{http_code}' -I "$URL")" = "200" ]; then echo "$VERSION" >> r-versions-existing.txt fi From 56d26ef59c4077e7f5222816b0a95a43ab3df4bc Mon Sep 17 00:00:00 2001 From: pat-s Date: Thu, 2 Jul 2026 08:43:18 +0200 Subject: [PATCH 4/4] docs: add CLAUDE.md with B2 storage gotchas and CI workflow conventions for agents --- CLAUDE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..911139b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,13 @@ +# CLAUDE.md + +CI pipelines (Crow, in `.crow/`) that build R binaries and upload them to Backblaze B2. + +## Conventions + +- **PRs:** the remote is Forgejo on `git.devxy.io`; use `fj -H git.devxy.io` (not `gh`). +- **Storage:** Backblaze B2 bucket `devxy-r-builds` (endpoint configured in `.crow/build.yaml`). + +## Gotchas + +- **B2 requires authentication for the list-bucket API.** Anonymous GET only works for individual public-read objects — an empty bucket listing usually means missing credentials, not an empty bucket. This silently broke rebuild dedup once. +- When a pipeline fails, fetch the Crow logs yourself instead of asking the user to paste them, and monitor reruns in the background until green.