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..1b4957e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,59 +2,115 @@ 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 }} + sync_sha: ${{ steps.sync.outputs.sync_sha }} 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: Sync supported Node.js majors + id: sync + 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 + echo "sync_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + else + echo "sync_sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT" + 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 + permissions: + contents: read + 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 + with: + ref: ${{ needs.setup.outputs.sync_sha }} + persist-credentials: false - - name: Setup Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub @@ -64,37 +120,138 @@ jobs: password: ${{ secrets.DOCKER_TOKEN }} - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 - name: Build and push Docker images + 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: 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" + + - name: Name the build result per combination + id: name-result 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 }} + MATRIX_BUN_TAG: ${{ matrix.bun_tag }} + MATRIX_BUN_VERSION: ${{ matrix.bun_version }} + MATRIX_NODE_VERSION: ${{ matrix.node_version }} + MATRIX_DISTRO: ${{ matrix.distro }} + run: | + 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: ${{ steps.name-result.outputs.result_file }} + if-no-files-found: error - - name: Commit changes - run: ./commit_changes.sh + finalize: + 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: | + 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 + 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 }} + 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"; "")') + 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" + 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 + env: + GH_TOKEN: ${{ github.token }} + + - name: Write release summary + if: ${{ steps.merge.outputs.updates_found == 'true' }} + 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' && github.run_attempt < 3 }} 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 | 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 + 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..d327bb6 --- /dev/null +++ b/build_single.sh @@ -0,0 +1,160 @@ +#!/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 + bun_version="canary" + 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}" +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-git" \ + "${git_tag_args[@]}" \ + --build-arg BUN_VERSION="$bun_build_arg" \ + "./src/git/${NODE_MAJOR}/alpine" \ + --push +fi + +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..1d42525 100644 --- a/check-bun-node.ts +++ b/check-bun-node.ts @@ -1,172 +1,135 @@ #!/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 [--majors 22,24] [--versions versions.json] + * bun check-bun-node.ts --matrix [--majors 22,24] [--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, readdir, 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 (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"; +} - if (currentStart && now >= new Date(currentStart)) { - return "Current"; - } +function getMajorNodeReleases() { + return Object.entries(nodevuDataTyped).filter(([version, { support }]) => { + if (!support) return false; + if (version.startsWith("v0.") && version !== "v0.12") return false; + return true; + }); +} - return "Pending"; -}; +async function generateReleaseData(): Promise { + const majors = getMajorNodeReleases(); + const releases: NodeRelease[] = []; -/** - * 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(); + for (const [, major] of majors) { + const versions = Object.values(major.releases); + if (versions.length === 0) continue; - return majors.map(([, major]) => { - const [latestVersion] = Object.values(major.releases); + const latestVersion = versions.slice(1).reduce( + (newest, release) => (compareVersions(release.semver.raw, newest.semver.raw) > 0 ? release : newest), + versions[0]! + ); - 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, + }); + } + + return releases.sort((a, b) => a.major - b.major); +} -async function getNpmDistTags( - pkgName: string -): Promise> { +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)); +} + +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 +138,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 +148,368 @@ async function getVersions( } } -/** - * This will detect, wether --bun or --node is requested - */ -const main = async () => { +async function loadVersionsState(path: string): Promise { + try { + 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: {} }; + } +} + +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; +} + +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 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 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 || ""; + } + const matches = names.filter((name) => pattern.test(name)); + if (matches.length === 0) return null; + matches.sort((a, b) => { + 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; +} + +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 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 [ + "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 supported = await resolveSupportedMajors(); + const created: number[] = []; + const updated: number[] = []; + + 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); + } + + let existingEntrypoint: string | null = null; + try { + 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")}`; + } + }; + + 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[] = []; + 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); + } + } + } + + 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 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()) + .filter(Boolean); + const bunTags = argOrEnv("--bun", "BUN_TAGS_TO_CHECK", "canary,latest") + .split(",") + .map((t) => t.trim()) + .filter(Boolean); + + 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])); + + 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, + }); + } + } + } + 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", + }); + } + } + } + return 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 = majorsArg(); + 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..32144be 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. @@ -23,6 +27,30 @@ 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 (rotated every start) + +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: + +```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 @@ -49,6 +77,39 @@ 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 + +# 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 + +# 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..992f014 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,18 @@ +# 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 +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/20/alpine/docker-entrypoint.sh b/src/base/20/alpine/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/base/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/base/20/debian-slim/docker-entrypoint.sh b/src/base/20/debian-slim/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/base/20/debian-slim/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/debian/docker-entrypoint.sh b/src/base/20/debian/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/base/20/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/22/alpine/docker-entrypoint.sh b/src/base/22/alpine/docker-entrypoint.sh index a0e45cb..b333345 100755 --- a/src/base/22/alpine/docker-entrypoint.sh +++ b/src/base/22/alpine/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then set -- /usr/local/bin/bun "$@" fi diff --git a/src/base/22/alpine/dockerfile b/src/base/22/alpine/dockerfile index cf5a945..89b07ff 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,15 +57,13 @@ 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 \ 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/base/22/debian-slim/docker-entrypoint.sh b/src/base/22/debian-slim/docker-entrypoint.sh index a0e45cb..b333345 100755 --- a/src/base/22/debian-slim/docker-entrypoint.sh +++ b/src/base/22/debian-slim/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + 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/dockerfile b/src/base/22/debian-slim/dockerfile index 7d33f13..8ffe236 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,10 +66,12 @@ 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 \ +RUN apt-get update -qq \ + && apt-get install -y -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 a0e45cb..b333345 100755 --- a/src/base/22/debian/docker-entrypoint.sh +++ b/src/base/22/debian/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + 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/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/docker-entrypoint.sh b/src/base/24/alpine/docker-entrypoint.sh index a0e45cb..b333345 100755 --- a/src/base/24/alpine/docker-entrypoint.sh +++ b/src/base/24/alpine/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + 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/dockerfile b/src/base/24/alpine/dockerfile index 4f7824e..c2ed292 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 @@ -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/base/24/debian-slim/docker-entrypoint.sh b/src/base/24/debian-slim/docker-entrypoint.sh index a0e45cb..b333345 100755 --- a/src/base/24/debian-slim/docker-entrypoint.sh +++ b/src/base/24/debian-slim/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + 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/dockerfile b/src/base/24/debian-slim/dockerfile index 99dca2c..a75a434 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 @@ -69,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 -y -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 a0e45cb..b333345 100755 --- a/src/base/24/debian/docker-entrypoint.sh +++ b/src/base/24/debian/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + 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/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/alpine/docker-entrypoint.sh b/src/base/25/alpine/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/base/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/debian-slim/docker-entrypoint.sh b/src/base/25/debian-slim/docker-entrypoint.sh deleted file mode 100755 index a0e45cb..0000000 --- a/src/base/25/debian-slim/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/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/26/alpine/docker-entrypoint.sh b/src/base/26/alpine/docker-entrypoint.sh new file mode 100755 index 0000000..b333345 --- /dev/null +++ b/src/base/26/alpine/docker-entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + +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/dockerfile b/src/base/26/alpine/dockerfile similarity index 87% rename from src/base/25/alpine/dockerfile rename to src/base/26/alpine/dockerfile index 07f60ea..a31d540 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 @@ -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/base/26/debian-slim/docker-entrypoint.sh b/src/base/26/debian-slim/docker-entrypoint.sh new file mode 100755 index 0000000..b333345 --- /dev/null +++ b/src/base/26/debian-slim/docker-entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + +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/debian-slim/dockerfile b/src/base/26/debian-slim/dockerfile similarity index 83% rename from src/base/20/debian-slim/dockerfile rename to src/base/26/debian-slim/dockerfile index 2f07073..75652da 100644 --- a/src/base/20/debian-slim/dockerfile +++ b/src/base/26/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:26-bookworm-slim # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful @@ -68,10 +66,12 @@ 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 \ +RUN apt-get update -qq \ + && apt-get install -y -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 new file mode 100755 index 0000000..b333345 --- /dev/null +++ b/src/base/26/debian/docker-entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + +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/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/docker-entrypoint.sh b/src/git/22/alpine/docker-entrypoint.sh index a0e45cb..b333345 100755 --- a/src/git/22/alpine/docker-entrypoint.sh +++ b/src/git/22/alpine/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + 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/dockerfile b/src/git/22/alpine/dockerfile index a3a9b2e..7d3c600 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,15 +57,13 @@ 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 \ 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/docker-entrypoint.sh b/src/git/24/alpine/docker-entrypoint.sh index a0e45cb..b333345 100755 --- a/src/git/24/alpine/docker-entrypoint.sh +++ b/src/git/24/alpine/docker-entrypoint.sh @@ -1,6 +1,26 @@ #!/bin/sh set -e +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + 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/dockerfile b/src/git/24/alpine/dockerfile index d844929..ff97e5c 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 @@ -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/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/git/26/alpine/docker-entrypoint.sh b/src/git/26/alpine/docker-entrypoint.sh new file mode 100755 index 0000000..b333345 --- /dev/null +++ b/src/git/26/alpine/docker-entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + +if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then + set -- /usr/local/bin/bun "$@" +fi + +exec "$@" diff --git a/src/git/25/alpine/dockerfile b/src/git/26/alpine/dockerfile similarity index 87% rename from src/git/25/alpine/dockerfile rename to src/git/26/alpine/dockerfile index 167ff10..2627563 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 @@ -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/20/alpine/dockerfile b/templates/alpine-git.dockerfile similarity index 85% rename from src/git/20/alpine/dockerfile rename to templates/alpine-git.dockerfile index ae371ee..199e7ac 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,15 +57,13 @@ 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 \ 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/base/20/alpine/dockerfile b/templates/alpine.dockerfile similarity index 85% rename from src/base/20/alpine/dockerfile rename to templates/alpine.dockerfile index 79d00a0..26ea8d2 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,15 +57,13 @@ 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 \ 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/base/25/debian-slim/dockerfile b/templates/debian-slim.dockerfile similarity index 82% rename from src/base/25/debian-slim/dockerfile rename to templates/debian-slim.dockerfile index a0505c4..94cafbc 100644 --- a/src/base/25/debian-slim/dockerfile +++ b/templates/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:__NODE_MAJOR__-bookworm-slim # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful @@ -69,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 -y -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/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/templates/docker-entrypoint.sh b/templates/docker-entrypoint.sh new file mode 100644 index 0000000..b333345 --- /dev/null +++ b/templates/docker-entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +# Anonymous usage telemetry for the imbios/bun-node image. +# 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) + 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\"}" + 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 + +if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then + set -- /usr/local/bin/bun "$@" +fi + +exec "$@" 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..1850cc5 --- /dev/null +++ b/web/src/index.ts @@ -0,0 +1,548 @@ +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 TelemetryDay { + count: number; + byBun: Record; + byNode: Record; + byArch: Record; +} + +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 || ![429, 500, 502, 503].includes(res.status)) 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?: 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 && (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 ?? 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 && (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 ?? 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 })); + } + await Promise.all(writes); + 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" } }); + } + const body = await parseTelemetryBody(request); + if (!body) { + return new Response("bad request", { status: 400, headers: { "Content-Type": "text/plain" } }); + } + 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; + const next: TelemetryDay = { + count: (current?.count || 0) + 1, + byBun: current?.byBun || {}, + byNode: current?.byNode || {}, + byArch: current?.byArch || {}, + }; + 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(() => {}); +} + +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 }).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 }> = []; + 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 }); + 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 }).catch(() => null); + if (!keys) return {}; + const out: Record = {}; + for (const key of keys.keys) { + const value = await env.STATS_KV.get(key.name).catch(() => null); + 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 = 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 ` + + + + + + + + + + + + ${escapeHtml(label)} + ${escapeHtml(label)} + ${escapeHtml(value)} + ${escapeHtml(value)} + +`; +} + +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 + ? `` + : ""; + 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
+
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
${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, starts, last-updated)

+
+ 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, + 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 ` + + + + + +bun-node private stats + + + +
+

๐Ÿ”’ bun-node private stats owner only

+

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)}
+
+ + + + + + + + + +

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.
+
+ + +`; +} + +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, 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"]!; + 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(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, 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 { + const today = new Date().toISOString().slice(0, 10); + const key = `views:${today}`; + 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 { + 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)); + 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, 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") { + 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 === "POST" && path === "/telemetry/ping") { + return handleTelemetryPing(request, env); + } + + if (request.method === "GET" && path === "/private") { + 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"] +}