From 0d2f78c09404d36c244c43b4034f51e36cc47d56 Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Fri, 14 Aug 2026 11:55:42 +0700 Subject: [PATCH 1/7] feat: matrix-based release pipeline with automatic Node version updates Resolve #43, #38, #35, #31, and #24 by replacing the single-builder workflow with a dynamic matrix pipeline: - check-bun-node.ts gains --sync/--matrix/--node modes backed by @nodevu/core: supported Node majors are derived from the release schedule (not hardcoded), EOL majors are removed from src/, and Dockerfiles for new majors are generated from templates/ using the newest Alpine tag actually available on Docker Hub (#43). Dockerfile template changes mark majors for rebuild via _needs_rebuild. - versions.json state moves to the GitHub Release 'versions' asset instead of being committed to the repo (#35). - the release workflow builds each combo in parallel via build_single.sh (which passes the correct BUN_VERSION build-arg, fixing canary builds), then a finalize job merges results, re-points the flat 'latest' tag to the newest candidate via imagetools create only after all builds succeed (#38), and publishes a summary of only the combos that actually changed (#31). - docs/research_matrix.md records why the matrix approach wins (#24). - Dockerfiles are normalized to bookworm/alpine-3.22+ bases via templates; curl calls enforce https on redirects; the bun-as-node PATH shim is dropped. - removed dead tooling: build_updated.sh, commit_changes.sh, merge_lcov.sh, empty test stubs, pnpm-lock.yaml. - workflow hardened per SonarCloud: inputs reach run blocks only via env (no script injection), secrets via env only, permissions scoped per job, sonar-project.properties documents false-positive rule exclusions (docker:S6506, githubactions:S8543/S6505). - new web/ Cloudflare Worker serves public stats at bun-node.imbios.dev (pulls, tags, stars, badges, private owner page) with a daily seed hook into the release workflow. --- .github/workflows/ci.yml | 9 - .github/workflows/release.yml | 210 +++++-- .gitignore | 2 + build_single.sh | 150 +++++ build_updated.sh | 170 ------ build_updated_test.sh | 1 - bun.lock | 37 ++ check-bun-node.ts | 536 +++++++++++++----- commit_changes.sh | 26 - commit_changes_test.sh | 34 -- docs/research_matrix.md | 40 ++ merge_lcov.sh | 27 - merge_lcov_test.sh | 1 - pnpm-lock.yaml | 101 ---- readme.md | 34 ++ sonar-project.properties | 14 + src/base/22/alpine/dockerfile | 10 +- src/base/22/debian-slim/dockerfile | 10 +- src/base/22/debian/dockerfile | 6 +- src/base/24/alpine/dockerfile | 8 +- src/base/24/debian-slim/dockerfile | 12 +- src/base/24/debian/dockerfile | 8 +- src/base/25/debian/docker-entrypoint.sh | 8 - .../{20 => 26}/alpine/docker-entrypoint.sh | 0 src/base/{25 => 26}/alpine/dockerfile | 8 +- .../debian-slim/docker-entrypoint.sh | 0 src/base/{25 => 26}/debian-slim/dockerfile | 12 +- .../{20 => 26}/debian/docker-entrypoint.sh | 0 src/base/{25 => 26}/debian/dockerfile | 8 +- src/git/20/alpine/docker-entrypoint.sh | 8 - src/git/22/alpine/dockerfile | 10 +- src/git/24/alpine/dockerfile | 8 +- src/git/25/alpine/docker-entrypoint.sh | 8 - .../25 => git/26}/alpine/docker-entrypoint.sh | 0 src/git/{25 => 26}/alpine/dockerfile | 8 +- .../alpine-git.dockerfile | 10 +- .../dockerfile => templates/alpine.dockerfile | 10 +- .../debian-slim.dockerfile | 12 +- .../dockerfile => templates/debian.dockerfile | 8 +- .../docker-entrypoint.sh | 0 versions.json | 24 - web/bun.lock | 198 +++++++ web/cloudflare.config.ts | 17 + web/package.json | 10 + web/src/index.ts | 404 +++++++++++++ 45 files changed, 1518 insertions(+), 699 deletions(-) create mode 100755 build_single.sh delete mode 100755 build_updated.sh delete mode 100755 build_updated_test.sh create mode 100644 bun.lock delete mode 100755 commit_changes.sh delete mode 100644 commit_changes_test.sh create mode 100644 docs/research_matrix.md delete mode 100755 merge_lcov.sh delete mode 100755 merge_lcov_test.sh delete mode 100644 pnpm-lock.yaml create mode 100644 sonar-project.properties delete mode 100755 src/base/25/debian/docker-entrypoint.sh rename src/base/{20 => 26}/alpine/docker-entrypoint.sh (100%) rename src/base/{25 => 26}/alpine/dockerfile (88%) rename src/base/{20 => 26}/debian-slim/docker-entrypoint.sh (100%) rename src/base/{25 => 26}/debian-slim/dockerfile (87%) rename src/base/{20 => 26}/debian/docker-entrypoint.sh (100%) rename src/base/{25 => 26}/debian/dockerfile (89%) delete mode 100755 src/git/20/alpine/docker-entrypoint.sh delete mode 100755 src/git/25/alpine/docker-entrypoint.sh rename src/{base/25 => git/26}/alpine/docker-entrypoint.sh (100%) rename src/git/{25 => 26}/alpine/dockerfile (89%) rename src/git/20/alpine/dockerfile => templates/alpine-git.dockerfile (86%) rename src/base/20/alpine/dockerfile => templates/alpine.dockerfile (86%) rename src/base/20/debian-slim/dockerfile => templates/debian-slim.dockerfile (84%) rename src/base/20/debian/dockerfile => templates/debian.dockerfile (87%) rename {src/base/25/debian-slim => templates}/docker-entrypoint.sh (100%) mode change 100755 => 100644 delete mode 100644 versions.json create mode 100644 web/bun.lock create mode 100644 web/cloudflare.config.ts create mode 100644 web/package.json create mode 100644 web/src/index.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c05e420..d9df195 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,18 +22,9 @@ jobs: - name: Checkout code uses: actions/checkout@v5 - # - name: Set up bashcov - # uses: infertux/bashcov/.github/actions/set-up-bashcov@master - - name: Install lcov run: sudo apt-get update && sudo apt-get install -y lcov - # - name: Run tests with coverage - # run: | - # pipenv run pytest --cov --cov-report=lcov - # # bashcov ./commit_changes_test.sh - # # ./merge_lcov.sh src merged.lcov - - name: Coverage Badge uses: ImBIOS/lcov-coverage-badge@b548b874a74d1c0bb832498745ff98ccb6a81430 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 274c214..068fece 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,59 +2,105 @@ name: Release on: schedule: - - cron: "0 0 * * *" # Every day at midnight + - cron: "0 0 * * *" workflow_dispatch: inputs: bun-versions: - description: "Bun version, comma separated (e.g. 0.0.1,0.0.2,1.0.8-canary.20231104.1)" + description: "Bun version, comma separated (e.g. 0.0.1,1.0.8-canary.20231104.1)" required: false default: "" nodejs-version: - description: "Node.js version, comma separated (e.g. 14.7.4,17.3.8,18.4.5)" + description: "Node.js version, comma separated (e.g. 18.4.5,20.11.0)" required: false default: "" distros: description: "Distro, comma separated (e.g. alpine,debian-slim,debian)" required: false default: "" - # TODO: To be implemented - # skip-check: - # description: "Skip version check" - # required: false - # default: "true" env: REGISTRY: imbios PLATFORMS: linux/amd64,linux/arm64 - NODE_MAJOR_VERSIONS_TO_CHECK: 20,22,24,25 - NODE_VERSIONS_TO_BUILD: "" BUN_TAGS_TO_CHECK: canary,latest - BUN_VERSIONS_TO_BUILD: "" - DISTROS: alpine,debian-slim,debian + DISTROS: ${{ inputs.distros || 'alpine,debian-slim,debian' }} + VERSIONS_RELEASE: versions + INPUT_BUN_VERSIONS: ${{ inputs.bun-versions }} + INPUT_NODE_VERSIONS: ${{ inputs.nodejs-version }} jobs: - # TODO: To be implemented - # test-job: - # uses: ./.github/workflows/ci.yml - build-job: - # TODO: To be implemented - # needs: test-job + setup: runs-on: ubuntu-latest + permissions: + contents: write + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} steps: - - name: Checkout code - uses: actions/checkout@v5 + - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@635640504f6d7197d3bb29876a652f671028dc97 - - run: bun install + - run: bun install --frozen-lockfile --ignore-scripts - - name: Check for new releases of nodejs and bun - if: ${{ inputs.nodejs-version == '' && inputs.bun-versions == '' && inputs.distros == '' }} + - name: Download version state run: | - set -e - echo "NODE_VERSIONS_TO_BUILD=$(bun run check-bun-node.ts --node ${{ env.NODE_MAJOR_VERSIONS_TO_CHECK }})" >> $GITHUB_ENV - echo "BUN_VERSIONS_TO_BUILD=$(bun run check-bun-node.ts --bun ${{ env.BUN_TAGS_TO_CHECK }})" >> $GITHUB_ENV + gh release download "${{ env.VERSIONS_RELEASE }}" -p versions.json || echo '{}' > versions.json + cat versions.json + env: + GH_TOKEN: ${{ github.token }} + + - name: Seed stats cache for the stats site + continue-on-error: true + env: + STATS_SEED: ${{ secrets.BUN_NODE_STATS_SEED }} + run: | + DOCKER=$(curl -fsS --proto '=https' --tlsv1.2 "https://hub.docker.com/v2/repositories/imbios/bun-node/") + TAGS=$(curl -fsS --proto '=https' --tlsv1.2 "https://hub.docker.com/v2/repositories/imbios/bun-node/tags/?page_size=1") + GH=$(curl -fsS --proto '=https' --tlsv1.2 "https://api.github.com/repos/ImBIOS/bun-node") + jq -n \ + --argjson d "$DOCKER" \ + --argjson t "$TAGS" \ + --argjson g "$GH" \ + '{docker: {pull_count: $d.pull_count, star_count: $d.star_count, last_updated: $d.last_updated, count: $t.count}, github: {stargazers_count: $g.stargazers_count, forks_count: $g.forks_count, open_issues_count: $g.open_issues_count, pushed_at: $g.pushed_at}}' \ + > /tmp/stats-seed.json + curl -fsS --proto '=https' --tlsv1.2 -X POST \ + -H "Authorization: Bearer ${STATS_SEED}" \ + -H "Content-Type: application/json" \ + --data-binary @/tmp/stats-seed.json \ + https://bun-node.imbios.dev/internal/seed - - name: Setup Docker Buildx + - name: Sync supported Node.js majors + run: | + bun run check-bun-node.ts --sync --versions versions.json + if [[ -n "$(git status --porcelain)" ]]; then + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add src templates + git commit -m "chore: sync Dockerfiles with supported Node.js majors" + git push origin main + fi + + - name: Generate build matrix + id: matrix + env: + DISTROS: ${{ env.DISTROS }} + INPUT_BUN_VERSIONS: ${{ env.INPUT_BUN_VERSIONS }} + INPUT_NODE_VERSIONS: ${{ env.INPUT_NODE_VERSIONS }} + run: | + MATRIX=$(bun run check-bun-node.ts --matrix --versions versions.json) + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + echo "matrix=$MATRIX" + + build: + needs: setup + runs-on: ubuntu-latest + if: ${{ fromJson(needs.setup.outputs.matrix).include[0] != null }} + strategy: + fail-fast: false + max-parallel: 12 + matrix: ${{ fromJson(needs.setup.outputs.matrix) }} + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub @@ -64,37 +110,107 @@ jobs: password: ${{ secrets.DOCKER_TOKEN }} - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 - name: Build and push Docker images + name: Build and push with: - timeout_minutes: 120 + timeout_minutes: 90 max_attempts: 3 retry_on: error - command: ./build_updated.sh + command: ./build_single.sh --bun "${{ matrix.bun_version }}" --node "${{ matrix.node_version }}" --node-major "${{ matrix.node_major }}" --codename "${{ matrix.codename }}" --distro "${{ matrix.distro }}" --latest-candidate "${{ matrix.latest_candidate }}" env: REGISTRY: ${{ env.REGISTRY }} PLATFORMS: ${{ env.PLATFORMS }} - NODE_VERSIONS_TO_BUILD: ${{ env.NODE_VERSIONS_TO_BUILD || inputs.nodejs-version }} - BUN_VERSIONS_TO_BUILD: ${{ env.BUN_VERSIONS_TO_BUILD || inputs.bun-versions }} - DISTROS: ${{ env.DISTROS || inputs.distros }} - - name: Commit changes - run: ./commit_changes.sh + - name: Upload build result + uses: actions/upload-artifact@v4 + with: + name: build-success-${{ matrix.node_major }}-${{ matrix.bun_version }}-${{ matrix.distro }} + path: build_success.json + if-no-files-found: error + + finalize: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + if: ${{ needs.build.result == 'success' }} + steps: + - uses: actions/checkout@v5 + + - name: Download version state + run: | + gh release download "${{ env.VERSIONS_RELEASE }}" -p versions.json || echo '{}' > versions.json env: - BUN_VERSIONS_TO_BUILD: ${{ env.BUN_VERSIONS_TO_BUILD }} - NODE_VERSIONS_TO_BUILD: ${{ env.NODE_VERSIONS_TO_BUILD }} - DISTROS: ${{ env.DISTROS }} - - name: Pull changes - run: git pull -r - - name: Push changes - uses: ad-m/github-push-action@77c5b412c50b723d2a4fbc6d71fb5723bcd439aa + GH_TOKEN: ${{ github.token }} + + - name: Download build results + uses: actions/download-artifact@v4 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + pattern: build-success-* + merge-multiple: true + path: updates + + - name: Merge version state + run: | + ls -la updates + jq -s 'reduce .[] as $u ({}; . * $u)' versions.json updates/*.json > versions.json.tmp + jq 'del(._needs_rebuild)' versions.json.tmp > versions.json + rm -f versions.json.tmp + cat versions.json + + - name: Point latest at the newest candidate + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} + run: | + candidate=$(jq -s 'map(select(.latest_candidate == true)) | .[0]' updates/*.json) + if [ "$candidate" != "null" ]; then + bun_version=$(echo "$candidate" | jq -r '.bun.latest | sub("^v"; "")') + node_version=$(echo "$candidate" | jq -r '.nodejs | to_entries[0].value.version | sub("^v"; "")') + docker login -u "${DOCKER_USERNAME}" -p "${DOCKER_TOKEN}" + docker buildx imagetools create --tag "${{ env.REGISTRY }}/bun-node:latest" "${{ env.REGISTRY }}/bun-node:${bun_version}-${node_version}-debian" + else + echo "no latest candidate in this run, skipping" + fi + + - name: Upload version state + run: | + gh release create "${{ env.VERSIONS_RELEASE }}" versions.json --title "Versions State" --notes "" || \ + gh release upload "${{ env.VERSIONS_RELEASE }}" versions.json --clobber + env: + GH_TOKEN: ${{ github.token }} + + - name: Write release summary + run: | + { + echo "## Release summary" + echo "" + echo "Built $(ls updates | wc -l) image combination(s):" + echo "" + for file in updates/*.json; do + bun=$(jq -r '.bun | to_entries[] | "\(.key)=\(.value)"' "$file" | tr '\n' ' ') + node=$(jq -r '.nodejs | to_entries[] | "node\(.key)=\(.value.version)"' "$file" | tr '\n' ' ') + echo "- $node | $bun" + done + } >> "$GITHUB_STEP_SUMMARY" + gh release edit "${{ env.VERSIONS_RELEASE }}" --notes "$(cat "$GITHUB_STEP_SUMMARY")" + env: + GH_TOKEN: ${{ github.token }} + rerun-failed-jobs: runs-on: ubuntu-latest - needs: [build-job] - if: failure() + needs: [build] + permissions: + actions: write + if: ${{ failure() && needs.build.result == 'failure' }} steps: - - name: Rerun failed jobs in the current workflow + - name: Rerun failed build jobs env: GH_TOKEN: ${{ github.token }} - run: gh run rerun ${{ github.run_id }} --repo ${{ github.repository }} --failed + run: | + failed=$(gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json jobs \ + --jq '[.jobs[] | select(.name == "build" and .conclusion == "failure") | .databaseId] | join(" ")') + if [ -n "$failed" ]; then + for job in $failed; do + gh run rerun ${{ github.run_id }} --repo ${{ github.repository }} --job "$job" || true + done + fi diff --git a/.gitignore b/.gitignore index 126a51a..587ed93 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ coverage.info coverage/ node_modules/ +.wrangler/ +.cloudflare/ diff --git a/build_single.sh b/build_single.sh new file mode 100755 index 0000000..ae3879a --- /dev/null +++ b/build_single.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Build and push a single bun-node image combination. +# +# Usage: +# ./build_single.sh --bun --node --node-major \ +# --codename --distro [--latest-candidate true|false] +# +# All version tags for the combination are pushed. The flat `latest` tag is +# intentionally NOT pushed here: it is re-pointed to the newest candidate in a +# dedicated finalize job so it is always the last tag touched in a release. + +set -e + +log() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*" +} + +retry() { + local retries=${RETRIES:-3} + local count=0 + until "$@"; do + local exit_code=$? + count=$((count + 1)) + if [ "$count" -lt "$retries" ]; then + log "Retrying ($count/$retries)..." + sleep 5 + else + log "Failed after $count attempts." + return "$exit_code" + fi + done + return 0 +} + +LATEST_CANDIDATE=false + +while [[ "$#" -gt 0 ]]; do + case $1 in + --bun) BUN_VERSION="$2"; shift ;; + --node) NODE_VERSION="$2"; shift ;; + --node-major) NODE_MAJOR="$2"; shift ;; + --codename) CODENAME="$2"; shift ;; + --distro) DISTRO="$2"; shift ;; + --bun-tag) BUN_TAG="$2"; shift ;; + --latest-candidate) LATEST_CANDIDATE="$2"; shift ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done + +if [ -z "${BUN_VERSION:-}" ] || [ -z "${NODE_VERSION:-}" ] || [ -z "${NODE_MAJOR:-}" ] || [ -z "${DISTRO:-}" ]; then + echo "Usage: $0 --bun --node --node-major --distro [--codename ] [--bun-tag ] [--latest-candidate true|false]" + exit 1 +fi + +BUN_TAG=${BUN_TAG:-latest} +if [[ "$BUN_VERSION" == *"-canary"* ]]; then + BUN_TAG="canary" +fi + +REGISTRY=${REGISTRY:-imbios} +PLATFORMS=${PLATFORMS:-linux/amd64,linux/arm64} +CODENAME=${CODENAME:-} + +tag_distro="$DISTRO" +if [ "$DISTRO" == "debian-slim" ]; then + tag_distro="slim" +fi + +generate_tags() { + local bun_version=$1 + local node_version=$2 + local distro=$3 + + local node_minor=${node_version%.*} + local bun_major=${bun_version%%.*} + local bun_minor=${bun_version%.*} + local is_canary=false + + if [ "$bun_version" == "canary" ]; then + is_canary=true + fi + + echo "$REGISTRY/bun-node:${bun_version}-${node_version}-${distro}" + + if [ "$is_canary" == false ]; then + echo "$REGISTRY/bun-node:${bun_minor}-${node_version}-${distro}" + echo "$REGISTRY/bun-node:${bun_major}-${node_version}-${distro}" + echo "$REGISTRY/bun-node:${bun_version}-${node_minor}-${distro}" + echo "$REGISTRY/bun-node:${bun_version}-${NODE_MAJOR}-${distro}" + else + echo "$REGISTRY/bun-node:canary-${node_minor}-${distro}" + echo "$REGISTRY/bun-node:canary-${NODE_MAJOR}-${distro}" + fi + + if [ -n "$CODENAME" ]; then + echo "$REGISTRY/bun-node:${bun_version}-${CODENAME}-${distro}" + if [ "$is_canary" == false ]; then + echo "$REGISTRY/bun-node:latest-${CODENAME}-${distro}" + fi + fi + + if [ "$is_canary" == false ]; then + echo "$REGISTRY/bun-node:latest-${node_version}-${distro}" + echo "$REGISTRY/bun-node:latest-${NODE_MAJOR}-${distro}" + echo "$REGISTRY/bun-node:${NODE_MAJOR}-${distro}" + fi +} + +bun_build_arg="$BUN_VERSION" +if [[ "$BUN_VERSION" == *"-canary"* ]]; then + bun_build_arg="canary" +fi + +log "Building image for Bun version $BUN_VERSION, Node version $NODE_VERSION, Distro $DISTRO" +image_name="$REGISTRY/bun-node:${BUN_VERSION}-${NODE_VERSION}-${tag_distro}" +tags=($(generate_tags "$BUN_VERSION" "$NODE_VERSION" "$tag_distro")) + +for tag in "${tags[@]}"; do + log "Tagging $image_name as $tag" + retry docker buildx build \ + --sbom=true --provenance=true \ + --platform "$PLATFORMS" \ + -t "$image_name" -t "$tag" \ + --build-arg BUN_VERSION="$bun_build_arg" \ + "./src/base/${NODE_MAJOR}/${DISTRO}" \ + --push + + if [ "$DISTRO" == "alpine" ]; then + log "Building and Tagging Alpine image with Git" + retry docker buildx build \ + --sbom=true --provenance=true \ + --platform "$PLATFORMS" \ + -t "$image_name-git" -t "$tag-git" \ + --build-arg BUN_VERSION="$bun_build_arg" \ + "./src/git/${NODE_MAJOR}/alpine" \ + --push + fi +done + +cat > build_success.json <". - bun_build_arg="$bun_version" - if [[ $bun_version == *"-canary"* ]]; then - bun_build_arg="canary" - fi - - for tag in "${tags[@]}"; do - log "Tagging $image_name as $tag" - retry docker buildx build --sbom=true --provenance=true --platform "$PLATFORMS" -t "$image_name" -t "$tag" --build-arg BUN_VERSION="$bun_build_arg" "./src/base/${node_major}/${distro}" --push --provenance=mode=max - - if [ "$distro" == "alpine" ]; then - log "Building and Tagging Alpine image with Git" - retry docker buildx build --sbom=true --provenance=true --platform "$PLATFORMS" -t "$image_name-git" -t "$tag-git" --build-arg BUN_VERSION="$bun_build_arg" "./src/git/${node_major}/${distro}" --push --provenance=mode=max - fi - done - - log "Updating versions.json file" - bun_tag="latest" - if [[ $bun_version == *"-canary"* ]]; then - bun_tag="canary" - fi - json_data=$(echo "${json_data}" | jq ".nodejs.\"${node_major}\".version = \"v${node_version}\"" | jq ".bun.\"${bun_tag}\" = \"v${bun_version}\"") - echo "${json_data}" >versions.json - done - done -done diff --git a/build_updated_test.sh b/build_updated_test.sh deleted file mode 100755 index 8b13789..0000000 --- a/build_updated_test.sh +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..7bfc5da --- /dev/null +++ b/bun.lock @@ -0,0 +1,37 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "bun-node", + "dependencies": { + "@nodevu/core": "^0.3.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@nodevu/core": ["@nodevu/core@0.3.0", "", { "dependencies": { "@nodevu/parsefiles": "^0.0.3", "luxon": "^3.5.0", "semver": "^7.6.3" } }, "sha512-kVJh6kQViCE8Qf1j7pc7yhUke2kKb1eZM+rqln+EytHqyOq+eZUEtfWbDk1vfWhgiknnAcFOhdVA5apT8rTejQ=="], + + "@nodevu/parsefiles": ["@nodevu/parsefiles@0.0.3", "", {}, "sha512-IjwkVqA2SlH8XweoAw7EJkeyPGk7gW9imlvhpywrKiPCLP1H0meG/blNAF5X38pirsmvhxPe/BCyywT7Exwuow=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/check-bun-node.ts b/check-bun-node.ts index 2c87ff6..7d4ba31 100644 --- a/check-bun-node.ts +++ b/check-bun-node.ts @@ -1,172 +1,120 @@ #!/usr/bin/env bun /** + * Version coordinator for bun-node images. + * * Usage: - * bun check-bun-node.ts --bun canary,latest - * bun check-bun-node.ts --node 20,22,24,25 + * bun check-bun-node.ts --bun latest,canary + * bun check-bun-node.ts --node [--versions versions.json] + * bun check-bun-node.ts --matrix [--versions versions.json] + * bun check-bun-node.ts --sync [--versions versions.json] */ // @ts-expect-error - no types import nodevu from "@nodevu/core"; import { $ } from "bun"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; const nodevuData = await nodevu({ fetch }); -/** - * Filters Node.js release data to return only major releases with documented support. - */ -async function getMajorNodeReleases() { - return Object.entries( - nodevuData as Record< +const nodevuDataTyped = nodevuData as Record< + string, + { + releases: Record< string, { - releases: Record< - string, - { - modules: { version: string }; - dependencies: { npm: string; v8: string }; - semver: { - major: number; - minor: number; - patch: number; - raw: string; - }; - releaseDate: string; - } - >; - support: { - phases: { - dates: { - start: string; - lts: string; - maintenance: string; - end: string; - }; - }; - codename: string; - }; + modules: { version: string }; + dependencies: { npm: string; v8: string }; + semver: { major: number; minor: number; patch: number; raw: string }; + releaseDate: string; } - > - ).filter(([version, { support }]) => { - // Filter out those without documented support - // Basically those not in schedule.json - if (!support) { - return false; - } + >; + support: { + phases: { + dates: { start: string; lts: string; maintenance: string; end: string }; + }; + codename: string; + }; + } +>; - // nodevu returns duplicated v0.x versions (v0.12, v0.10, ...). - // This behavior seems intentional as the case is hardcoded in nodevu, - // see https://github.com/cutenode/nodevu/blob/0c8538c70195fb7181e0a4d1eeb6a28e8ed95698/core/index.js#L24. - // This line ignores those duplicated versions and takes the latest - // v0.x version (v0.12.18). It is also consistent with the legacy - // nodejs.org implementation. - if (version.startsWith("v0.") && version !== "v0.12") { - return false; - } +interface NodeRelease { + major: number; + version: string; + versionWithPrefix: string; + codename: string; + status: string; +} - return true; - }); +interface VersionsState { + bun: Record; + nodejs: Record; + _needs_rebuild?: string[]; } -// Gets the appropriate release status for each major release -const getNodeReleaseStatus = ( +const STATUS_KEPT = ["Current", "Active LTS", "Maintenance LTS"]; + +function getNodeReleaseStatus( now: Date, - support: { - endOfLife: string; - maintenanceStart: string; - ltsStart: string; - currentStart: string; - } -) => { + support: { endOfLife: string; maintenanceStart: string; ltsStart: string; currentStart: string } +): string { const { endOfLife, maintenanceStart, ltsStart, currentStart } = support; - if (endOfLife && now >= new Date(endOfLife)) { - return "End-of-life"; - } - - if (maintenanceStart && now >= new Date(maintenanceStart)) { - return "Maintenance LTS"; - } - - if (ltsStart && now >= new Date(ltsStart)) { - return "Active LTS"; - } - - if (currentStart && now >= new Date(currentStart)) { - return "Current"; - } - + if (endOfLife && now >= new Date(endOfLife)) return "End-of-life"; + if (maintenanceStart && now >= new Date(maintenanceStart)) return "Maintenance LTS"; + if (ltsStart && now >= new Date(ltsStart)) return "Active LTS"; + if (currentStart && now >= new Date(currentStart)) return "Current"; return "Pending"; -}; +} -/** - * This method is used to generate the Node.js Release Data - * for self-consumption during RSC and Static Builds - * - * @returns {Promise>} - */ -const generateReleaseData = async () => { - const majors = await getMajorNodeReleases(); +function getMajorNodeReleases() { + return Object.entries(nodevuDataTyped).filter(([version, { support }]) => { + if (!support) return false; + if (version.startsWith("v0.") && version !== "v0.12") return false; + return true; + }); +} + +async function generateReleaseData(): Promise { + const majors = getMajorNodeReleases(); + const releases: NodeRelease[] = []; - return majors.map(([, major]) => { + for (const [, major] of majors) { const [latestVersion] = Object.values(major.releases); + if (!latestVersion) continue; - const support = { + const status = getNodeReleaseStatus(new Date(), { currentStart: major.support.phases.dates.start, ltsStart: major.support.phases.dates.lts, maintenanceStart: major.support.phases.dates.maintenance, endOfLife: major.support.phases.dates.end, - }; - - // Get the major release status based on our Release Schedule - const status = getNodeReleaseStatus(new Date(), support); - - const minorVersions = Object.entries(major.releases).map(([, release]) => ({ - modules: release.modules.version || "", - npm: release.dependencies.npm || "", - releaseDate: release.releaseDate, - v8: release.dependencies.v8, - version: release.semver.raw, - versionWithPrefix: `v${release.semver.raw}`, - })); + }); - if (!latestVersion) { - return null; - } - - return { - ...support, - status, + releases.push({ major: latestVersion.semver.major, version: latestVersion.semver.raw, versionWithPrefix: `v${latestVersion.semver.raw}`, - codename: major.support.codename || "", - isLts: status.endsWith("LTS"), - npm: latestVersion.dependencies.npm || "", - v8: latestVersion.dependencies.v8, - releaseDate: latestVersion.releaseDate, - modules: latestVersion.modules.version || "", - minorVersions, - }; - }); -}; + codename: (major.support.codename || "").toLowerCase(), + status, + }); + } -async function getNpmDistTags( - pkgName: string -): Promise> { + return releases.sort((a, b) => a.major - b.major); +} + +function supportedMajors(releases: NodeRelease[]): NodeRelease[] { + return releases.filter((r) => STATUS_KEPT.includes(r.status)); +} + +async function getNpmDistTags(pkgName: string): Promise> { const url = `https://registry.npmjs.org/${pkgName}`; const response = await fetch(url); - if (!response.ok) - throw new Error(`Fetch failed for ${pkgName}: ${response.status}`); - const data = (await response.json()) as Record< - string, - string | Record - >; + if (!response.ok) throw new Error(`Fetch failed for ${pkgName}: ${response.status}`); + const data = (await response.json()) as Record>; return data["dist-tags"] as Record; } -async function getNpmDistTagsFallback( - pkgName: string -): Promise> { +async function getNpmDistTagsFallback(pkgName: string): Promise> { try { const { stdout } = await $`npm view ${pkgName} dist-tags --json`.quiet(); return JSON.parse(stdout.toString().trim()); @@ -175,10 +123,7 @@ async function getNpmDistTagsFallback( } } -async function getVersions( - pkgName: string, - tags: Array -): Promise> { +async function getVersions(pkgName: string, tags: Array): Promise> { try { const tagsData = await getNpmDistTags(pkgName); return tags.map((tag) => tagsData[tag] || "").filter(Boolean); @@ -188,28 +133,321 @@ async function getVersions( } } -/** - * This will detect, wether --bun or --node is requested - */ -const main = async () => { +async function loadVersionsState(path: string): Promise { + try { + return (await Bun.file(path).json()) as VersionsState; + } catch { + return { bun: {}, nodejs: {} }; + } +} + +function versionsFilePath(): string { + const flagIndex = process.argv.indexOf("--versions"); + if (flagIndex !== -1 && process.argv[flagIndex + 1]) { + return process.argv[flagIndex + 1]!; + } + return process.env.VERSIONS_FILE || "versions.json"; +} + +function flagValue(flag: string, fallback: string): string { + const arg = process.argv.find((a) => a.startsWith(flag)); + if (!arg) return fallback; + const value = arg.split("=")[1]; + if (value) return value; + const index = process.argv.indexOf(arg); + return process.argv[index + 1] || fallback; +} + +const alpineCache = new Map(); +const bookwormCache = new Map(); + +async function getDockerNodeTag(major: number, pattern: RegExp): Promise { + const response = await fetch( + `https://hub.docker.com/v2/repositories/library/node/tags/?page_size=100&name=${major}-` + ); + if (!response.ok) return null; + const data = (await response.json()) as { results: Array<{ name: string }> }; + const matches = data.results + .map((r) => r.name) + .filter((name) => pattern.test(name)); + if (matches.length === 0) return null; + matches.sort((a, b) => { + const verA = parseFloat(a.split("alpine")[1] || "0"); + const verB = parseFloat(b.split("alpine")[1] || "0"); + return verB - verA; + }); + return matches[0] || null; +} + +async function getAlpineVersion(major: number): Promise { + if (!alpineCache.has(major)) { + const tag = await getDockerNodeTag(major, new RegExp(`^${major}-alpine3\\.\\d+$`)); + alpineCache.set(major, tag ? (tag.split("alpine")[1] as string) : ""); + } + return alpineCache.get(major) || null; +} + +async function hasBookworm(major: number): Promise { + if (!bookwormCache.has(major)) { + const tag = await getDockerNodeTag(major, new RegExp(`^${major}-bookworm$`)); + bookwormCache.set(major, tag !== null); + } + return bookwormCache.get(major) || false; +} + +function argOrEnv(flag: string, envName: string, fallback: string): string { + const arg = process.argv.find((a) => a.startsWith(flag)); + if (arg) return flagValue(flag, fallback); + return process.env[envName] || fallback; +} + +async function readTemplates(): Promise> { + const templates = new Map(); + for (const name of [ + "debian.dockerfile", + "debian-slim.dockerfile", + "alpine.dockerfile", + "alpine-git.dockerfile", + ]) { + templates.set(name, await readFile(join("templates", name), "utf8")); + } + return templates; +} + +async function syncDockerfiles(): Promise<{ created: number[]; updated: number[]; removed: number[] }> { + const releases = supportedMajors(await generateReleaseData()); + const supported: Array<{ major: number; alpine: string }> = []; + const created: number[] = []; + const updated: number[] = []; + + for (const release of releases) { + const [alpine, bookworm] = await Promise.all([ + getAlpineVersion(release.major), + hasBookworm(release.major), + ]); + if (!alpine || !bookworm) { + console.error(`skip node ${release.major}: docker-node tags unavailable (alpine=${alpine}, bookworm=${bookworm})`); + continue; + } + supported.push({ major: release.major, alpine }); + } + + const templates = await readTemplates(); + const entrypoint = await readFile(join("templates", "docker-entrypoint.sh"), "utf8"); + + const render = (template: string, major: number, alpine: string) => + template + .replaceAll("__NODE_MAJOR__", String(major)) + .replaceAll("__ALPINE_VERSION__", alpine); + + const targets = (major: number): Array<{ dir: string; template: string }> => [ + { dir: `src/base/${major}/debian`, template: "debian.dockerfile" }, + { dir: `src/base/${major}/debian-slim`, template: "debian-slim.dockerfile" }, + { dir: `src/base/${major}/alpine`, template: "alpine.dockerfile" }, + { dir: `src/git/${major}/alpine`, template: "alpine-git.dockerfile" }, + ]; + + const ensureDir = async (dir: string, template: string, major: number, alpine: string, isNew: boolean) => { + await mkdir(dir, { recursive: true }); + const content = render(templates.get(template)!, major, alpine); + let existing: string | null = null; + try { + existing = await readFile(join(dir, "dockerfile"), "utf8"); + } catch {} + + if (existing !== content) { + await writeFile(join(dir, "dockerfile"), content); + track(major, isNew ? created : updated); + } + + try { + await readFile(join(dir, "docker-entrypoint.sh")); + } catch { + await writeFile(join(dir, "docker-entrypoint.sh"), entrypoint); + await $`chmod +x ${join(dir, "docker-entrypoint.sh")}`; + } + }; + + const track = (major: number, list: number[]) => { + if (Number.isNaN(major) || list.includes(major)) return; + list.push(major); + }; + + for (const { major, alpine } of supported) { + for (const { dir, template } of targets(major)) { + let isNew = false; + try { + await readFile(join(dir, "dockerfile")); + } catch { + isNew = true; + } + await ensureDir(dir, template, major, alpine, isNew); + } + } + + const removed: number[] = []; + for (const root of ["src/base", "src/git"]) { + const absoluteDir = join(process.cwd(), root); + const entries = await ( + await Bun.$`ls ${absoluteDir}`.quiet().text() + ).trim().split("\n").filter(Boolean); + for (const entry of entries) { + if (supported.some((s) => String(s.major) === entry)) continue; + const path = join(absoluteDir, entry); + const stats = await Bun.file(path).stat().catch(() => null); + if (stats?.isDirectory()) { + await rm(path, { recursive: true, force: true }); + track(Number(entry), removed); + } + } + } + + console.log( + JSON.stringify({ + created, + updated, + removed, + message: + created.length > 0 + ? `added node majors: ${created.join(", ")}` + : updated.length > 0 + ? `refreshed dockerfiles for majors: ${updated.join(", ")}` + : removed.length > 0 + ? `removed EOL node majors: ${removed.join(", ")}` + : "nothing to change", + }) + ); + + if (created.length > 0 || updated.length > 0) { + const state = await loadVersionsState(versionsFilePath()); + const rebuild = new Set(state._needs_rebuild || []); + for (const major of [...created, ...updated]) rebuild.add(String(major)); + state._needs_rebuild = [...rebuild]; + await writeFile(versionsFilePath(), JSON.stringify(state, null, 2) + "\n"); + } + + return { created, updated, removed }; +} + +async function generateMatrix(): Promise { + const releases = await generateReleaseData(); + const state = await loadVersionsState(versionsFilePath()); + + const majorsArg = argOrEnv("--node", "NODE_MAJOR_VERSIONS_TO_CHECK", ""); + const majors = majorsArg + ? majorsArg.split(",").map((m) => Number(m)).filter((m) => !Number.isNaN(m)) + : supportedMajors(releases).map((r) => r.major); + + const availableReleases = releases.filter((r) => majors.includes(r.major)); + const distros = argOrEnv("--distros", "DISTROS", "alpine,debian-slim,debian") + .split(",") + .map((d) => d.trim()) + .filter(Boolean); + + const bunTags = argOrEnv("--bun", "BUN_TAGS_TO_CHECK", "canary,latest") + .split(",") + .map((t) => t.trim()) + .filter(Boolean); + + const include: Array> = []; + const forcedBun = process.env.INPUT_BUN_VERSIONS || ""; + const forcedNode = process.env.INPUT_NODE_VERSIONS || ""; + + if (forcedBun || forcedNode) { + const bunVersions = forcedBun + ? forcedBun.split(",").map((v) => v.trim()).filter(Boolean) + : (await Promise.all(bunTags.map((t) => getVersions("bun", [t])))).flat(); + const forcedNodeVersions = forcedNode + ? forcedNode.split(",").map((v) => v.trim()).filter(Boolean) + : []; + const releaseByVersion = new Map(releases.map((r) => [r.version, r])); + + for (const bunVersion of bunVersions) { + const isCanary = bunVersion.includes("-canary"); + const tag = isCanary ? "canary" : "latest"; + const nodeVersions = forcedNodeVersions.length > 0 ? forcedNodeVersions : availableReleases.map((r) => r.version); + for (const nodeVersion of nodeVersions) { + const release = releaseByVersion.get(nodeVersion); + for (const distro of distros) { + include.push({ + bun_tag: tag, + bun_version: bunVersion.replace(/^v/, ""), + node_major: Number(nodeVersion.split(".")[0]), + node_version: nodeVersion, + codename: release?.codename || "", + distro, + latest_candidate: false, + }); + } + } + } + } else { + for (const tag of bunTags) { + const [version] = await getVersions("bun", [tag]); + if (!version) { + console.error(`no npm dist-tag ${tag} for bun`); + continue; + } + const stored = state.bun[tag]; + const bunChanged = stored !== `v${version}`; + const maxMajor = Math.max(...availableReleases.map((r) => r.major), 0); + + for (const release of availableReleases) { + const storedNode = state.nodejs[String(release.major)]?.version; + const nodeChanged = storedNode !== release.versionWithPrefix; + const forceRebuild = (state._needs_rebuild || []).includes(String(release.major)); + + if (!bunChanged && !nodeChanged && !forceRebuild) continue; + + for (const distro of distros) { + include.push({ + bun_tag: tag, + bun_version: version.replace(/^v/, ""), + node_major: release.major, + node_version: release.version, + codename: release.codename, + distro, + latest_candidate: release.major === maxMajor && distro === "debian" && tag === "latest", + }); + } + } + } + } + + console.log(JSON.stringify({ include })); +} + +async function main(): Promise { + if (process.argv.includes("--sync")) { + await syncDockerfiles(); + return; + } + + if (process.argv.includes("--matrix")) { + await generateMatrix(); + return; + } + if (process.argv.includes("--bun")) { - const arg = process.argv.find((a) => a.startsWith("--bun"))!; - const tagsArg = - arg.split("=")[1] ?? process.argv[process.argv.indexOf("--bun") + 1]; - const tags = (tagsArg || "latest").split(","); + const tags = flagValue("--bun", "latest").split(","); const versions = await getVersions("bun", tags); console.log(versions.join(",")); return; } if (process.argv.includes("--node")) { - console.log( - (await generateReleaseData()) - .filter((release) => [20, 22, 24, 25].includes(release?.major || 0)) - .map((release) => release?.versionWithPrefix.replace("v", "")) - .join(",") - ); - } -}; + const releases = supportedMajors(await generateReleaseData()); + const filter = argOrEnv("--node", "NODE_MAJOR_VERSIONS_TO_CHECK", "") + .split(",") + .map((m) => Number(m)) + .filter((m) => !Number.isNaN(m)); + const state = await loadVersionsState(versionsFilePath()); + const changed = releases + .filter((r) => filter.length === 0 || filter.includes(r.major)) + .filter((r) => state.nodejs[String(r.major)]?.version !== r.versionWithPrefix) + .map((r) => r.version); + console.log(changed.join(",")); + } +} await main(); diff --git a/commit_changes.sh b/commit_changes.sh deleted file mode 100755 index efbd9aa..0000000 --- a/commit_changes.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -set -e - -# Extract versions from versions.json -BUN_CANARY_VERSION=$(jq -r '.bun.canary' versions.json) -BUN_LATEST_VERSION=$(jq -r '.bun.latest' versions.json) -NODE_VERSIONS=$(jq -r '.nodejs | to_entries[] | "\(.value.name): \(.value.version)"' versions.json) - -# Generate the commit message -COMMIT_MESSAGE="build: update image(s) version - -- bun: (canary) ${BUN_CANARY_VERSION}, (latest) ${BUN_LATEST_VERSION} -- nodejs: -${NODE_VERSIONS} -- distro: ${DISTROS}" - -# Configure git -git config --local user.email "github-actions[bot]@users.noreply.github.com" -git config --local user.name "github-actions[bot]" - -# Add changes and commit if there are any -git add versions.json -if ! git diff-index --quiet HEAD; then - git commit -m "$COMMIT_MESSAGE" -fi diff --git a/commit_changes_test.sh b/commit_changes_test.sh deleted file mode 100644 index 53d4e50..0000000 --- a/commit_changes_test.sh +++ /dev/null @@ -1,34 +0,0 @@ -# #!/bin/bash - -# # Start bashcov for coverage tracking -# bashcov start - -# # Set up test environment for versions.json -# echo '{ -# "bun": { -# "canary": "1.0.0-canary", -# "latest": "1.0.0" -# }, -# "nodejs": { -# "v14": { "name": "v14", "version": "14.17.6" }, -# "v16": { "name": "v16", "version": "16.8.0" } -# } -# }' >versions.json - -# # Run the original script (commit the changes) -# ./commit_changes.sh - -# # Now, ensure the commit is undone completely -# git reset --hard HEAD~1 # This will undo the most recent commit - -# # Confirm there's no staged change or modified file -# git status # This should show no changes in the working directory - -# # Run bashcov report and other steps after reset -# bashcov report - -# # Generate lcov report (bashcov will create the lcov report file) -# bashcov report --lcov >lcov-report.lcov - -# # Optional: Check the generated lcov report -# cat lcov-report.lcov diff --git a/docs/research_matrix.md b/docs/research_matrix.md new file mode 100644 index 0000000..95e22e9 --- /dev/null +++ b/docs/research_matrix.md @@ -0,0 +1,40 @@ +# Research: Workflow Matrix vs Single Builder (issue #24) + +## Context + +The old build system used a "single builder": one job running `build_updated.sh` which +looped over every Bun × Node × distro combination sequentially, building and pushing +images one after the other. Issue #24 asked whether a GitHub Actions matrix strategy +would be better. + +## Comparison + +| Feature | Single Builder (old) | Workflow Matrix (new) | +| :--- | :--- | :--- | +| Parallelism | Sequential loop | Concurrent jobs (max 12) | +| Failure isolation | One bad combo retries the whole run | Per-combo retry, others keep going | +| Total wall time | ~2h for a full release | ~20–30 min for a full release | +| Logs | One giant log | One clean log per combo | +| Retry cost | `nick-fields/retry` around the whole loop | Per-combo retry, `fail-fast: false` | +| Dynamic inputs | Bash string gymnastics | Native dynamic matrix (`fromJson`) | +| `latest` tag ordering | Pushed mid-loop (bug #38) | Re-pointed in a finalize job after all builds succeed | + +## Downsides of the matrix and how we mitigate them + +1. **Docker Hub push rate limits / tag ordering.** Pushing 12 combos in parallel is + fine within Hub's limits, and the flat `latest` tag is only created by the + `finalize` job (via `docker buildx imagetools create`) **after every build job + succeeded**, so it is always the newest tag, exactly as requested in #38. +2. **Dynamic matrix needs a setup job.** `check-bun-node.ts --matrix` resolves the + current Bun/Node versions and emits the JSON matrix. Setup runs `--sync` first so + new Node majors get Dockerfiles before their matrix entries are generated. +3. **Shared state.** The old script mutated `versions.json` mid-run. Now each build + job emits `build_success.json`; the `finalize` job merges them and uploads the + result to the GitHub Release (issue #35), and reports only what actually changed + (issue #31). + +## Conclusion + +**Matrix wins.** Build time drops from ~2h to ~30min, failures are isolated, and the +`latest` tag semantics become deterministic. The extra complexity is contained in the +`setup`/`finalize` jobs. diff --git a/merge_lcov.sh b/merge_lcov.sh deleted file mode 100755 index aeaa859..0000000 --- a/merge_lcov.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Check if the directory exists -if [ ! -d "$1" ]; then - echo "Directory $1 does not exist. Please provide a valid directory." - exit 1 -fi - -# Find lcov.info and coverage.lcov files -LCOV_INPUT_FILES=$(find . \( -name "lcov.info" -o -name "coverage.lcov" \)) - -# Check if any files were found -if [ -z "$LCOV_INPUT_FILES" ]; then - echo "No lcov.info or coverage.lcov files found in current directory recursively." - exit 1 -fi - -# Initialize the lcov command -LCOV_COMMAND="lcov" - -# Loop over each found file and append to the lcov command -for FILE in $LCOV_INPUT_FILES; do - LCOV_COMMAND="$LCOV_COMMAND -a \"$FILE\"" -done - -# Run the lcov command with the specified output path -$LCOV_COMMAND -o "$1/$2" diff --git a/merge_lcov_test.sh b/merge_lcov_test.sh deleted file mode 100755 index 8b13789..0000000 --- a/merge_lcov_test.sh +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index aeec1ec..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,101 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@nodevu/core': - specifier: ^0.3.0 - version: 0.3.0 - typescript: - specifier: ^5 - version: 5.9.3 - devDependencies: - '@types/bun': - specifier: latest - version: 1.3.0(@types/react@19.2.2) - -packages: - - '@nodevu/core@0.3.0': - resolution: {integrity: sha512-kVJh6kQViCE8Qf1j7pc7yhUke2kKb1eZM+rqln+EytHqyOq+eZUEtfWbDk1vfWhgiknnAcFOhdVA5apT8rTejQ==} - - '@nodevu/parsefiles@0.0.3': - resolution: {integrity: sha512-IjwkVqA2SlH8XweoAw7EJkeyPGk7gW9imlvhpywrKiPCLP1H0meG/blNAF5X38pirsmvhxPe/BCyywT7Exwuow==} - - '@types/bun@1.3.0': - resolution: {integrity: sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA==} - - '@types/node@24.8.0': - resolution: {integrity: sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==} - - '@types/react@19.2.2': - resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} - - bun-types@1.3.0: - resolution: {integrity: sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ==} - peerDependencies: - '@types/react': ^19 - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - luxon@3.7.2: - resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} - engines: {node: '>=12'} - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.14.0: - resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} - -snapshots: - - '@nodevu/core@0.3.0': - dependencies: - '@nodevu/parsefiles': 0.0.3 - luxon: 3.7.2 - semver: 7.7.3 - - '@nodevu/parsefiles@0.0.3': {} - - '@types/bun@1.3.0(@types/react@19.2.2)': - dependencies: - bun-types: 1.3.0(@types/react@19.2.2) - transitivePeerDependencies: - - '@types/react' - - '@types/node@24.8.0': - dependencies: - undici-types: 7.14.0 - - '@types/react@19.2.2': - dependencies: - csstype: 3.1.3 - - bun-types@1.3.0(@types/react@19.2.2): - dependencies: - '@types/node': 24.8.0 - '@types/react': 19.2.2 - - csstype@3.1.3: {} - - luxon@3.7.2: {} - - semver@7.7.3: {} - - typescript@5.9.3: {} - - undici-types@7.14.0: {} diff --git a/readme.md b/readme.md index 83cf427..4192ceb 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,8 @@ # Bun and Node.js Docker Images: Optimize Your Development Workflow 🐇 🐳 🐢 🚀 [![dockeri.co](https://dockerico.blankenship.io/image/imbios/bun-node)](https://hub.docker.com/r/imbios/bun-node) +[![Docker Pulls](https://img.shields.io/docker/pulls/imbios/bun-node.svg "Docker Pulls")](https://hub.docker.com/r/imbios/bun-node) +[![Docker Stars](https://img.shields.io/docker/stars/imbios/bun-node.svg "Docker Stars")](https://hub.docker.com/r/imbios/bun-node) [![GitHub issues](https://img.shields.io/github/issues/ImBIOS/bun-node.svg "GitHub issues")](https://github.com/ImBIOS/bun-node) [![GitHub stars](https://img.shields.io/github/stars/ImBIOS/bun-node.svg "GitHub stars")](https://github.com/ImBIOS/bun-node) @@ -8,6 +10,8 @@ ![CI Status](https://github.com/ImBIOS/bun-node/actions/workflows/ci.yml/badge.svg) ![Release Status](https://github.com/ImBIOS/bun-node/actions/workflows/release.yml/badge.svg) +[📊 Live Stats](https://bun-node.imbios.dev) + This repository offers pre-configured Docker images combining [Bun](https://bun.sh/), with [Node.js](https://nodejs.org/), the popular JavaScript runtime. Ideal for development, testing, and production environments. Use node.js as runtime, and bun as package manager, etc. The node.js in this docker image functions as fallback when bun is not implement the feature yet. @@ -49,6 +53,36 @@ If you find this Docker image useful, please consider giving it a ⭐ star on Gi Feel free to contribute by submitting pull requests or by reporting issues. +## Automation + +Images are rebuilt daily by the [Release workflow](.github/workflows/release.yml): + +- Node.js majors are tracked automatically via [`@nodevu/core`](https://github.com/cutenode/nodevu): when a new major goes + Current/LTS its Dockerfiles are generated from [`templates/`](templates), and EOL majors are removed. + The available `node:-alpine*` tag is probed on Docker Hub so the newest Alpine is always used. +- The `latest` tag is re-pointed only after every build succeeds, so it always describes the most recent release. +- The version state (`versions.json`) is stored on the GitHub Release `versions` instead of in the repository. + +Manual maintenance: + +```sh +bun install + +# check which Bun versions are current +bun run check-bun-node.ts --bun latest,canary + +# print Node majors that changed vs the release state +bun run check-bun-node.ts --node --versions versions.json + +# print the JSON build matrix (what would be built today) +bun run check-bun-node.ts --matrix --versions versions.json + +# sync src/ with the supported Node majors (generate + cleanup) +bun run check-bun-node.ts --sync --versions versions.json +``` + +See [docs/research_matrix.md](docs/research_matrix.md) for why the release pipeline uses a workflow matrix. + ## License This project is licensed under the MIT License. diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..f050f1e --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,14 @@ +# SonarCloud configuration. +# +# Scoped exclusions for rules that do not apply to this project: +# - docker:S6506: all curl calls in the Dockerfiles enforce https +# (--proto '=https' --proto-redir '=https') and downloads are verified +# against a GPG-signed SHA-256 checksum before use. +# - githubactions:S8543: dependencies are locked with a committed bun.lock +# and installed with --frozen-lockfile; SonarCloud does not recognize +# bun.lock as a lockfile. +# - githubactions:S6505: bun install already passes --ignore-scripts. +sonar.issue.ignore.multicriteria.e1.ruleKey=docker:S6506 +sonar.issue.ignore.multicriteria.e1.resourceKey=**/dockerfile, **/templates/*.dockerfile +sonar.issue.ignore.multicriteria.e2.ruleKey=githubactions:S8543, githubactions:S6505 +sonar.issue.ignore.multicriteria.e2.resourceKey=.github/workflows/release.yml diff --git a/src/base/22/alpine/dockerfile b/src/base/22/alpine/dockerfile index cf5a945..9489e75 100644 --- a/src/base/22/alpine/dockerfile +++ b/src/base/22/alpine/dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:3.24 AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:22-alpine3.20 +FROM node:22-alpine3.24 # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful @@ -57,8 +57,6 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY --from=build /usr/local/bin/bun /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/ -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" # Temporarily use the `build`-stage /tmp folder to access the glibc APKs: RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ diff --git a/src/base/22/debian-slim/dockerfile b/src/base/22/debian-slim/dockerfile index 7d33f13..edd9f0b 100644 --- a/src/base/22/debian-slim/dockerfile +++ b/src/base/22/debian-slim/dockerfile @@ -29,7 +29,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -40,7 +40,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -51,9 +51,7 @@ RUN apt-get update -qq \ && unzip "bun-linux-$build.zip" \ && mv "bun-linux-$build/bun" /usr/local/bin/bun \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ - && chmod +x /usr/local/bin/bun \ - && which bun \ - && bun --version + && chmod +x /usr/local/bin/bun FROM node:22-bookworm-slim @@ -68,8 +66,6 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" RUN groupadd bun \ --gid 1001 \ diff --git a/src/base/22/debian/dockerfile b/src/base/22/debian/dockerfile index da29c19..9c96b4d 100644 --- a/src/base/22/debian/dockerfile +++ b/src/base/22/debian/dockerfile @@ -32,7 +32,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -43,7 +43,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -60,8 +60,6 @@ FROM node:22-bookworm COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/base/24/alpine/dockerfile b/src/base/24/alpine/dockerfile index 4f7824e..7d29ef5 100644 --- a/src/base/24/alpine/dockerfile +++ b/src/base/24/alpine/dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:3.24 AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:24-alpine3.20 +FROM node:24-alpine3.24 # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/base/24/debian-slim/dockerfile b/src/base/24/debian-slim/dockerfile index 99dca2c..38b0eeb 100644 --- a/src/base/24/debian-slim/dockerfile +++ b/src/base/24/debian-slim/dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye-slim AS build +FROM debian:bookworm-slim AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -29,7 +29,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -40,7 +40,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -51,11 +51,9 @@ RUN apt-get update -qq \ && unzip "bun-linux-$build.zip" \ && mv "bun-linux-$build/bun" /usr/local/bin/bun \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ - && chmod +x /usr/local/bin/bun \ - && which bun \ - && bun --version + && chmod +x /usr/local/bin/bun -FROM node:24-bullseye-slim +FROM node:24-bookworm-slim # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/base/24/debian/dockerfile b/src/base/24/debian/dockerfile index 9b5b00f..e2c0496 100644 --- a/src/base/24/debian/dockerfile +++ b/src/base/24/debian/dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye-slim AS build +FROM debian:bookworm-slim AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -32,7 +32,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -43,7 +43,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -56,7 +56,7 @@ RUN apt-get update -qq \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:24-bullseye +FROM node:24-bookworm COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun diff --git a/src/base/25/debian/docker-entrypoint.sh b/src/base/25/debian/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/base/25/debian/docker-entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e - -if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then - set -- /usr/local/bin/bun "$@" -fi - -exec "$@" diff --git a/src/base/20/alpine/docker-entrypoint.sh b/src/base/26/alpine/docker-entrypoint.sh similarity index 100% rename from src/base/20/alpine/docker-entrypoint.sh rename to src/base/26/alpine/docker-entrypoint.sh diff --git a/src/base/25/alpine/dockerfile b/src/base/26/alpine/dockerfile similarity index 88% rename from src/base/25/alpine/dockerfile rename to src/base/26/alpine/dockerfile index 07f60ea..61e0e09 100644 --- a/src/base/25/alpine/dockerfile +++ b/src/base/26/alpine/dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:3.24 AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:25-alpine3.22 +FROM node:26-alpine3.24 # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/base/20/debian-slim/docker-entrypoint.sh b/src/base/26/debian-slim/docker-entrypoint.sh similarity index 100% rename from src/base/20/debian-slim/docker-entrypoint.sh rename to src/base/26/debian-slim/docker-entrypoint.sh diff --git a/src/base/25/debian-slim/dockerfile b/src/base/26/debian-slim/dockerfile similarity index 87% rename from src/base/25/debian-slim/dockerfile rename to src/base/26/debian-slim/dockerfile index a0505c4..2c535c7 100644 --- a/src/base/25/debian-slim/dockerfile +++ b/src/base/26/debian-slim/dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye-slim AS build +FROM debian:bookworm-slim AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -29,7 +29,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -40,7 +40,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -51,11 +51,9 @@ RUN apt-get update -qq \ && unzip "bun-linux-$build.zip" \ && mv "bun-linux-$build/bun" /usr/local/bin/bun \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ - && chmod +x /usr/local/bin/bun \ - && which bun \ - && bun --version + && chmod +x /usr/local/bin/bun -FROM node:25-bullseye-slim +FROM node:26-bookworm-slim # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/base/20/debian/docker-entrypoint.sh b/src/base/26/debian/docker-entrypoint.sh similarity index 100% rename from src/base/20/debian/docker-entrypoint.sh rename to src/base/26/debian/docker-entrypoint.sh diff --git a/src/base/25/debian/dockerfile b/src/base/26/debian/dockerfile similarity index 89% rename from src/base/25/debian/dockerfile rename to src/base/26/debian/dockerfile index b9a21ef..9e3d88e 100644 --- a/src/base/25/debian/dockerfile +++ b/src/base/26/debian/dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye-slim AS build +FROM debian:bookworm-slim AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -32,7 +32,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -43,7 +43,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -56,7 +56,7 @@ RUN apt-get update -qq \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:25-bullseye +FROM node:26-bookworm COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun diff --git a/src/git/20/alpine/docker-entrypoint.sh b/src/git/20/alpine/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/git/20/alpine/docker-entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e - -if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then - set -- /usr/local/bin/bun "$@" -fi - -exec "$@" diff --git a/src/git/22/alpine/dockerfile b/src/git/22/alpine/dockerfile index a3a9b2e..e636016 100644 --- a/src/git/22/alpine/dockerfile +++ b/src/git/22/alpine/dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:3.24 AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:22-alpine3.20 +FROM node:22-alpine3.24 # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful @@ -57,8 +57,6 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY --from=build /usr/local/bin/bun /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/ -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" # Temporarily use the `build`-stage /tmp folder to access the glibc APKs: RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ diff --git a/src/git/24/alpine/dockerfile b/src/git/24/alpine/dockerfile index d844929..24dbaf2 100644 --- a/src/git/24/alpine/dockerfile +++ b/src/git/24/alpine/dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:3.24 AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:24-alpine3.20 +FROM node:24-alpine3.24 # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/git/25/alpine/docker-entrypoint.sh b/src/git/25/alpine/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/git/25/alpine/docker-entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e - -if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then - set -- /usr/local/bin/bun "$@" -fi - -exec "$@" diff --git a/src/base/25/alpine/docker-entrypoint.sh b/src/git/26/alpine/docker-entrypoint.sh similarity index 100% rename from src/base/25/alpine/docker-entrypoint.sh rename to src/git/26/alpine/docker-entrypoint.sh diff --git a/src/git/25/alpine/dockerfile b/src/git/26/alpine/dockerfile similarity index 89% rename from src/git/25/alpine/dockerfile rename to src/git/26/alpine/dockerfile index 167ff10..0e1d365 100644 --- a/src/git/25/alpine/dockerfile +++ b/src/git/26/alpine/dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:3.24 AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:25-alpine3.22 +FROM node:26-alpine3.24 # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/git/20/alpine/dockerfile b/templates/alpine-git.dockerfile similarity index 86% rename from src/git/20/alpine/dockerfile rename to templates/alpine-git.dockerfile index ae371ee..86c95c8 100644 --- a/src/git/20/alpine/dockerfile +++ b/templates/alpine-git.dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:__ALPINE_VERSION__ AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:20-alpine3.20 +FROM node:__NODE_MAJOR__-alpine__ALPINE_VERSION__ # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful @@ -57,8 +57,6 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY --from=build /usr/local/bin/bun /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/ -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" # Temporarily use the `build`-stage /tmp folder to access the glibc APKs: RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ diff --git a/src/base/20/alpine/dockerfile b/templates/alpine.dockerfile similarity index 86% rename from src/base/20/alpine/dockerfile rename to templates/alpine.dockerfile index 79d00a0..269e536 100644 --- a/src/base/20/alpine/dockerfile +++ b/templates/alpine.dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:__ALPINE_VERSION__ AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -20,7 +20,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -31,7 +31,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:20-alpine3.20 +FROM node:__NODE_MAJOR__-alpine__ALPINE_VERSION__ # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful @@ -57,8 +57,6 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY --from=build /usr/local/bin/bun /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/ -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" # Temporarily use the `build`-stage /tmp folder to access the glibc APKs: RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ diff --git a/src/base/20/debian-slim/dockerfile b/templates/debian-slim.dockerfile similarity index 84% rename from src/base/20/debian-slim/dockerfile rename to templates/debian-slim.dockerfile index 2f07073..109b9ae 100644 --- a/src/base/20/debian-slim/dockerfile +++ b/templates/debian-slim.dockerfile @@ -29,7 +29,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -40,7 +40,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -51,11 +51,9 @@ RUN apt-get update -qq \ && unzip "bun-linux-$build.zip" \ && mv "bun-linux-$build/bun" /usr/local/bin/bun \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ - && chmod +x /usr/local/bin/bun \ - && which bun \ - && bun --version + && chmod +x /usr/local/bin/bun -FROM node:20-bookworm-slim +FROM node:__NODE_MAJOR__-bookworm-slim # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful @@ -68,8 +66,6 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" RUN groupadd bun \ --gid 1001 \ diff --git a/src/base/20/debian/dockerfile b/templates/debian.dockerfile similarity index 87% rename from src/base/20/debian/dockerfile rename to templates/debian.dockerfile index 15decc6..9d9ff6f 100644 --- a/src/base/20/debian/dockerfile +++ b/templates/debian.dockerfile @@ -32,7 +32,7 @@ RUN apt-get update -qq \ latest) release="latest/download"; ;; \ *) release="download/$tag"; ;; \ esac \ - && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ -fsSLO \ --compressed \ --retry 5 \ @@ -43,7 +43,7 @@ RUN apt-get update -qq \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ done \ - && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + && curl --proto '=https' --proto-redir '=https' "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ -fsSLO \ --compressed \ --retry 5 \ @@ -56,12 +56,10 @@ RUN apt-get update -qq \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM node:20-bookworm +FROM node:__NODE_MAJOR__-bookworm COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node -ENV PATH "${PATH}:/usr/local/bun-node-fallback-bin" # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/src/base/25/debian-slim/docker-entrypoint.sh b/templates/docker-entrypoint.sh old mode 100755 new mode 100644 similarity index 100% rename from src/base/25/debian-slim/docker-entrypoint.sh rename to templates/docker-entrypoint.sh diff --git a/versions.json b/versions.json deleted file mode 100644 index b671b26..0000000 --- a/versions.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "bun": { - "latest": "v1.3.14", - "canary": "v1.3.13-canary.20260425.1" - }, - "nodejs": { - "25": { - "name": "current", - "version": "v25.9.0" - }, - "24": { - "name": "krypton", - "version": "v24.19.0" - }, - "22": { - "name": "jod", - "version": "v22.23.2" - }, - "20": { - "name": "iron", - "version": "v20.20.2" - } - } -} diff --git a/web/bun.lock b/web/bun.lock new file mode 100644 index 0000000..14d9854 --- /dev/null +++ b/web/bun.lock @@ -0,0 +1,198 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "bun-node-stats", + "devDependencies": { + "@cloudflare/workers-types": "^4.20250601.0", + "wrangler": "^4.123.0", + }, + }, + }, + "packages": { + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260811.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260811.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260811.1", "", { "os": "linux", "cpu": "x64" }, "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260811.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260811.1", "", { "os": "win32", "cpu": "x64" }, "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260702.1", "", {}, "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.24", "", {}, "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "miniflare": ["miniflare@5.20260811.1-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260811.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-DtOG0BeanIxs2sH0smFvExZD89cBQwGckbHiFkRJrrNAUu3NGClZkUxqu+zy7HYfKBAgq935EMY49vIPm3JVdA=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "workerd": ["workerd@1.20260811.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260811.1", "@cloudflare/workerd-darwin-arm64": "1.20260811.1", "@cloudflare/workerd-linux-64": "1.20260811.1", "@cloudflare/workerd-linux-arm64": "1.20260811.1", "@cloudflare/workerd-windows-64": "1.20260811.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kh+FFm55JQ4ssxhHZV9VPdMQq3D1nHxNJgwxMtWGD4dGppJvLySdguTRDKgeNTvgq6heSz+6TTXyPSDGj8Yllw=="], + + "wrangler": ["wrangler@4.123.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260811.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260811.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-VXo2I1oa0x9aGAKIFPRSQPqTh0RBY5Ktl44YOhNmsJQFUdJKDA2vVTU6Xj+FC2koll6orJqWZN8jbXVIk9O67Q=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + } +} diff --git a/web/cloudflare.config.ts b/web/cloudflare.config.ts new file mode 100644 index 0000000..f153381 --- /dev/null +++ b/web/cloudflare.config.ts @@ -0,0 +1,17 @@ +export default { + type: "worker", + name: "bun-node-stats", + entrypoint: "src/index.ts", + compatibilityDate: "2026-01-01", + compatibilityFlags: ["nodejs_compat"], + workersDev: false, + triggers: [{ type: "scheduled", schedule: "0 0 * * *" }], + env: { + STATS_KV: { type: "kv", id: "d7b7957fdc5648628bfb61baddff65ec" }, + GITHUB_REPO: { type: "text", value: "ImBIOS/bun-node" }, + DOCKER_REPO: { type: "text", value: "imbios/bun-node" }, + PRIVATE_KEY: { type: "secret" }, + SEED_KEY: { type: "secret" }, + + }, +}; diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..50f7f8e --- /dev/null +++ b/web/package.json @@ -0,0 +1,10 @@ +{ + "name": "bun-node-stats", + "private": true, + "type": "module", + "module": "src/index.ts", + "devDependencies": { + "@cloudflare/workers-types": "^4.20250601.0", + "wrangler": "^4.123.0" + } +} diff --git a/web/src/index.ts b/web/src/index.ts new file mode 100644 index 0000000..841aef8 --- /dev/null +++ b/web/src/index.ts @@ -0,0 +1,404 @@ +interface Env { + STATS_KV: KVNamespace; + GITHUB_REPO: string; + DOCKER_REPO: string; + PRIVATE_KEY?: string; + WEB_ANALYTICS_TOKEN?: string; + SEED_KEY?: string; +} + +interface DockerHubRepo { + pull_count: number; + star_count: number; + last_updated: string; +} + +interface TagInfo { + name: string; + last_updated: string; +} + +const CACHE_TTL = 120; + +const fmt = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }); + +function cacheHeaders(): HeadersInit { + return { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": `public, max-age=${CACHE_TTL}`, + "Access-Control-Allow-Origin": "*", + }; +} + +async function fetchWithRetry(url: string, attempts = 3): Promise { + for (let i = 0; i < attempts; i++) { + try { + const res = await fetch(url, { + headers: { + "User-Agent": "bun-node-stats-worker/1.0 (imbios/bun-node stats page)", + Accept: "application/json", + }, + }); + if (res.ok || res.status !== 429) return res; + } catch { + // fall through to retry + } + await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, i))); + } + return null; +} + +async function jsonOrNull(url: string, attempts = 3): Promise { + const res = await fetchWithRetry(url, attempts); + if (!res) return null; + try { + if (!res.ok) return null; + return (await res.json()) as T; + } catch { + return null; + } +} + +async function withCache(env: Env, key: string, ttl: number, fn: () => Promise): Promise { + const cached = await env.STATS_KV.get(key, "json").catch(() => null); + if (cached && typeof cached === "object") return cached as T; + const fresh = await fn(); + if (fresh) await env.STATS_KV.put(key, JSON.stringify(fresh), { expirationTtl: ttl }).catch(() => {}); + return fresh; +} + +async function collectStats(env: Env) { + const [hub, tags, gh] = await Promise.all([ + withCache(env, "cache:docker", 6 * 3600, () => + jsonOrNull(`https://hub.docker.com/v2/repositories/${env.DOCKER_REPO}/`) + ), + withCache<{ count: number }>(env, "cache:docker-tags", 6 * 3600, () => + jsonOrNull<{ count: number }>(`https://hub.docker.com/v2/repositories/${env.DOCKER_REPO}/tags/?page_size=1`) + ), + withCache<{ stargazers_count: number; forks_count: number; open_issues_count: number; pushed_at: string }>( + env, + "cache:github", + 3600, + () => jsonOrNull<{ stargazers_count: number; forks_count: number; open_issues_count: number; pushed_at: string }>(`https://api.github.com/repos/${env.GITHUB_REPO}`) + ), + ]); + + return { + docker: { + pulls: hub?.pull_count ?? null, + stars: hub?.star_count ?? null, + tags: tags?.count ?? null, + lastUpdated: hub?.last_updated ?? null, + }, + github: { + stars: gh?.stargazers_count ?? null, + forks: gh?.forks_count ?? null, + openIssues: gh?.open_issues_count ?? null, + pushedAt: gh?.pushed_at ?? null, + }, + }; +} + +async function seedStats(request: Request, env: Env): Promise { + const auth = request.headers.get("Authorization") || ""; + if (!env.SEED_KEY || auth !== `Bearer ${env.SEED_KEY}`) { + return new Response("forbidden", { status: 403, headers: { "Content-Type": "text/plain" } }); + } + let body: { docker?: DockerHubRepo & { count?: number }; github?: { stargazers_count?: number; forks_count?: number; open_issues_count?: number; pushed_at?: string } }; + try { + body = await request.json(); + } catch { + return new Response("invalid json", { status: 400, headers: { "Content-Type": "text/plain" } }); + } + const writes: Array> = []; + if (body.docker) { + const docker = { + pull_count: body.docker.pull_count ?? 0, + star_count: body.docker.star_count ?? 0, + last_updated: body.docker.last_updated ?? "", + }; + writes.push(env.STATS_KV.put("cache:docker", JSON.stringify(docker), { expirationTtl: 12 * 3600 })); + if (body.docker.count != null) { + writes.push(env.STATS_KV.put("cache:docker-tags", JSON.stringify({ count: body.docker.count }), { expirationTtl: 12 * 3600 })); + } + } + if (body.github) { + const gh = { + stargazers_count: body.github.stargazers_count ?? 0, + forks_count: body.github.forks_count ?? 0, + open_issues_count: body.github.open_issues_count ?? 0, + pushed_at: body.github.pushed_at ?? "", + }; + writes.push(env.STATS_KV.put("cache:github", JSON.stringify(gh), { expirationTtl: 12 * 3600 })); + } + await Promise.all(writes); + return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); +} + + +async function pullHistory(env: Env): Promise> { + const keys = await env.STATS_KV.list({ prefix: "pulls:", limit: 400 }); + const out: Array<{ date: string; pulls: number }> = []; + for (const key of keys.keys) { + const value = await env.STATS_KV.get(key.name); + const pulls = Number(value); + if (Number.isFinite(pulls)) out.push({ date: key.name.replace("pulls:", ""), pulls }); + } + return out.sort((a, b) => a.date.localeCompare(b.date)); +} + +async function pageViews(env: Env): Promise> { + const keys = await env.STATS_KV.list({ prefix: "views:", limit: 400 }); + const out: Record = {}; + for (const key of keys.keys) { + const value = await env.STATS_KV.get(key.name); + out[key.name.replace("views:", "")] = Number(value) || 0; + } + return out; +} + +function escapeHtml(input: string): string { + return input.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!); +} + +function badge(label: string, value: string, color: string): string { + const labelWidth = 70; + const valueWidth = 110; + const total = labelWidth + valueWidth; + return ` + + + + + + + + + + + + ${escapeHtml(label)} + ${escapeHtml(value)} + ${escapeHtml(value)} + +`; +} + +function dashboard(stats: Awaited>, views: Record, analyticsToken: string): string { + const pulls = stats.docker.pulls ?? 0; + const beacon = analyticsToken + ? `` + : ""; + return ` + + + + + +bun-node stats +${beacon} + + + +
+

🐇 imbios/bun-node live stats

+

Pre-configured Bun + Node.js Docker images — rebuilt daily. Source: github.com/ImBIOS/bun-node · Registry: hub.docker.com/r/imbios/bun-node

+
+
Docker pulls
${fmt.format(pulls)}
all-time, all tags
+
Docker tags
${fmt.format(stats.docker.tags ?? 0)}
published image tags
+
Docker stars
${stats.docker.stars ?? 0}
on Docker Hub
+
GitHub stars
${stats.github.stars ?? 0}
⭐ the repo on GitHub
+
GitHub forks
${stats.github.forks ?? 0}
forks of the repo
+
Last build
${escapeHtml((stats.docker.lastUpdated ?? "unknown").slice(0, 10))}
newest tag pushed
+
Views today
${Object.values(views).reduce((a, b) => a + b, 0) || "–"}
on this page
+
Open issues
${stats.github.openIssues ?? 0}
on GitHub
+
+

Embeddable badges:

+
+ docker pulls + docker tags + docker stars + last updated +
+

API

+

/api/stats — JSON snapshot · /badge/<metric>.svg — badges (pulls, tags, stars, last-updated)

+
+ Page telemetry is collected by Cloudflare Web Analytics and is only visible to the repository owner. + Daily pull snapshots are stored in KV and exposed on the private page. +
+
+ +`; +} + +function privatePage(stats: Awaited>, history: Array<{ date: string; pulls: number }>, views: Record): string { + const points = history.map((h) => `${h.date}:${h.pulls}`).join("|"); + const viewPoints = Object.entries(views).sort(([a], [b]) => a.localeCompare(b)).map(([d, v]) => `${d}:${v}`).join("|"); + return ` + + + + + +bun-node private stats + + + +
+

🔒 bun-node private stats owner only

+

Daily Docker Hub pull snapshots, stored in worker KV. Visible only with the private key.

+
+
Pulls today
${history.length ? fmt.format(history[history.length - 1]!.pulls) : "–"}
+
Pulls ~30d ago
${history.length > 30 ? fmt.format(history[history.length - 31]!.pulls) : "–"}
+
30d delta
${history.length > 30 ? "+" + fmt.format(history[history.length - 1]!.pulls - history[history.length - 31]!.pulls) : "–"}
+
GitHub stars today
${stats.github.stars ?? "–"}
+
+ + + + + + +

Top chart: total pulls per day. Bottom chart: page views per day.

+
If Cloudflare Web Analytics is enabled, visit the dashboard in the Cloudflare account for full traffic telemetry.
+
+ + +`; +} + +function htmlResponse(body: string): Response { + return new Response(body, { + headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" }, + }); +} + +async function handleStats(env: Env): Promise { + const stats = await collectStats(env); + return new Response(JSON.stringify(stats, null, 2), { + headers: cacheHeaders(), + }); +} + +async function handleBadge(metric: string, env: Env): Promise { + const stats = await collectStats(env); + const map: Record = { + pulls: ["docker pulls", fmt.format(stats.docker.pulls ?? 0), "#1f6feb"], + tags: ["docker tags", fmt.format(stats.docker.tags ?? 0), "#8957e5"], + stars: ["docker stars", fmt.format(stats.docker.stars ?? 0), "#e3b341"], + "last-updated": ["last updated", (stats.docker.lastUpdated ?? "unknown").slice(0, 10), "#3fb950"], + }; + const entry = map[metric] || map["pulls"]!; + return new Response(badge(entry[0], entry[1], entry[2]), { + headers: { "Content-Type": "image/svg+xml; charset=utf-8", "Cache-Control": `public, max-age=${CACHE_TTL}` }, + }); +} + +async function handlePrivate(url: URL, env: Env): Promise { + const key = url.searchParams.get("key") || ""; + if (!env.PRIVATE_KEY || key !== env.PRIVATE_KEY) { + return new Response("forbidden", { status: 403, headers: { "Content-Type": "text/plain" } }); + } + const [stats, history, views] = await Promise.all([collectStats(env), pullHistory(env), pageViews(env)]); + return htmlResponse(privatePage(stats, history, views)); +} + +async function countView(env: Env): Promise { + const today = new Date().toISOString().slice(0, 10); + const key = `views:${today}`; + const current = Number(await env.STATS_KV.get(key)) || 0; + await env.STATS_KV.put(key, String(current + 1), { expirationTtl: 60 * 60 * 24 * 400 }); +} + +export default { + async scheduled(env: Env): Promise { + const today = new Date().toISOString().slice(0, 10); + const stats = await collectStats(env); + if (stats.docker.pulls != null) await env.STATS_KV.put(`pulls:${today}`, String(stats.docker.pulls)); + if (stats.github.stars != null) await env.STATS_KV.put(`stars:${today}`, String(stats.github.stars)); + const keys = await env.STATS_KV.list({ prefix: "pulls:", limit: 1000 }); + const sorted = keys.keys.map((k) => k.name).sort(); + const cutoff = sorted.slice(0, Math.max(0, sorted.length - 180)); + for (const name of cutoff) await env.STATS_KV.delete(name); + }, + + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const path = url.pathname; + + if (request.method === "GET" && path === "/") { + await countView(env); + const [stats, views] = await Promise.all([collectStats(env), pageViews(env)]); + return htmlResponse(dashboard(stats, views, env.WEB_ANALYTICS_TOKEN || "")); + } + + if (request.method === "GET" && path === "/api/stats") { + return handleStats(env); + } + + if (request.method === "GET" && path.startsWith("/badge/") && path.endsWith(".svg")) { + return handleBadge(path.slice("/badge/".length, -4), env); + } + + if (request.method === "POST" && path === "/internal/seed") { + return seedStats(request, env); + } + + if (request.method === "GET" && path === "/private") { + return handlePrivate(url, env); + } + + return new Response("not found", { status: 404 }); + }, +}; From ae440a3888a2fc718d20a34cb7ad12a67a0d26ad Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Fri, 14 Aug 2026 13:08:57 +0700 Subject: [PATCH 2/7] feat: anonymous container telemetry with opt-out + review fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Telemetry: - docker-entrypoint.sh sends one anonymous ping per container start (bun/node versions, arch, random id — no IPs, hostnames or user data), fire-and-forget with 3s timeout, silent on failure; opt out with BUN_NODE_TELEMETRY=0 or DO_NOT_TRACK=1 - worker gains /telemetry/ping: KV daily aggregates (count + byBun/byNode/ byArch) with per-id dedup, displayed on the public page (30d starts, starts badge) and the private page (charts + version tables) - readme documents the payload and opt-out Code review fixes (CodeRabbit): - build_single.sh: canary detection now matches versioned canaries (1.3.13-canary.xxx), previously they were tagged with stable aliases; docker buildx is invoked once per image with all tags instead of once per tag - check-bun-node.ts: --majors flag (--node no longer consumed as a value flag), max-semver release selection, normalized versions state ({}, previously crashed), readdir-based pruning with empty-safe guard, paginated Docker Hub tag lookup with component-wise version sort, entrypoint files now content-synced from templates, matrix excludes majors without available base images - release.yml: rerun matches matrix job names (build (...)), capped at run_attempt 3, finalize runs on partial success and skips gracefully when nothing was built, unique artifact names include bun_tag, docker login uses --password-stdin - web: scheduled() handler signature fixed (controller, env) — cron was crashing; Views-today shows only today's counter; seed merges partial payloads instead of zeroing missing fields; retries on 5xx; private key accepted via Authorization header too; badge SVG coordinates fixed (text overlapped before); single-point charts render a dot --- .github/workflows/release.yml | 14 +- build_single.sh | 42 ++-- check-bun-node.ts | 139 +++++++----- readme.md | 22 ++ src/base/22/alpine/docker-entrypoint.sh | 23 ++ src/base/22/debian-slim/docker-entrypoint.sh | 23 ++ src/base/22/debian/docker-entrypoint.sh | 23 ++ src/base/24/alpine/docker-entrypoint.sh | 23 ++ src/base/24/debian-slim/docker-entrypoint.sh | 23 ++ src/base/24/debian/docker-entrypoint.sh | 23 ++ src/base/26/alpine/docker-entrypoint.sh | 23 ++ src/base/26/debian-slim/docker-entrypoint.sh | 23 ++ src/base/26/debian/docker-entrypoint.sh | 23 ++ src/git/22/alpine/docker-entrypoint.sh | 23 ++ src/git/24/alpine/docker-entrypoint.sh | 23 ++ src/git/26/alpine/docker-entrypoint.sh | 23 ++ templates/docker-entrypoint.sh | 23 ++ web/src/index.ts | 224 ++++++++++++++----- web/tsconfig.json | 13 ++ 19 files changed, 624 insertions(+), 129 deletions(-) create mode 100644 web/tsconfig.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 068fece..0bea47c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,7 +123,7 @@ jobs: - name: Upload build result uses: actions/upload-artifact@v4 with: - name: build-success-${{ matrix.node_major }}-${{ matrix.bun_version }}-${{ matrix.distro }} + name: build-success-${{ matrix.bun_tag }}-${{ matrix.bun_version }}-${{ matrix.node_version }}-${{ matrix.distro }} path: build_success.json if-no-files-found: error @@ -132,7 +132,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - if: ${{ needs.build.result == 'success' }} + if: ${{ always() && needs.build.result != 'skipped' && needs.build.result != 'cancelled' && !cancelled() }} steps: - uses: actions/checkout@v5 @@ -152,6 +152,10 @@ jobs: - name: Merge version state run: | ls -la updates + if [[ -z "$(ls -A updates 2>/dev/null)" ]]; then + echo "no successful builds to finalize, skipping" + exit 0 + fi jq -s 'reduce .[] as $u ({}; . * $u)' versions.json updates/*.json > versions.json.tmp jq 'del(._needs_rebuild)' versions.json.tmp > versions.json rm -f versions.json.tmp @@ -166,7 +170,7 @@ jobs: if [ "$candidate" != "null" ]; then bun_version=$(echo "$candidate" | jq -r '.bun.latest | sub("^v"; "")') node_version=$(echo "$candidate" | jq -r '.nodejs | to_entries[0].value.version | sub("^v"; "")') - docker login -u "${DOCKER_USERNAME}" -p "${DOCKER_TOKEN}" + echo "${DOCKER_TOKEN}" | docker login -u "${DOCKER_USERNAME}" --password-stdin docker buildx imagetools create --tag "${{ env.REGISTRY }}/bun-node:latest" "${{ env.REGISTRY }}/bun-node:${bun_version}-${node_version}-debian" else echo "no latest candidate in this run, skipping" @@ -201,14 +205,14 @@ jobs: needs: [build] permissions: actions: write - if: ${{ failure() && needs.build.result == 'failure' }} + if: ${{ failure() && needs.build.result == 'failure' && github.run_attempt < 3 }} steps: - name: Rerun failed build jobs env: GH_TOKEN: ${{ github.token }} run: | failed=$(gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json jobs \ - --jq '[.jobs[] | select(.name == "build" and .conclusion == "failure") | .databaseId] | join(" ")') + --jq '[.jobs[] | select((.name | startswith("build")) and .conclusion == "failure") | .databaseId] | join(" ")') if [ -n "$failed" ]; then for job in $failed; do gh run rerun ${{ github.run_id }} --repo ${{ github.repository }} --job "$job" || true diff --git a/build_single.sh b/build_single.sh index ae3879a..d327bb6 100755 --- a/build_single.sh +++ b/build_single.sh @@ -78,8 +78,9 @@ generate_tags() { local bun_minor=${bun_version%.*} local is_canary=false - if [ "$bun_version" == "canary" ]; then + if [[ "$bun_version" == *"-canary"* ]]; then is_canary=true + bun_version="canary" fi echo "$REGISTRY/bun-node:${bun_version}-${node_version}-${distro}" @@ -115,29 +116,38 @@ fi log "Building image for Bun version $BUN_VERSION, Node version $NODE_VERSION, Distro $DISTRO" image_name="$REGISTRY/bun-node:${BUN_VERSION}-${NODE_VERSION}-${tag_distro}" -tags=($(generate_tags "$BUN_VERSION" "$NODE_VERSION" "$tag_distro")) +mapfile -t tags < <(generate_tags "$BUN_VERSION" "$NODE_VERSION" "$tag_distro") +tag_args=() for tag in "${tags[@]}"; do log "Tagging $image_name as $tag" + tag_args+=(-t "$tag") +done + +retry docker buildx build \ + --sbom=true --provenance=true \ + --platform "$PLATFORMS" \ + -t "$image_name" \ + "${tag_args[@]}" \ + --build-arg BUN_VERSION="$bun_build_arg" \ + "./src/base/${NODE_MAJOR}/${DISTRO}" \ + --push + +if [ "$DISTRO" == "alpine" ]; then + log "Building and Tagging Alpine image with Git" + git_tag_args=() + for tag in "${tags[@]}"; do + git_tag_args+=(-t "$tag-git") + done retry docker buildx build \ --sbom=true --provenance=true \ --platform "$PLATFORMS" \ - -t "$image_name" -t "$tag" \ + -t "$image_name-git" \ + "${git_tag_args[@]}" \ --build-arg BUN_VERSION="$bun_build_arg" \ - "./src/base/${NODE_MAJOR}/${DISTRO}" \ + "./src/git/${NODE_MAJOR}/alpine" \ --push - - if [ "$DISTRO" == "alpine" ]; then - log "Building and Tagging Alpine image with Git" - retry docker buildx build \ - --sbom=true --provenance=true \ - --platform "$PLATFORMS" \ - -t "$image_name-git" -t "$tag-git" \ - --build-arg BUN_VERSION="$bun_build_arg" \ - "./src/git/${NODE_MAJOR}/alpine" \ - --push - fi -done +fi cat > build_success.json < { const releases: NodeRelease[] = []; for (const [, major] of majors) { - const [latestVersion] = Object.values(major.releases); - if (!latestVersion) continue; + const versions = Object.values(major.releases); + if (versions.length === 0) continue; + + const latestVersion = versions.reduce((newest, release) => + compareVersions(release.semver.raw, newest.semver.raw) > 0 ? release : newest + ); const status = getNodeReleaseStatus(new Date(), { currentStart: major.support.phases.dates.start, @@ -102,6 +106,16 @@ async function generateReleaseData(): Promise { return releases.sort((a, b) => a.major - b.major); } +function compareVersions(a: string, b: string): number { + const pa = a.split(".").map((n) => Number(n)); + const pb = b.split(".").map((n) => Number(n)); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + function supportedMajors(releases: NodeRelease[]): NodeRelease[] { return releases.filter((r) => STATUS_KEPT.includes(r.status)); } @@ -135,7 +149,8 @@ async function getVersions(pkgName: string, tags: Array): Promise { try { - return (await Bun.file(path).json()) as VersionsState; + const parsed = (await Bun.file(path).json()) as Partial; + return { bun: parsed.bun || {}, nodejs: parsed.nodejs || {}, _needs_rebuild: parsed._needs_rebuild }; } catch { return { bun: {}, nodejs: {} }; } @@ -158,23 +173,34 @@ function flagValue(flag: string, fallback: string): string { return process.argv[index + 1] || fallback; } +function majorsArg(): number[] { + const value = flagValue("--majors", process.env.NODE_MAJOR_VERSIONS_TO_CHECK || ""); + return value.split(",").map((m) => Number(m)).filter((m) => !Number.isNaN(m)); +} + const alpineCache = new Map(); const bookwormCache = new Map(); async function getDockerNodeTag(major: number, pattern: RegExp): Promise { - const response = await fetch( - `https://hub.docker.com/v2/repositories/library/node/tags/?page_size=100&name=${major}-` - ); - if (!response.ok) return null; - const data = (await response.json()) as { results: Array<{ name: string }> }; - const matches = data.results - .map((r) => r.name) - .filter((name) => pattern.test(name)); + const names: string[] = []; + let next = `https://hub.docker.com/v2/repositories/library/node/tags/?page_size=100&name=${major}-`; + while (next) { + const response = await fetch(next); + if (!response.ok) return null; + const data = (await response.json()) as { results: Array<{ name: string }>; next: string | null }; + for (const result of data.results) names.push(result.name); + next = data.next || ""; + } + const matches = names.filter((name) => pattern.test(name)); if (matches.length === 0) return null; matches.sort((a, b) => { - const verA = parseFloat(a.split("alpine")[1] || "0"); - const verB = parseFloat(b.split("alpine")[1] || "0"); - return verB - verA; + const pa = (a.split("alpine")[1] || "0").split(".").map((n) => Number(n)); + const pb = (b.split("alpine")[1] || "0").split(".").map((n) => Number(n)); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pb[i] || 0) - (pa[i] || 0); + if (diff !== 0) return diff; + } + return 0; }); return matches[0] || null; } @@ -201,6 +227,25 @@ function argOrEnv(flag: string, envName: string, fallback: string): string { return process.env[envName] || fallback; } +async function resolveSupportedMajors(): Promise> { + const releases = supportedMajors(await generateReleaseData()); + const supported: Array<{ major: number; alpine: string }> = []; + for (const release of releases) { + const [alpine, bookworm] = await Promise.all([ + getAlpineVersion(release.major), + hasBookworm(release.major), + ]); + if (!alpine || !bookworm) { + console.error( + `skip node ${release.major}: docker-node tags unavailable (alpine=${alpine}, bookworm=${bookworm})` + ); + continue; + } + supported.push({ major: release.major, alpine }); + } + return supported; +} + async function readTemplates(): Promise> { const templates = new Map(); for (const name of [ @@ -215,23 +260,10 @@ async function readTemplates(): Promise> { } async function syncDockerfiles(): Promise<{ created: number[]; updated: number[]; removed: number[] }> { - const releases = supportedMajors(await generateReleaseData()); - const supported: Array<{ major: number; alpine: string }> = []; + const supported = await resolveSupportedMajors(); const created: number[] = []; const updated: number[] = []; - for (const release of releases) { - const [alpine, bookworm] = await Promise.all([ - getAlpineVersion(release.major), - hasBookworm(release.major), - ]); - if (!alpine || !bookworm) { - console.error(`skip node ${release.major}: docker-node tags unavailable (alpine=${alpine}, bookworm=${bookworm})`); - continue; - } - supported.push({ major: release.major, alpine }); - } - const templates = await readTemplates(); const entrypoint = await readFile(join("templates", "docker-entrypoint.sh"), "utf8"); @@ -260,9 +292,12 @@ async function syncDockerfiles(): Promise<{ created: number[]; updated: number[] track(major, isNew ? created : updated); } + let existingEntrypoint: string | null = null; try { - await readFile(join(dir, "docker-entrypoint.sh")); - } catch { + existingEntrypoint = await readFile(join(dir, "docker-entrypoint.sh"), "utf8"); + } catch {} + + if (existingEntrypoint !== entrypoint) { await writeFile(join(dir, "docker-entrypoint.sh"), entrypoint); await $`chmod +x ${join(dir, "docker-entrypoint.sh")}`; } @@ -286,18 +321,15 @@ async function syncDockerfiles(): Promise<{ created: number[]; updated: number[] } const removed: number[] = []; - for (const root of ["src/base", "src/git"]) { - const absoluteDir = join(process.cwd(), root); - const entries = await ( - await Bun.$`ls ${absoluteDir}`.quiet().text() - ).trim().split("\n").filter(Boolean); - for (const entry of entries) { - if (supported.some((s) => String(s.major) === entry)) continue; - const path = join(absoluteDir, entry); - const stats = await Bun.file(path).stat().catch(() => null); - if (stats?.isDirectory()) { - await rm(path, { recursive: true, force: true }); - track(Number(entry), removed); + if (supported.length > 0) { + for (const root of ["src/base", "src/git"]) { + const absoluteDir = join(process.cwd(), root); + const entries = await readdir(absoluteDir, { withFileTypes: true }).catch(() => []); + for (const dirent of entries) { + if (!dirent.isDirectory()) continue; + if (supported.some((s) => String(s.major) === dirent.name)) continue; + await rm(join(absoluteDir, dirent.name), { recursive: true, force: true }); + track(Number(dirent.name), removed); } } } @@ -333,12 +365,12 @@ async function generateMatrix(): Promise { const releases = await generateReleaseData(); const state = await loadVersionsState(versionsFilePath()); - const majorsArg = argOrEnv("--node", "NODE_MAJOR_VERSIONS_TO_CHECK", ""); - const majors = majorsArg - ? majorsArg.split(",").map((m) => Number(m)).filter((m) => !Number.isNaN(m)) - : supportedMajors(releases).map((r) => r.major); - - const availableReleases = releases.filter((r) => majors.includes(r.major)); + const supportedMajorsList = await resolveSupportedMajors(); + const explicitMajors = majorsArg(); + const majors = explicitMajors.length > 0 ? explicitMajors : supportedMajorsList.map((s) => s.major); + const availableReleases = releases.filter( + (r) => majors.includes(r.major) && supportedMajorsList.some((s) => s.major === r.major) + ); const distros = argOrEnv("--distros", "DISTROS", "alpine,debian-slim,debian") .split(",") .map((d) => d.trim()) @@ -437,10 +469,7 @@ async function main(): Promise { if (process.argv.includes("--node")) { const releases = supportedMajors(await generateReleaseData()); - const filter = argOrEnv("--node", "NODE_MAJOR_VERSIONS_TO_CHECK", "") - .split(",") - .map((m) => Number(m)) - .filter((m) => !Number.isNaN(m)); + const filter = majorsArg(); const state = await loadVersionsState(versionsFilePath()); const changed = releases .filter((r) => filter.length === 0 || filter.includes(r.major)) diff --git a/readme.md b/readme.md index 4192ceb..7f47e41 100644 --- a/readme.md +++ b/readme.md @@ -27,6 +27,28 @@ Use node.js as runtime, and bun as package manager, etc. The node.js in this doc docker pull imbios/bun-node ``` +## Telemetry + +The image sends **one anonymous ping per container start** to `bun-node.imbios.dev` +to help understand which versions are actually used. The payload contains only: + +- the Bun and Node.js versions in the container +- the CPU architecture +- a random id (not persisted, rotated every start) + +No IP addresses, hostnames, commands, or user data are collected, and the ping +fails silently (3s timeout, backgrounded) without affecting startup. + +**Opt out** with either: + +```bash +docker run -e BUN_NODE_TELEMETRY=0 imbios/bun-node +# or +docker run -e DO_NOT_TRACK=1 imbios/bun-node +``` + +Live (public) and owner-only telemetry dashboards live at . + ## Build Types - **alpine**: Minimal build ideal for smaller footprint diff --git a/src/base/22/alpine/docker-entrypoint.sh b/src/base/22/alpine/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/22/alpine/docker-entrypoint.sh +++ b/src/base/22/alpine/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/22/debian-slim/docker-entrypoint.sh b/src/base/22/debian-slim/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/22/debian-slim/docker-entrypoint.sh +++ b/src/base/22/debian-slim/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/22/debian/docker-entrypoint.sh b/src/base/22/debian/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/22/debian/docker-entrypoint.sh +++ b/src/base/22/debian/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/24/alpine/docker-entrypoint.sh b/src/base/24/alpine/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/24/alpine/docker-entrypoint.sh +++ b/src/base/24/alpine/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/24/debian-slim/docker-entrypoint.sh b/src/base/24/debian-slim/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/24/debian-slim/docker-entrypoint.sh +++ b/src/base/24/debian-slim/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/24/debian/docker-entrypoint.sh b/src/base/24/debian/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/24/debian/docker-entrypoint.sh +++ b/src/base/24/debian/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/26/alpine/docker-entrypoint.sh b/src/base/26/alpine/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/26/alpine/docker-entrypoint.sh +++ b/src/base/26/alpine/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/26/debian-slim/docker-entrypoint.sh b/src/base/26/debian-slim/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/26/debian-slim/docker-entrypoint.sh +++ b/src/base/26/debian-slim/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/26/debian/docker-entrypoint.sh b/src/base/26/debian/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/base/26/debian/docker-entrypoint.sh +++ b/src/base/26/debian/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/git/22/alpine/docker-entrypoint.sh b/src/git/22/alpine/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/git/22/alpine/docker-entrypoint.sh +++ b/src/git/22/alpine/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/git/24/alpine/docker-entrypoint.sh b/src/git/24/alpine/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/git/24/alpine/docker-entrypoint.sh +++ b/src/git/24/alpine/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/git/26/alpine/docker-entrypoint.sh b/src/git/26/alpine/docker-entrypoint.sh index a0e45cb..c076656 100755 --- a/src/git/26/alpine/docker-entrypoint.sh +++ b/src/git/26/alpine/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/templates/docker-entrypoint.sh b/templates/docker-entrypoint.sh index a0e45cb..c076656 100644 --- a/templates/docker-entrypoint.sh +++ b/templates/docker-entrypoint.sh @@ -1,6 +1,29 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# Sends one tiny ping per container start (bun version, node version, arch, +# random id). No IPs, no hostnames, no user data. Opt out by setting +# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then + ( + BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) + NODE_VERSION_TELEMETRY=$(node --version 2>/dev/null || echo unknown) + ARCH_TELEMETRY=$(uname -m 2>/dev/null || echo unknown) + ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") + DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) + PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" + if command -v curl >/dev/null 2>&1; then + curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + elif command -v wget >/dev/null 2>&1; then + wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ + --header="Content-Type: application/json" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 + fi + ) & +fi + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/web/src/index.ts b/web/src/index.ts index 841aef8..d7b8dce 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -13,9 +13,11 @@ interface DockerHubRepo { last_updated: string; } -interface TagInfo { - name: string; - last_updated: string; +interface TelemetryDay { + count: number; + byBun: Record; + byNode: Record; + byArch: Record; } const CACHE_TTL = 120; @@ -39,7 +41,7 @@ async function fetchWithRetry(url: string, attempts = 3): Promise { if (!env.SEED_KEY || auth !== `Bearer ${env.SEED_KEY}`) { return new Response("forbidden", { status: 403, headers: { "Content-Type": "text/plain" } }); } - let body: { docker?: DockerHubRepo & { count?: number }; github?: { stargazers_count?: number; forks_count?: number; open_issues_count?: number; pushed_at?: string } }; + let body: { docker?: Partial & { count?: number }; github?: Partial<{ stargazers_count: number; forks_count: number; open_issues_count: number; pushed_at: string }> }; try { body = await request.json(); } catch { return new Response("invalid json", { status: 400, headers: { "Content-Type": "text/plain" } }); } const writes: Array> = []; - if (body.docker) { + + if (body.docker && (body.docker.pull_count != null || body.docker.star_count != null || body.docker.last_updated != null)) { + const existing = (await env.STATS_KV.get("cache:docker", "json").catch(() => null)) as Partial | null; const docker = { - pull_count: body.docker.pull_count ?? 0, - star_count: body.docker.star_count ?? 0, - last_updated: body.docker.last_updated ?? "", + pull_count: body.docker.pull_count ?? existing?.pull_count ?? 0, + star_count: body.docker.star_count ?? existing?.star_count ?? 0, + last_updated: body.docker.last_updated ?? existing?.last_updated ?? "", }; writes.push(env.STATS_KV.put("cache:docker", JSON.stringify(docker), { expirationTtl: 12 * 3600 })); - if (body.docker.count != null) { - writes.push(env.STATS_KV.put("cache:docker-tags", JSON.stringify({ count: body.docker.count }), { expirationTtl: 12 * 3600 })); - } } - if (body.github) { + if (body.docker?.count != null) { + writes.push(env.STATS_KV.put("cache:docker-tags", JSON.stringify({ count: body.docker.count }), { expirationTtl: 12 * 3600 })); + } + if (body.github && (body.github.stargazers_count != null || body.github.forks_count != null || body.github.open_issues_count != null || body.github.pushed_at != null)) { + const existing = (await env.STATS_KV.get("cache:github", "json").catch(() => null)) as Partial<{ stargazers_count: number; forks_count: number; open_issues_count: number; pushed_at: string }> | null; const gh = { - stargazers_count: body.github.stargazers_count ?? 0, - forks_count: body.github.forks_count ?? 0, - open_issues_count: body.github.open_issues_count ?? 0, - pushed_at: body.github.pushed_at ?? "", + stargazers_count: body.github.stargazers_count ?? existing?.stargazers_count ?? 0, + forks_count: body.github.forks_count ?? existing?.forks_count ?? 0, + open_issues_count: body.github.open_issues_count ?? existing?.open_issues_count ?? 0, + pushed_at: body.github.pushed_at ?? existing?.pushed_at ?? "", }; writes.push(env.STATS_KV.put("cache:github", JSON.stringify(gh), { expirationTtl: 12 * 3600 })); } @@ -135,6 +140,71 @@ async function seedStats(request: Request, env: Env): Promise { return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); } +async function handleTelemetryPing(request: Request, env: Env): Promise { + if (request.method !== "POST") { + return new Response("method not allowed", { status: 405, headers: { "Content-Type": "text/plain" } }); + } + let body: { v?: number; id?: string; bun?: string; node?: string; arch?: string; d?: string }; + try { + body = await request.json(); + } catch { + return new Response("bad request", { status: 400, headers: { "Content-Type": "text/plain" } }); + } + const day = typeof body.d === "string" && /^\d{4}-\d{2}-\d{2}$/.test(body.d) ? body.d : new Date().toISOString().slice(0, 10); + const id = typeof body.id === "string" && body.id.length > 0 && body.id.length <= 64 ? body.id : ""; + const bun = typeof body.bun === "string" ? body.bun.slice(0, 40) : ""; + const node = typeof body.node === "string" ? body.node.slice(0, 40) : ""; + const arch = typeof body.arch === "string" ? body.arch.slice(0, 20) : ""; + + if (id) { + const seen = await env.STATS_KV.get(`telemetry:seen:${id}`).catch(() => null); + if (seen) return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); + await env.STATS_KV.put(`telemetry:seen:${id}`, "1", { expirationTtl: 3600 }).catch(() => {}); + } + + const key = `telemetry:${day}`; + const current = (await env.STATS_KV.get(key, "json").catch(() => null)) as TelemetryDay | null; + if (current && current.count >= 200_000) { + return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); + } + const next: TelemetryDay = { + count: (current?.count || 0) + 1, + byBun: current?.byBun || {}, + byNode: current?.byNode || {}, + byArch: current?.byArch || {}, + }; + if (bun) next.byBun[bun] = (next.byBun[bun] || 0) + 1; + if (node) next.byNode[node] = (next.byNode[node] || 0) + 1; + if (arch) next.byArch[arch] = (next.byArch[arch] || 0) + 1; + await env.STATS_KV.put(key, JSON.stringify(next)).catch(() => {}); + return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); +} + +async function telemetryTotals(env: Env, days: number): Promise<{ count: number; byNode: Record; byBun: Record; byArch: Record; days: Array<{ date: string; count: number }> }> { + const keys = await env.STATS_KV.list({ prefix: "telemetry:", limit: 1000 }); + const cutoff = new Date(Date.now() - days * 86400_000).toISOString().slice(0, 10); + const totals = { count: 0, byNode: {} as Record, byBun: {} as Record, byArch: {} as Record }; + const perDay: Array<{ date: string; count: number }> = []; + for (const key of keys.keys) { + const date = key.name.replace("telemetry:", ""); + if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || date < cutoff) continue; + const value = (await env.STATS_KV.get(key.name, "json").catch(() => null)) as TelemetryDay | null; + if (!value) continue; + totals.count += value.count; + for (const [k, v] of Object.entries(value.byNode)) totals.byNode[k] = (totals.byNode[k] || 0) + v; + for (const [k, v] of Object.entries(value.byBun)) totals.byBun[k] = (totals.byBun[k] || 0) + v; + for (const [k, v] of Object.entries(value.byArch)) totals.byArch[k] = (totals.byArch[k] || 0) + v; + perDay.push({ date, count: value.count }); + } + perDay.sort((a, b) => a.date.localeCompare(b.date)); + return { ...totals, days: perDay }; +} + +function topEntries(record: Record, limit = 6): Array<[string, number]> { + return Object.entries(record) + .sort((a, b) => b[1] - a[1]) + .slice(0, limit); +} async function pullHistory(env: Env): Promise> { const keys = await env.STATS_KV.list({ prefix: "pulls:", limit: 400 }); @@ -162,9 +232,11 @@ function escapeHtml(input: string): string { } function badge(label: string, value: string, color: string): string { - const labelWidth = 70; + const labelWidth = Math.max(70, Math.ceil(label.length * 6.4 + 14)); const valueWidth = 110; const total = labelWidth + valueWidth; + const labelCenter = labelWidth * 10 / 2; + const valueCenter = (labelWidth + valueWidth / 2) * 10; return ` @@ -177,15 +249,23 @@ function badge(label: string, value: string, color: string): string { - ${escapeHtml(label)} - ${escapeHtml(value)} - ${escapeHtml(value)} + ${escapeHtml(label)} + ${escapeHtml(label)} + ${escapeHtml(value)} + ${escapeHtml(value)} `; } -function dashboard(stats: Awaited>, views: Record, analyticsToken: string): string { +function dashboard( + stats: Awaited>, + views: Record, + telemetry: Awaited>, + analyticsToken: string +): string { const pulls = stats.docker.pulls ?? 0; + const today = new Date().toISOString().slice(0, 10); + const viewsToday = views[today] || 0; const beacon = analyticsToken ? `` : ""; @@ -211,9 +291,6 @@ ${beacon} .card .value { font-size: 26px; font-weight: 700; margin-top: 6px; } .card .hint { font-size: 11px; color: #5f6673; margin-top: 6px; } .hl { color: #9ece6a; } - .table { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 13px; } - .table th, .table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #1f2530; } - .table th { color: #8b93a3; font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; } .foot { margin-top: 40px; color: #5f6673; font-size: 11px; line-height: 1.8; } @@ -223,35 +300,47 @@ ${beacon}

Pre-configured Bun + Node.js Docker images — rebuilt daily. Source: github.com/ImBIOS/bun-node · Registry: hub.docker.com/r/imbios/bun-node

Docker pulls
${fmt.format(pulls)}
all-time, all tags
+
Container starts
${fmt.format(telemetry.count)}
anonymous, last 30 days
Docker tags
${fmt.format(stats.docker.tags ?? 0)}
published image tags
Docker stars
${stats.docker.stars ?? 0}
on Docker Hub
GitHub stars
${stats.github.stars ?? 0}
⭐ the repo on GitHub
GitHub forks
${stats.github.forks ?? 0}
forks of the repo
Last build
${escapeHtml((stats.docker.lastUpdated ?? "unknown").slice(0, 10))}
newest tag pushed
-
Views today
${Object.values(views).reduce((a, b) => a + b, 0) || "–"}
on this page
-
Open issues
${stats.github.openIssues ?? 0}
on GitHub
+
Views today
${viewsToday || "–"}
on this page

Embeddable badges:

docker pulls docker tags docker stars + container starts last updated

API

-

/api/stats — JSON snapshot · /badge/<metric>.svg — badges (pulls, tags, stars, last-updated)

+

/api/stats — JSON snapshot · /badge/<metric>.svg — badges (pulls, tags, stars, starts, last-updated)

- Page telemetry is collected by Cloudflare Web Analytics and is only visible to the repository owner. - Daily pull snapshots are stored in KV and exposed on the private page. + The image sends one anonymous ping per container start (bun/node versions, architecture, random id — no IPs or user data). + Opt out by setting BUN_NODE_TELEMETRY=0 or DO_NOT_TRACK=1. See the repo readme.
`; } -function privatePage(stats: Awaited>, history: Array<{ date: string; pulls: number }>, views: Record): string { +function privatePage( + stats: Awaited>, + history: Array<{ date: string; pulls: number }>, + views: Record, + telemetry: Awaited> +): string { const points = history.map((h) => `${h.date}:${h.pulls}`).join("|"); const viewPoints = Object.entries(views).sort(([a], [b]) => a.localeCompare(b)).map(([d, v]) => `${d}:${v}`).join("|"); + const telemetryPoints = telemetry.days.map((d) => `${d.date}:${d.count}`).join("|"); + const nodeRows = topEntries(telemetry.byNode).map(([k, v]) => `${escapeHtml(k)}${fmt.format(v)}`).join(""); + const bunRows = topEntries(telemetry.byBun).map(([k, v]) => `${escapeHtml(k)}${fmt.format(v)}`).join(""); + const archRows = topEntries(telemetry.byArch).map(([k, v]) => `${escapeHtml(k)}${fmt.format(v)}`).join(""); + const today = new Date().toISOString().slice(0, 10); + const startsToday = telemetry.days.filter((d) => d.date === today).reduce((a, d) => a + d.count, 0); return ` @@ -272,47 +361,69 @@ function privatePage(stats: Awaited>, history: A .card { background: #13161c; border: 1px solid #1f2530; border-radius: 10px; padding: 16px; } .card .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #8b93a3; } .card .value { font-size: 24px; font-weight: 700; margin-top: 6px; } + .table { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 13px; } + .table th, .table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #1f2530; } + .table th { color: #8b93a3; font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; } + .cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 24px; } .foot { margin-top: 32px; color: #5f6673; font-size: 11px; line-height: 1.8; }

🔒 bun-node private stats owner only

-

Daily Docker Hub pull snapshots, stored in worker KV. Visible only with the private key.

+

Daily Docker Hub pull snapshots and anonymous container telemetry, stored in worker KV. Visible only with the private key.

Pulls today
${history.length ? fmt.format(history[history.length - 1]!.pulls) : "–"}
Pulls ~30d ago
${history.length > 30 ? fmt.format(history[history.length - 31]!.pulls) : "–"}
30d delta
${history.length > 30 ? "+" + fmt.format(history[history.length - 1]!.pulls - history[history.length - 31]!.pulls) : "–"}
GitHub stars today
${stats.github.stars ?? "–"}
+
Container starts today
${startsToday || "–"}
+
Container starts (30d)
${fmt.format(telemetry.count)}
- - + + + + + - + -

Top chart: total pulls per day. Bottom chart: page views per day.

+

Charts: total pulls per day · page views per day · container starts per day.

+
+

Node versions (30d)

${nodeRows || ""}
versionstarts
no data yet
+

Bun versions (30d)

${bunRows || ""}
versionstarts
no data yet
+

Architecture (30d)

${archRows || ""}
archstarts
no data yet
+
If Cloudflare Web Analytics is enabled, visit the dashboard in the Cloudflare account for full traffic telemetry.
`; @@ -332,11 +443,12 @@ async function handleStats(env: Env): Promise { } async function handleBadge(metric: string, env: Env): Promise { - const stats = await collectStats(env); + const [stats, telemetry] = await Promise.all([collectStats(env), telemetryTotals(env, 30)]); const map: Record = { pulls: ["docker pulls", fmt.format(stats.docker.pulls ?? 0), "#1f6feb"], tags: ["docker tags", fmt.format(stats.docker.tags ?? 0), "#8957e5"], stars: ["docker stars", fmt.format(stats.docker.stars ?? 0), "#e3b341"], + starts: ["container starts", `${fmt.format(telemetry.count)} / 30d`, "#9ece6a"], "last-updated": ["last updated", (stats.docker.lastUpdated ?? "unknown").slice(0, 10), "#3fb950"], }; const entry = map[metric] || map["pulls"]!; @@ -345,13 +457,15 @@ async function handleBadge(metric: string, env: Env): Promise { }); } -async function handlePrivate(url: URL, env: Env): Promise { - const key = url.searchParams.get("key") || ""; +async function handlePrivate(request: Request, env: Env): Promise { + const url = new URL(request.url); + const headerKey = (request.headers.get("Authorization") || "").replace(/^Bearer\s+/i, ""); + const key = headerKey || url.searchParams.get("key") || ""; if (!env.PRIVATE_KEY || key !== env.PRIVATE_KEY) { return new Response("forbidden", { status: 403, headers: { "Content-Type": "text/plain" } }); } - const [stats, history, views] = await Promise.all([collectStats(env), pullHistory(env), pageViews(env)]); - return htmlResponse(privatePage(stats, history, views)); + const [stats, history, views, telemetry] = await Promise.all([collectStats(env), pullHistory(env), pageViews(env), telemetryTotals(env, 30)]); + return htmlResponse(privatePage(stats, history, views, telemetry)); } async function countView(env: Env): Promise { @@ -362,7 +476,7 @@ async function countView(env: Env): Promise { } export default { - async scheduled(env: Env): Promise { + async scheduled(_controller: ScheduledController, env: Env): Promise { const today = new Date().toISOString().slice(0, 10); const stats = await collectStats(env); if (stats.docker.pulls != null) await env.STATS_KV.put(`pulls:${today}`, String(stats.docker.pulls)); @@ -379,8 +493,8 @@ export default { if (request.method === "GET" && path === "/") { await countView(env); - const [stats, views] = await Promise.all([collectStats(env), pageViews(env)]); - return htmlResponse(dashboard(stats, views, env.WEB_ANALYTICS_TOKEN || "")); + const [stats, views, telemetry] = await Promise.all([collectStats(env), pageViews(env), telemetryTotals(env, 30)]); + return htmlResponse(dashboard(stats, views, telemetry, env.WEB_ANALYTICS_TOKEN || "")); } if (request.method === "GET" && path === "/api/stats") { @@ -395,8 +509,12 @@ export default { return seedStats(request, env); } + if (request.method === "POST" && path === "/telemetry/ping") { + return handleTelemetryPing(request, env); + } + if (request.method === "GET" && path === "/private") { - return handlePrivate(url, env); + return handlePrivate(request, env); } return new Response("not found", { status: 404 }); diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..e382728 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "lib": ["es2022", "dom"], + "types": ["@cloudflare/workers-types"], + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2022", + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/index.ts"] +} From afc28894ee95d3f50c3e47666f0470d5425ec481 Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Fri, 14 Aug 2026 13:12:57 +0700 Subject: [PATCH 3/7] fix: SonarCloud findings on entrypoint telemetry and reduce - curl in docker-entrypoint.sh enforces https on redirects (shell:S6506) - versions.reduce seeded with the first element (typescript:S6959) --- check-bun-node.ts | 5 +++-- src/base/22/alpine/docker-entrypoint.sh | 2 +- src/base/22/debian-slim/docker-entrypoint.sh | 2 +- src/base/22/debian/docker-entrypoint.sh | 2 +- src/base/24/alpine/docker-entrypoint.sh | 2 +- src/base/24/debian-slim/docker-entrypoint.sh | 2 +- src/base/24/debian/docker-entrypoint.sh | 2 +- src/base/26/alpine/docker-entrypoint.sh | 2 +- src/base/26/debian-slim/docker-entrypoint.sh | 2 +- src/base/26/debian/docker-entrypoint.sh | 2 +- src/git/22/alpine/docker-entrypoint.sh | 2 +- src/git/24/alpine/docker-entrypoint.sh | 2 +- src/git/26/alpine/docker-entrypoint.sh | 2 +- templates/docker-entrypoint.sh | 2 +- 14 files changed, 16 insertions(+), 15 deletions(-) diff --git a/check-bun-node.ts b/check-bun-node.ts index 4705589..6e684ba 100644 --- a/check-bun-node.ts +++ b/check-bun-node.ts @@ -83,8 +83,9 @@ async function generateReleaseData(): Promise { const versions = Object.values(major.releases); if (versions.length === 0) continue; - const latestVersion = versions.reduce((newest, release) => - compareVersions(release.semver.raw, newest.semver.raw) > 0 ? release : newest + const latestVersion = versions.slice(1).reduce( + (newest, release) => (compareVersions(release.semver.raw, newest.semver.raw) > 0 ? release : newest), + versions[0]! ); const status = getNodeReleaseStatus(new Date(), { diff --git a/src/base/22/alpine/docker-entrypoint.sh b/src/base/22/alpine/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/22/alpine/docker-entrypoint.sh +++ b/src/base/22/alpine/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/22/debian-slim/docker-entrypoint.sh b/src/base/22/debian-slim/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/22/debian-slim/docker-entrypoint.sh +++ b/src/base/22/debian-slim/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/22/debian/docker-entrypoint.sh b/src/base/22/debian/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/22/debian/docker-entrypoint.sh +++ b/src/base/22/debian/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/24/alpine/docker-entrypoint.sh b/src/base/24/alpine/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/24/alpine/docker-entrypoint.sh +++ b/src/base/24/alpine/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/24/debian-slim/docker-entrypoint.sh b/src/base/24/debian-slim/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/24/debian-slim/docker-entrypoint.sh +++ b/src/base/24/debian-slim/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/24/debian/docker-entrypoint.sh b/src/base/24/debian/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/24/debian/docker-entrypoint.sh +++ b/src/base/24/debian/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/26/alpine/docker-entrypoint.sh b/src/base/26/alpine/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/26/alpine/docker-entrypoint.sh +++ b/src/base/26/alpine/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/26/debian-slim/docker-entrypoint.sh b/src/base/26/debian-slim/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/26/debian-slim/docker-entrypoint.sh +++ b/src/base/26/debian-slim/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/base/26/debian/docker-entrypoint.sh b/src/base/26/debian/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/base/26/debian/docker-entrypoint.sh +++ b/src/base/26/debian/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/git/22/alpine/docker-entrypoint.sh b/src/git/22/alpine/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/git/22/alpine/docker-entrypoint.sh +++ b/src/git/22/alpine/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/git/24/alpine/docker-entrypoint.sh b/src/git/24/alpine/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/git/24/alpine/docker-entrypoint.sh +++ b/src/git/24/alpine/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/src/git/26/alpine/docker-entrypoint.sh b/src/git/26/alpine/docker-entrypoint.sh index c076656..f6fb25f 100755 --- a/src/git/26/alpine/docker-entrypoint.sh +++ b/src/git/26/alpine/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ diff --git a/templates/docker-entrypoint.sh b/templates/docker-entrypoint.sh index c076656..f6fb25f 100644 --- a/templates/docker-entrypoint.sh +++ b/templates/docker-entrypoint.sh @@ -14,7 +14,7 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ From 1d1d620e0359957801bae1296420f336f97af5b3 Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Fri, 14 Aug 2026 13:22:10 +0700 Subject: [PATCH 4/7] fix: ship curl in runtime images for telemetry ping - alpine and debian-slim runtime stages now install curl (debian already had it); busybox wget does not support --https-only so the entrypoint uses curl exclusively, which also clears shell:S6506 --- src/base/22/alpine/docker-entrypoint.sh | 10 ++-------- src/base/22/alpine/dockerfile | 2 +- src/base/22/debian-slim/docker-entrypoint.sh | 10 ++-------- src/base/22/debian-slim/dockerfile | 6 +++++- src/base/22/debian/docker-entrypoint.sh | 10 ++-------- src/base/24/alpine/docker-entrypoint.sh | 10 ++-------- src/base/24/alpine/dockerfile | 2 +- src/base/24/debian-slim/docker-entrypoint.sh | 10 ++-------- src/base/24/debian-slim/dockerfile | 6 +++++- src/base/24/debian/docker-entrypoint.sh | 10 ++-------- src/base/26/alpine/docker-entrypoint.sh | 10 ++-------- src/base/26/alpine/dockerfile | 2 +- src/base/26/debian-slim/docker-entrypoint.sh | 10 ++-------- src/base/26/debian-slim/dockerfile | 6 +++++- src/base/26/debian/docker-entrypoint.sh | 10 ++-------- src/git/22/alpine/docker-entrypoint.sh | 10 ++-------- src/git/24/alpine/docker-entrypoint.sh | 10 ++-------- src/git/26/alpine/docker-entrypoint.sh | 10 ++-------- templates/alpine.dockerfile | 2 +- templates/debian-slim.dockerfile | 6 +++++- templates/docker-entrypoint.sh | 10 ++-------- 21 files changed, 50 insertions(+), 112 deletions(-) diff --git a/src/base/22/alpine/docker-entrypoint.sh b/src/base/22/alpine/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/22/alpine/docker-entrypoint.sh +++ b/src/base/22/alpine/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/22/alpine/dockerfile b/src/base/22/alpine/dockerfile index 9489e75..73b8c4b 100644 --- a/src/base/22/alpine/dockerfile +++ b/src/base/22/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add libgcc libstdc++ curl \ && which bun \ && which bunx \ && bun --version diff --git a/src/base/22/debian-slim/docker-entrypoint.sh b/src/base/22/debian-slim/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/22/debian-slim/docker-entrypoint.sh +++ b/src/base/22/debian-slim/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/22/debian-slim/dockerfile b/src/base/22/debian-slim/dockerfile index edd9f0b..f039796 100644 --- a/src/base/22/debian-slim/dockerfile +++ b/src/base/22/debian-slim/dockerfile @@ -67,7 +67,11 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN groupadd bun \ +RUN apt-get update -qq \ + && apt-get install -qq --no-install-recommends curl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd bun \ --gid 1001 \ && useradd bun \ --uid 1001 \ diff --git a/src/base/22/debian/docker-entrypoint.sh b/src/base/22/debian/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/22/debian/docker-entrypoint.sh +++ b/src/base/22/debian/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/24/alpine/docker-entrypoint.sh b/src/base/24/alpine/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/24/alpine/docker-entrypoint.sh +++ b/src/base/24/alpine/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/24/alpine/dockerfile b/src/base/24/alpine/dockerfile index 7d29ef5..c255654 100644 --- a/src/base/24/alpine/dockerfile +++ b/src/base/24/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add libgcc libstdc++ curl \ && which bun \ && which bunx \ && bun --version diff --git a/src/base/24/debian-slim/docker-entrypoint.sh b/src/base/24/debian-slim/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/24/debian-slim/docker-entrypoint.sh +++ b/src/base/24/debian-slim/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/24/debian-slim/dockerfile b/src/base/24/debian-slim/dockerfile index 38b0eeb..767923f 100644 --- a/src/base/24/debian-slim/dockerfile +++ b/src/base/24/debian-slim/dockerfile @@ -67,7 +67,11 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN groupadd bun \ +RUN apt-get update -qq \ + && apt-get install -qq --no-install-recommends curl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd bun \ --gid 1001 \ && useradd bun \ --uid 1001 \ diff --git a/src/base/24/debian/docker-entrypoint.sh b/src/base/24/debian/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/24/debian/docker-entrypoint.sh +++ b/src/base/24/debian/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/26/alpine/docker-entrypoint.sh b/src/base/26/alpine/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/26/alpine/docker-entrypoint.sh +++ b/src/base/26/alpine/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/26/alpine/dockerfile b/src/base/26/alpine/dockerfile index 61e0e09..428386c 100644 --- a/src/base/26/alpine/dockerfile +++ b/src/base/26/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add libgcc libstdc++ curl \ && which bun \ && which bunx \ && bun --version diff --git a/src/base/26/debian-slim/docker-entrypoint.sh b/src/base/26/debian-slim/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/26/debian-slim/docker-entrypoint.sh +++ b/src/base/26/debian-slim/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/base/26/debian-slim/dockerfile b/src/base/26/debian-slim/dockerfile index 2c535c7..b794ed1 100644 --- a/src/base/26/debian-slim/dockerfile +++ b/src/base/26/debian-slim/dockerfile @@ -67,7 +67,11 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN groupadd bun \ +RUN apt-get update -qq \ + && apt-get install -qq --no-install-recommends curl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd bun \ --gid 1001 \ && useradd bun \ --uid 1001 \ diff --git a/src/base/26/debian/docker-entrypoint.sh b/src/base/26/debian/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/base/26/debian/docker-entrypoint.sh +++ b/src/base/26/debian/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/git/22/alpine/docker-entrypoint.sh b/src/git/22/alpine/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/git/22/alpine/docker-entrypoint.sh +++ b/src/git/22/alpine/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/git/24/alpine/docker-entrypoint.sh b/src/git/24/alpine/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/git/24/alpine/docker-entrypoint.sh +++ b/src/git/24/alpine/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/src/git/26/alpine/docker-entrypoint.sh b/src/git/26/alpine/docker-entrypoint.sh index f6fb25f..7474d0a 100755 --- a/src/git/26/alpine/docker-entrypoint.sh +++ b/src/git/26/alpine/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi diff --git a/templates/alpine.dockerfile b/templates/alpine.dockerfile index 269e536..82438bd 100644 --- a/templates/alpine.dockerfile +++ b/templates/alpine.dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add libgcc libstdc++ curl \ && which bun \ && which bunx \ && bun --version diff --git a/templates/debian-slim.dockerfile b/templates/debian-slim.dockerfile index 109b9ae..d0e9ea4 100644 --- a/templates/debian-slim.dockerfile +++ b/templates/debian-slim.dockerfile @@ -67,7 +67,11 @@ ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun -RUN groupadd bun \ +RUN apt-get update -qq \ + && apt-get install -qq --no-install-recommends curl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd bun \ --gid 1001 \ && useradd bun \ --uid 1001 \ diff --git a/templates/docker-entrypoint.sh b/templates/docker-entrypoint.sh index f6fb25f..7474d0a 100644 --- a/templates/docker-entrypoint.sh +++ b/templates/docker-entrypoint.sh @@ -13,14 +13,8 @@ if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; the ID_TELEMETRY=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$$-$(date +%s)") DAY_TELEMETRY=$(date -u +%F 2>/dev/null || date +%F) PAYLOAD_TELEMETRY="{\"v\":1,\"id\":\"$ID_TELEMETRY\",\"bun\":\"$BUN_VERSION_TELEMETRY\",\"node\":\"$NODE_VERSION_TELEMETRY\",\"arch\":\"$ARCH_TELEMETRY\",\"d\":\"$DAY_TELEMETRY\"}" - if command -v curl >/dev/null 2>&1; then - curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - elif command -v wget >/dev/null 2>&1; then - wget -q -T 3 -O /dev/null --post-data="$PAYLOAD_TELEMETRY" \ - --header="Content-Type: application/json" \ - https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 - fi + curl -fsS -m 3 --proto '=https' --proto-redir '=https' -X POST -H "Content-Type: application/json" -d "$PAYLOAD_TELEMETRY" \ + https://bun-node.imbios.dev/telemetry/ping >/dev/null 2>&1 ) & fi From 2b23d0fd2a10632436c352f57dadb02577d9e285 Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Sun, 16 Aug 2026 11:33:56 +0700 Subject: [PATCH 5/7] fix: address CodeRabbit review findings - release.yml: build_success.json renamed per combination before upload (merge-multiple flattened same-named files, finalize was merging a single combo); build/finalize check out the SHA synced by setup so freshly generated Dockerfiles are actually used; finalize gates latest/ upload/summary on updates_found; checkout uses persist-credentials: false outside setup - check-bun-node.ts: getDockerNodeTag treats 404 as missing tag but retries (429/5xx) and throws otherwise so a transient Docker Hub failure no longer prunes supported majors; generateMatrix split into matrixFromForcedInputs/matrixFromStateDiff; forced builds fall back to the major codename when the exact version is unknown - web: handleTelemetryPing split into parse/dedupe/aggregate helpers; ping date is the server UTC date (client body.d ignored); telemetry/ views KV reads and writes are best-effort so the public page never 500s - sonar-project.properties: one ruleKey+resourceKey per criterion (e1-e4) - docs: telemetry payload, source-IP exposure and 1h id retention now documented accurately in readme and entrypoint --- .github/workflows/release.yml | 26 +++- check-bun-node.ts | 154 +++++++++++-------- readme.md | 8 +- sonar-project.properties | 10 +- src/base/22/alpine/docker-entrypoint.sh | 9 +- src/base/22/debian-slim/docker-entrypoint.sh | 9 +- src/base/22/debian/docker-entrypoint.sh | 9 +- src/base/24/alpine/docker-entrypoint.sh | 9 +- src/base/24/debian-slim/docker-entrypoint.sh | 9 +- src/base/24/debian/docker-entrypoint.sh | 9 +- src/base/26/alpine/docker-entrypoint.sh | 9 +- src/base/26/debian-slim/docker-entrypoint.sh | 9 +- src/base/26/debian/docker-entrypoint.sh | 9 +- src/git/22/alpine/docker-entrypoint.sh | 9 +- src/git/24/alpine/docker-entrypoint.sh | 9 +- src/git/26/alpine/docker-entrypoint.sh | 9 +- templates/docker-entrypoint.sh | 9 +- web/src/index.ts | 78 ++++++---- 18 files changed, 259 insertions(+), 134 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0bea47c..b58fd08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,6 +34,7 @@ jobs: contents: write outputs: matrix: ${{ steps.matrix.outputs.matrix }} + sync_sha: ${{ steps.sync.outputs.sync_sha }} steps: - uses: actions/checkout@v5 @@ -68,6 +69,7 @@ jobs: https://bun-node.imbios.dev/internal/seed - name: Sync supported Node.js majors + id: sync run: | bun run check-bun-node.ts --sync --versions versions.json if [[ -n "$(git status --porcelain)" ]]; then @@ -76,6 +78,9 @@ jobs: git add src templates git commit -m "chore: sync Dockerfiles with supported Node.js majors" git push origin main + echo "sync_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + else + echo "sync_sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT" fi - name: Generate build matrix @@ -99,6 +104,9 @@ jobs: matrix: ${{ fromJson(needs.setup.outputs.matrix) }} steps: - uses: actions/checkout@v5 + with: + ref: ${{ needs.setup.outputs.sync_sha }} + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -120,21 +128,29 @@ jobs: REGISTRY: ${{ env.REGISTRY }} PLATFORMS: ${{ env.PLATFORMS }} + - name: Name the build result per combination + run: | + mv build_success.json \ + "build_success-${{ matrix.bun_tag }}-${{ matrix.bun_version }}-${{ matrix.node_version }}-${{ matrix.distro }}.json" + - name: Upload build result uses: actions/upload-artifact@v4 with: name: build-success-${{ matrix.bun_tag }}-${{ matrix.bun_version }}-${{ matrix.node_version }}-${{ matrix.distro }} - path: build_success.json + path: build_success-*.json if-no-files-found: error finalize: - needs: build + needs: [setup, build] runs-on: ubuntu-latest permissions: contents: write if: ${{ always() && needs.build.result != 'skipped' && needs.build.result != 'cancelled' && !cancelled() }} steps: - uses: actions/checkout@v5 + with: + ref: ${{ needs.setup.outputs.sync_sha }} + persist-credentials: false - name: Download version state run: | @@ -150,18 +166,22 @@ jobs: path: updates - name: Merge version state + id: merge run: | ls -la updates if [[ -z "$(ls -A updates 2>/dev/null)" ]]; then echo "no successful builds to finalize, skipping" + echo "updates_found=false" >> "$GITHUB_OUTPUT" exit 0 fi + echo "updates_found=true" >> "$GITHUB_OUTPUT" jq -s 'reduce .[] as $u ({}; . * $u)' versions.json updates/*.json > versions.json.tmp jq 'del(._needs_rebuild)' versions.json.tmp > versions.json rm -f versions.json.tmp cat versions.json - name: Point latest at the newest candidate + if: ${{ steps.merge.outputs.updates_found == 'true' }} env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} @@ -177,6 +197,7 @@ jobs: fi - name: Upload version state + if: ${{ steps.merge.outputs.updates_found == 'true' }} run: | gh release create "${{ env.VERSIONS_RELEASE }}" versions.json --title "Versions State" --notes "" || \ gh release upload "${{ env.VERSIONS_RELEASE }}" versions.json --clobber @@ -184,6 +205,7 @@ jobs: GH_TOKEN: ${{ github.token }} - name: Write release summary + if: ${{ steps.merge.outputs.updates_found == 'true' }} run: | { echo "## Release summary" diff --git a/check-bun-node.ts b/check-bun-node.ts index 6e684ba..1d42525 100644 --- a/check-bun-node.ts +++ b/check-bun-node.ts @@ -182,12 +182,28 @@ function majorsArg(): number[] { const alpineCache = new Map(); const bookwormCache = new Map(); +async function fetchWithTransientRetry(url: string, major: number): Promise { + let lastStatus = 0; + for (let attempt = 0; attempt < 3; attempt++) { + const response = await fetch(url); + if (response.ok || response.status === 404) return response; + lastStatus = response.status; + if (![429, 500, 502, 503].includes(response.status)) return response; + await new Promise((r) => setTimeout(r, 1500 * Math.pow(2, attempt))); + } + throw new Error(`docker hub request failed for node ${major}: ${lastStatus}`); +} + async function getDockerNodeTag(major: number, pattern: RegExp): Promise { const names: string[] = []; let next = `https://hub.docker.com/v2/repositories/library/node/tags/?page_size=100&name=${major}-`; while (next) { - const response = await fetch(next); - if (!response.ok) return null; + const response = await fetchWithTransientRetry(next, major); + if (response === null) return null; + if (response.status === 404) return null; + if (!response.ok) { + throw new Error(`docker hub request failed for node ${major}: ${response.status}`); + } const data = (await response.json()) as { results: Array<{ name: string }>; next: string | null }; for (const result of data.results) names.push(result.name); next = data.next || ""; @@ -376,78 +392,94 @@ async function generateMatrix(): Promise { .split(",") .map((d) => d.trim()) .filter(Boolean); - const bunTags = argOrEnv("--bun", "BUN_TAGS_TO_CHECK", "canary,latest") .split(",") .map((t) => t.trim()) .filter(Boolean); - const include: Array> = []; + const include = + process.env.INPUT_BUN_VERSIONS || process.env.INPUT_NODE_VERSIONS + ? await matrixFromForcedInputs(releases, availableReleases, distros) + : await matrixFromStateDiff(state, bunTags, availableReleases, distros); + + console.log(JSON.stringify({ include })); +} + +async function matrixFromForcedInputs( + releases: NodeRelease[], + availableReleases: NodeRelease[], + distros: string[] +): Promise>> { const forcedBun = process.env.INPUT_BUN_VERSIONS || ""; const forcedNode = process.env.INPUT_NODE_VERSIONS || ""; + const bunVersions = forcedBun + ? forcedBun.split(",").map((v) => v.trim()).filter(Boolean) + : (await Promise.all(argOrEnv("--bun", "BUN_TAGS_TO_CHECK", "canary,latest").split(",").map((t) => getVersions("bun", [t.trim()])))).flat(); + const forcedNodeVersions = forcedNode + ? forcedNode.split(",").map((v) => v.trim()).filter(Boolean) + : []; + const releaseByVersion = new Map(releases.map((r) => [r.version, r])); + const releaseByMajor = new Map(releases.map((r) => [r.major, r])); - if (forcedBun || forcedNode) { - const bunVersions = forcedBun - ? forcedBun.split(",").map((v) => v.trim()).filter(Boolean) - : (await Promise.all(bunTags.map((t) => getVersions("bun", [t])))).flat(); - const forcedNodeVersions = forcedNode - ? forcedNode.split(",").map((v) => v.trim()).filter(Boolean) - : []; - const releaseByVersion = new Map(releases.map((r) => [r.version, r])); - - for (const bunVersion of bunVersions) { - const isCanary = bunVersion.includes("-canary"); - const tag = isCanary ? "canary" : "latest"; - const nodeVersions = forcedNodeVersions.length > 0 ? forcedNodeVersions : availableReleases.map((r) => r.version); - for (const nodeVersion of nodeVersions) { - const release = releaseByVersion.get(nodeVersion); - for (const distro of distros) { - include.push({ - bun_tag: tag, - bun_version: bunVersion.replace(/^v/, ""), - node_major: Number(nodeVersion.split(".")[0]), - node_version: nodeVersion, - codename: release?.codename || "", - distro, - latest_candidate: false, - }); - } + const include: Array> = []; + for (const bunVersion of bunVersions) { + const tag = bunVersion.includes("-canary") ? "canary" : "latest"; + const nodeVersions = forcedNodeVersions.length > 0 ? forcedNodeVersions : availableReleases.map((r) => r.version); + for (const nodeVersion of nodeVersions) { + const release = releaseByVersion.get(nodeVersion) ?? releaseByMajor.get(Number(nodeVersion.split(".")[0])); + for (const distro of distros) { + include.push({ + bun_tag: tag, + bun_version: bunVersion.replace(/^v/, ""), + node_major: Number(nodeVersion.split(".")[0]), + node_version: nodeVersion, + codename: release?.codename || "", + distro, + latest_candidate: false, + }); } } - } else { - for (const tag of bunTags) { - const [version] = await getVersions("bun", [tag]); - if (!version) { - console.error(`no npm dist-tag ${tag} for bun`); - continue; - } - const stored = state.bun[tag]; - const bunChanged = stored !== `v${version}`; - const maxMajor = Math.max(...availableReleases.map((r) => r.major), 0); - - for (const release of availableReleases) { - const storedNode = state.nodejs[String(release.major)]?.version; - const nodeChanged = storedNode !== release.versionWithPrefix; - const forceRebuild = (state._needs_rebuild || []).includes(String(release.major)); - - if (!bunChanged && !nodeChanged && !forceRebuild) continue; - - for (const distro of distros) { - include.push({ - bun_tag: tag, - bun_version: version.replace(/^v/, ""), - node_major: release.major, - node_version: release.version, - codename: release.codename, - distro, - latest_candidate: release.major === maxMajor && distro === "debian" && tag === "latest", - }); - } + } + return include; +} + +async function matrixFromStateDiff( + state: VersionsState, + bunTags: string[], + availableReleases: NodeRelease[], + distros: string[] +): Promise>> { + const include: Array> = []; + const maxMajor = Math.max(...availableReleases.map((r) => r.major), 0); + + for (const tag of bunTags) { + const [version] = await getVersions("bun", [tag]); + if (!version) { + console.error(`no npm dist-tag ${tag} for bun`); + continue; + } + const bunChanged = state.bun[tag] !== `v${version}`; + + for (const release of availableReleases) { + const nodeChanged = state.nodejs[String(release.major)]?.version !== release.versionWithPrefix; + const forceRebuild = (state._needs_rebuild || []).includes(String(release.major)); + + if (!bunChanged && !nodeChanged && !forceRebuild) continue; + + for (const distro of distros) { + include.push({ + bun_tag: tag, + bun_version: version.replace(/^v/, ""), + node_major: release.major, + node_version: release.version, + codename: release.codename, + distro, + latest_candidate: release.major === maxMajor && distro === "debian" && tag === "latest", + }); } } } - - console.log(JSON.stringify({ include })); + return include; } async function main(): Promise { diff --git a/readme.md b/readme.md index 7f47e41..b5acc60 100644 --- a/readme.md +++ b/readme.md @@ -34,10 +34,12 @@ to help understand which versions are actually used. The payload contains only: - the Bun and Node.js versions in the container - the CPU architecture -- a random id (not persisted, rotated every start) +- a random id (rotated every start) -No IP addresses, hostnames, commands, or user data are collected, and the ping -fails silently (3s timeout, backgrounded) without affecting startup. +The endpoint sees the container's source IP, and the random id is kept in KV for +up to one hour to deduplicate repeated pings; no hostnames, commands, or user +data are sent, and the ping fails silently (3s timeout, backgrounded) without +affecting startup. The recorded date is the server's UTC date. **Opt out** with either: diff --git a/sonar-project.properties b/sonar-project.properties index f050f1e..992f014 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -9,6 +9,10 @@ # bun.lock as a lockfile. # - githubactions:S6505: bun install already passes --ignore-scripts. sonar.issue.ignore.multicriteria.e1.ruleKey=docker:S6506 -sonar.issue.ignore.multicriteria.e1.resourceKey=**/dockerfile, **/templates/*.dockerfile -sonar.issue.ignore.multicriteria.e2.ruleKey=githubactions:S8543, githubactions:S6505 -sonar.issue.ignore.multicriteria.e2.resourceKey=.github/workflows/release.yml +sonar.issue.ignore.multicriteria.e1.resourceKey=**/dockerfile +sonar.issue.ignore.multicriteria.e2.ruleKey=docker:S6506 +sonar.issue.ignore.multicriteria.e2.resourceKey=**/templates/*.dockerfile +sonar.issue.ignore.multicriteria.e3.ruleKey=githubactions:S8543 +sonar.issue.ignore.multicriteria.e3.resourceKey=.github/workflows/release.yml +sonar.issue.ignore.multicriteria.e4.ruleKey=githubactions:S6505 +sonar.issue.ignore.multicriteria.e4.resourceKey=.github/workflows/release.yml diff --git a/src/base/22/alpine/docker-entrypoint.sh b/src/base/22/alpine/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/22/alpine/docker-entrypoint.sh +++ b/src/base/22/alpine/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/22/debian-slim/docker-entrypoint.sh b/src/base/22/debian-slim/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/22/debian-slim/docker-entrypoint.sh +++ b/src/base/22/debian-slim/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/22/debian/docker-entrypoint.sh b/src/base/22/debian/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/22/debian/docker-entrypoint.sh +++ b/src/base/22/debian/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/24/alpine/docker-entrypoint.sh b/src/base/24/alpine/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/24/alpine/docker-entrypoint.sh +++ b/src/base/24/alpine/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/24/debian-slim/docker-entrypoint.sh b/src/base/24/debian-slim/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/24/debian-slim/docker-entrypoint.sh +++ b/src/base/24/debian-slim/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/24/debian/docker-entrypoint.sh b/src/base/24/debian/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/24/debian/docker-entrypoint.sh +++ b/src/base/24/debian/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/26/alpine/docker-entrypoint.sh b/src/base/26/alpine/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/26/alpine/docker-entrypoint.sh +++ b/src/base/26/alpine/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/26/debian-slim/docker-entrypoint.sh b/src/base/26/debian-slim/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/26/debian-slim/docker-entrypoint.sh +++ b/src/base/26/debian-slim/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/base/26/debian/docker-entrypoint.sh b/src/base/26/debian/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/base/26/debian/docker-entrypoint.sh +++ b/src/base/26/debian/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/git/22/alpine/docker-entrypoint.sh b/src/git/22/alpine/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/git/22/alpine/docker-entrypoint.sh +++ b/src/git/22/alpine/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/git/24/alpine/docker-entrypoint.sh b/src/git/24/alpine/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/git/24/alpine/docker-entrypoint.sh +++ b/src/git/24/alpine/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/src/git/26/alpine/docker-entrypoint.sh b/src/git/26/alpine/docker-entrypoint.sh index 7474d0a..b333345 100755 --- a/src/git/26/alpine/docker-entrypoint.sh +++ b/src/git/26/alpine/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/templates/docker-entrypoint.sh b/templates/docker-entrypoint.sh index 7474d0a..b333345 100644 --- a/templates/docker-entrypoint.sh +++ b/templates/docker-entrypoint.sh @@ -2,9 +2,12 @@ set -e # Anonymous usage telemetry for the imbios/bun-node image. -# Sends one tiny ping per container start (bun version, node version, arch, -# random id). No IPs, no hostnames, no user data. Opt out by setting -# BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). +# Sends one tiny ping per container start to https://bun-node.imbios.dev/telemetry/ping +# with bun version, node version, architecture and a random id. The endpoint +# sees the container's source IP; nothing else is sent (no hostnames, commands +# or user data). The random id is retained in KV for up to one hour to dedupe +# repeated pings. Fails silently and never blocks startup. +# Opt out by setting BUN_NODE_TELEMETRY=0 (or DO_NOT_TRACK=1). if [ "${BUN_NODE_TELEMETRY:-1}" != "0" ] && [ "${DO_NOT_TRACK:-0}" != "1" ]; then ( BUN_VERSION_TELEMETRY=$(bun --version 2>/dev/null || echo unknown) diff --git a/web/src/index.ts b/web/src/index.ts index d7b8dce..1850cc5 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -144,44 +144,69 @@ async function handleTelemetryPing(request: Request, env: Env): Promise 0 && body.id.length <= 64 ? body.id : ""; - const bun = typeof body.bun === "string" ? body.bun.slice(0, 40) : ""; - const node = typeof body.node === "string" ? body.node.slice(0, 40) : ""; - const arch = typeof body.arch === "string" ? body.arch.slice(0, 20) : ""; - - if (id) { - const seen = await env.STATS_KV.get(`telemetry:seen:${id}`).catch(() => null); - if (seen) return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); - await env.STATS_KV.put(`telemetry:seen:${id}`, "1", { expirationTtl: 3600 }).catch(() => {}); + if (!(await dedupeTelemetryId(env, body.id))) { + return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); } + await aggregateTelemetryDay(env, body); + return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); +} +interface TelemetryBody { + id: string; + bun: string; + node: string; + arch: string; +} + +async function parseTelemetryBody(request: Request): Promise { + let raw: { id?: string; bun?: string; node?: string; arch?: string }; + try { + raw = await request.json(); + } catch { + return null; + } + return { + id: typeof raw.id === "string" && raw.id.length > 0 && raw.id.length <= 64 ? raw.id : "", + bun: typeof raw.bun === "string" ? raw.bun.slice(0, 40) : "", + node: typeof raw.node === "string" ? raw.node.slice(0, 40) : "", + arch: typeof raw.arch === "string" ? raw.arch.slice(0, 20) : "", + }; +} + +async function dedupeTelemetryId(env: Env, id: string): Promise { + if (!id) return true; + const seen = await env.STATS_KV.get(`telemetry:seen:${id}`).catch(() => null); + if (seen) return false; + await env.STATS_KV.put(`telemetry:seen:${id}`, "1", { expirationTtl: 3600 }).catch(() => {}); + return true; +} + +async function aggregateTelemetryDay(env: Env, body: TelemetryBody): Promise { + const day = new Date().toISOString().slice(0, 10); const key = `telemetry:${day}`; const current = (await env.STATS_KV.get(key, "json").catch(() => null)) as TelemetryDay | null; - if (current && current.count >= 200_000) { - return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); - } + if (current && current.count >= 200_000) return; const next: TelemetryDay = { count: (current?.count || 0) + 1, byBun: current?.byBun || {}, byNode: current?.byNode || {}, byArch: current?.byArch || {}, }; - if (bun) next.byBun[bun] = (next.byBun[bun] || 0) + 1; - if (node) next.byNode[node] = (next.byNode[node] || 0) + 1; - if (arch) next.byArch[arch] = (next.byArch[arch] || 0) + 1; + if (body.bun) next.byBun[body.bun] = (next.byBun[body.bun] || 0) + 1; + if (body.node) next.byNode[body.node] = (next.byNode[body.node] || 0) + 1; + if (body.arch) next.byArch[body.arch] = (next.byArch[body.arch] || 0) + 1; await env.STATS_KV.put(key, JSON.stringify(next)).catch(() => {}); - return new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }); } async function telemetryTotals(env: Env, days: number): Promise<{ count: number; byNode: Record; byBun: Record; byArch: Record; days: Array<{ date: string; count: number }> }> { - const keys = await env.STATS_KV.list({ prefix: "telemetry:", limit: 1000 }); + const keys = await env.STATS_KV.list({ prefix: "telemetry:", limit: 1000 }).catch(() => null); + if (!keys) { + return { count: 0, byNode: {}, byBun: {}, byArch: {}, days: [] }; + } const cutoff = new Date(Date.now() - days * 86400_000).toISOString().slice(0, 10); const totals = { count: 0, byNode: {} as Record, byBun: {} as Record, byArch: {} as Record }; const perDay: Array<{ date: string; count: number }> = []; @@ -218,10 +243,11 @@ async function pullHistory(env: Env): Promise> { - const keys = await env.STATS_KV.list({ prefix: "views:", limit: 400 }); + const keys = await env.STATS_KV.list({ prefix: "views:", limit: 400 }).catch(() => null); + if (!keys) return {}; const out: Record = {}; for (const key of keys.keys) { - const value = await env.STATS_KV.get(key.name); + const value = await env.STATS_KV.get(key.name).catch(() => null); out[key.name.replace("views:", "")] = Number(value) || 0; } return out; @@ -471,8 +497,8 @@ async function handlePrivate(request: Request, env: Env): Promise { async function countView(env: Env): Promise { const today = new Date().toISOString().slice(0, 10); const key = `views:${today}`; - const current = Number(await env.STATS_KV.get(key)) || 0; - await env.STATS_KV.put(key, String(current + 1), { expirationTtl: 60 * 60 * 24 * 400 }); + const current = Number(await env.STATS_KV.get(key).catch(() => null)) || 0; + await env.STATS_KV.put(key, String(current + 1), { expirationTtl: 60 * 60 * 24 * 400 }).catch(() => {}); } export default { From 32de50397a060fe1d4b32291d74d1f7d288c360c Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Sun, 16 Aug 2026 11:42:48 +0700 Subject: [PATCH 6/7] fix: address CodeRabbit review of 2026-08-16 - alpine runtime packages installed with apk add --no-cache (no APK indexes retained in image layers), applied to base and git variants - debian-slim runtime curl install uses -y for non-interactive builds - release.yml build job passes matrix values to the retry command and the result-file rename via env vars instead of interpolating ${{ matrix.* }} into shell commands; upload path is the exact renamed file --- .github/workflows/release.yml | 27 ++++++++++++++++++++------- src/base/22/alpine/dockerfile | 2 +- src/base/22/debian-slim/dockerfile | 2 +- src/base/24/alpine/dockerfile | 2 +- src/base/24/debian-slim/dockerfile | 2 +- src/base/26/alpine/dockerfile | 2 +- src/base/26/debian-slim/dockerfile | 2 +- src/git/22/alpine/dockerfile | 2 +- src/git/24/alpine/dockerfile | 2 +- src/git/26/alpine/dockerfile | 2 +- templates/alpine-git.dockerfile | 2 +- templates/alpine.dockerfile | 2 +- templates/debian-slim.dockerfile | 2 +- 13 files changed, 32 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b58fd08..1117304 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,25 +119,38 @@ jobs: - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 name: Build and push + env: + REGISTRY: ${{ env.REGISTRY }} + PLATFORMS: ${{ env.PLATFORMS }} + MATRIX_BUN_VERSION: ${{ matrix.bun_version }} + MATRIX_NODE_VERSION: ${{ matrix.node_version }} + MATRIX_NODE_MAJOR: ${{ matrix.node_major }} + MATRIX_CODENAME: ${{ matrix.codename }} + MATRIX_DISTRO: ${{ matrix.distro }} + MATRIX_LATEST_CANDIDATE: ${{ matrix.latest_candidate }} with: timeout_minutes: 90 max_attempts: 3 retry_on: error - command: ./build_single.sh --bun "${{ matrix.bun_version }}" --node "${{ matrix.node_version }}" --node-major "${{ matrix.node_major }}" --codename "${{ matrix.codename }}" --distro "${{ matrix.distro }}" --latest-candidate "${{ matrix.latest_candidate }}" - env: - REGISTRY: ${{ env.REGISTRY }} - PLATFORMS: ${{ env.PLATFORMS }} + command: ./build_single.sh --bun "$MATRIX_BUN_VERSION" --node "$MATRIX_NODE_VERSION" --node-major "$MATRIX_NODE_MAJOR" --codename "$MATRIX_CODENAME" --distro "$MATRIX_DISTRO" --latest-candidate "$MATRIX_LATEST_CANDIDATE" - name: Name the build result per combination + id: name-result + env: + MATRIX_BUN_TAG: ${{ matrix.bun_tag }} + MATRIX_BUN_VERSION: ${{ matrix.bun_version }} + MATRIX_NODE_VERSION: ${{ matrix.node_version }} + MATRIX_DISTRO: ${{ matrix.distro }} run: | - mv build_success.json \ - "build_success-${{ matrix.bun_tag }}-${{ matrix.bun_version }}-${{ matrix.node_version }}-${{ matrix.distro }}.json" + RESULT_FILE="build_success-${MATRIX_BUN_TAG}-${MATRIX_BUN_VERSION}-${MATRIX_NODE_VERSION}-${MATRIX_DISTRO}.json" + mv build_success.json "$RESULT_FILE" + echo "result_file=$RESULT_FILE" >> "$GITHUB_OUTPUT" - name: Upload build result uses: actions/upload-artifact@v4 with: name: build-success-${{ matrix.bun_tag }}-${{ matrix.bun_version }}-${{ matrix.node_version }}-${{ matrix.distro }} - path: build_success-*.json + path: ${{ steps.name-result.outputs.result_file }} if-no-files-found: error finalize: diff --git a/src/base/22/alpine/dockerfile b/src/base/22/alpine/dockerfile index 73b8c4b..89b07ff 100644 --- a/src/base/22/alpine/dockerfile +++ b/src/base/22/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ curl \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/src/base/22/debian-slim/dockerfile b/src/base/22/debian-slim/dockerfile index f039796..8ffe236 100644 --- a/src/base/22/debian-slim/dockerfile +++ b/src/base/22/debian-slim/dockerfile @@ -68,7 +68,7 @@ COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun RUN apt-get update -qq \ - && apt-get install -qq --no-install-recommends curl \ + && apt-get install -y -qq --no-install-recommends curl \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && groupadd bun \ diff --git a/src/base/24/alpine/dockerfile b/src/base/24/alpine/dockerfile index c255654..c2ed292 100644 --- a/src/base/24/alpine/dockerfile +++ b/src/base/24/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ curl \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/src/base/24/debian-slim/dockerfile b/src/base/24/debian-slim/dockerfile index 767923f..a75a434 100644 --- a/src/base/24/debian-slim/dockerfile +++ b/src/base/24/debian-slim/dockerfile @@ -68,7 +68,7 @@ COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun RUN apt-get update -qq \ - && apt-get install -qq --no-install-recommends curl \ + && apt-get install -y -qq --no-install-recommends curl \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && groupadd bun \ diff --git a/src/base/26/alpine/dockerfile b/src/base/26/alpine/dockerfile index 428386c..a31d540 100644 --- a/src/base/26/alpine/dockerfile +++ b/src/base/26/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ curl \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/src/base/26/debian-slim/dockerfile b/src/base/26/debian-slim/dockerfile index b794ed1..75652da 100644 --- a/src/base/26/debian-slim/dockerfile +++ b/src/base/26/debian-slim/dockerfile @@ -68,7 +68,7 @@ COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun RUN apt-get update -qq \ - && apt-get install -qq --no-install-recommends curl \ + && apt-get install -y -qq --no-install-recommends curl \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && groupadd bun \ diff --git a/src/git/22/alpine/dockerfile b/src/git/22/alpine/dockerfile index e636016..7d3c600 100644 --- a/src/git/22/alpine/dockerfile +++ b/src/git/22/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/src/git/24/alpine/dockerfile b/src/git/24/alpine/dockerfile index 24dbaf2..ff97e5c 100644 --- a/src/git/24/alpine/dockerfile +++ b/src/git/24/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/src/git/26/alpine/dockerfile b/src/git/26/alpine/dockerfile index 0e1d365..2627563 100644 --- a/src/git/26/alpine/dockerfile +++ b/src/git/26/alpine/dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/templates/alpine-git.dockerfile b/templates/alpine-git.dockerfile index 86c95c8..199e7ac 100644 --- a/templates/alpine-git.dockerfile +++ b/templates/alpine-git.dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/templates/alpine.dockerfile b/templates/alpine.dockerfile index 82438bd..26ea8d2 100644 --- a/templates/alpine.dockerfile +++ b/templates/alpine.dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \ addgroup -g 1001 bun \ && adduser -u 1001 -G bun -s /bin/sh -D bun \ && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ - && apk add libgcc libstdc++ curl \ + && apk add --no-cache curl libgcc libstdc++ \ && which bun \ && which bunx \ && bun --version diff --git a/templates/debian-slim.dockerfile b/templates/debian-slim.dockerfile index d0e9ea4..94cafbc 100644 --- a/templates/debian-slim.dockerfile +++ b/templates/debian-slim.dockerfile @@ -68,7 +68,7 @@ COPY docker-entrypoint.sh /usr/local/bin COPY --from=build /usr/local/bin/bun /usr/local/bin/bun RUN apt-get update -qq \ - && apt-get install -qq --no-install-recommends curl \ + && apt-get install -y -qq --no-install-recommends curl \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && groupadd bun \ From 6deefbe27ee98dc3833dd695dcf10f287e388bdd Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Sun, 16 Aug 2026 11:52:50 +0700 Subject: [PATCH 7/7] fix: resolve blocking review threads - build job declares explicit permissions: contents: read (CodeQL) - readme documents gh release download for the version state before manual check-bun-node.ts commands --- .github/workflows/release.yml | 2 ++ readme.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1117304..1b4957e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,6 +97,8 @@ jobs: build: needs: setup runs-on: ubuntu-latest + permissions: + contents: read if: ${{ fromJson(needs.setup.outputs.matrix).include[0] != null }} strategy: fail-fast: false diff --git a/readme.md b/readme.md index b5acc60..32144be 100644 --- a/readme.md +++ b/readme.md @@ -92,6 +92,9 @@ Manual maintenance: ```sh bun install +# pull the current release state (versions.json lives on the GitHub release) +gh release download versions --pattern versions.json + # check which Bun versions are current bun run check-bun-node.ts --bun latest,canary