diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec1259a..1dfa045 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,11 +5,23 @@ on: branches: [main] pull_request: +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Socket Firewall + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + allow-external-fork-fallback: true + configure-bun: true - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: @@ -86,6 +98,15 @@ jobs: node: [22, 24] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Socket Firewall + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + allow-external-fork-fallback: true + configure-bun: true - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: @@ -109,8 +130,54 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Socket Firewall + id: sfw + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + env: + XDG_CONFIG_HOME: ${{ runner.temp }}/sfw-docker-bun + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + allow-external-fork-fallback: true + configure-bun: true - - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + - name: Build image with Socket Firewall + if: steps.sfw.outputs.active == 'true' + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + with: + context: . + push: false + load: true + platforms: linux/amd64 + tags: emulate:ci + build-args: | + SFW_REQUIRED=true + add-hosts: | + registry.npmjs.org:127.0.0.1 + registry.yarnpkg.com:127.0.0.1 + secret-files: | + sfw_bunfig=${{ runner.temp }}/sfw-docker-bun/.bunfig.toml + + - name: Teardown Socket Firewall + if: ${{ always() && steps.sfw.outputs.active == 'true' }} + uses: workos/setup-socket-firewall/teardown@ca93dd8aa351f54f4729fe3377a9be23c631c25d + + - name: Remove private Docker install config + if: ${{ always() }} + env: + SFW_BUN_CONFIG_FILE: ${{ runner.temp }}/sfw-docker-bun/.bunfig.toml + run: | + rm -f "$SFW_BUN_CONFIG_FILE" + if [ -e "$SFW_BUN_CONFIG_FILE" ]; then + echo "::error::Private Docker install config was not removed." + exit 1 + fi + + - name: Build image (external fork fallback) + if: steps.sfw.outputs.active == 'false' + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: false @@ -135,8 +202,54 @@ jobs: runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Socket Firewall + id: sfw + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + env: + XDG_CONFIG_HOME: ${{ runner.temp }}/sfw-docker-bun + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + allow-external-fork-fallback: true + configure-bun: true + + - name: Build image with Socket Firewall + if: steps.sfw.outputs.active == 'true' + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + with: + context: . + push: false + load: true + platforms: linux/arm64 + tags: emulate:ci + build-args: | + SFW_REQUIRED=true + add-hosts: | + registry.npmjs.org:127.0.0.1 + registry.yarnpkg.com:127.0.0.1 + secret-files: | + sfw_bunfig=${{ runner.temp }}/sfw-docker-bun/.bunfig.toml + + - name: Teardown Socket Firewall + if: ${{ always() && steps.sfw.outputs.active == 'true' }} + uses: workos/setup-socket-firewall/teardown@ca93dd8aa351f54f4729fe3377a9be23c631c25d + + - name: Remove private Docker install config + if: ${{ always() }} + env: + SFW_BUN_CONFIG_FILE: ${{ runner.temp }}/sfw-docker-bun/.bunfig.toml + run: | + rm -f "$SFW_BUN_CONFIG_FILE" + if [ -e "$SFW_BUN_CONFIG_FILE" ]; then + echo "::error::Private Docker install config was not removed." + exit 1 + fi - - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + - name: Build image (external fork fallback) + if: steps.sfw.outputs.active == 'false' + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d31a30d..94fb201 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,11 +5,23 @@ on: branches: [main] pull_request: +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Socket Firewall + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + allow-external-fork-fallback: true + configure-bun: true - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fe1bf0..2747e79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,9 @@ on: type: string required: true secrets: + PUBLIC_SOCKET_FIREWALL_TOKEN: + description: Public Socket Firewall token for release dependency downloads + required: false SDK_BOT_PRIVATE_KEY: description: Private key for the org SDK bot GitHub App, used to mint a short-lived token scoped to workos/homebrew-tap required: false @@ -56,6 +59,17 @@ jobs: with: ref: ${{ inputs.tag_name }} + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + + - name: Setup Socket Firewall + id: sfw + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + configure-bun: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.14 @@ -63,15 +77,25 @@ jobs: - name: Install run: bun install --frozen-lockfile + - name: Check out release tooling + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.workflow_sha }} + path: .release-tools + persist-credentials: false + sparse-checkout: scripts/build-binaries.sh + sparse-checkout-cone-mode: false + - name: Build standalone binary - run: >- - bun build - --compile - --no-compile-autoload-dotenv - --no-compile-autoload-bunfig - --target="${{ matrix.target }}" - ./src/cli.ts - --outfile "dist/${{ matrix.asset }}" + env: + TAG_NAME: ${{ inputs.tag_name }} + TARGET: ${{ matrix.target }} + ASSET: ${{ matrix.asset }} + run: .release-tools/scripts/build-binaries.sh "$TAG_NAME" "$TARGET" "$ASSET" + + - name: Teardown Socket Firewall + if: ${{ always() && steps.sfw.outputs.active == 'true' }} + uses: workos/setup-socket-firewall/teardown@ca93dd8aa351f54f4729fe3377a9be23c631c25d - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -202,9 +226,20 @@ jobs: node-version: 24 registry-url: 'https://registry.npmjs.org' + - name: Setup Socket Firewall + id: sfw + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + configure-bun: true + - name: Install run: bun install --frozen-lockfile + - name: Teardown Socket Firewall + if: ${{ always() && steps.sfw.outputs.active == 'true' }} + uses: workos/setup-socket-firewall/teardown@ca93dd8aa351f54f4729fe3377a9be23c631c25d + - name: Build run: bun run build @@ -279,12 +314,50 @@ jobs: - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + - name: Setup Socket Firewall + id: sfw + uses: workos/setup-socket-firewall@ca93dd8aa351f54f4729fe3377a9be23c631c25d + env: + XDG_CONFIG_HOME: ${{ runner.temp }}/sfw-docker-bun + with: + token: ${{ secrets.PUBLIC_SOCKET_FIREWALL_TOKEN }} + configure-bun: true + + - name: Require Socket Firewall for container publication + if: steps.sfw.outputs.active != 'true' + run: | + echo "::error::Socket Firewall must be active before container publication." + exit 1 + + - name: Publish image with Socket Firewall + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.tags.outputs.tags }} + build-args: | + SFW_REQUIRED=true + add-hosts: | + registry.npmjs.org:127.0.0.1 + registry.yarnpkg.com:127.0.0.1 + secret-files: | + sfw_bunfig=${{ runner.temp }}/sfw-docker-bun/.bunfig.toml + + - name: Teardown Socket Firewall + if: ${{ always() && steps.sfw.outputs.active == 'true' }} + uses: workos/setup-socket-firewall/teardown@ca93dd8aa351f54f4729fe3377a9be23c631c25d + + - name: Remove private Docker install config + if: ${{ always() }} + env: + SFW_BUN_CONFIG_FILE: ${{ runner.temp }}/sfw-docker-bun/.bunfig.toml + run: | + rm -f "$SFW_BUN_CONFIG_FILE" + if [ -e "$SFW_BUN_CONFIG_FILE" ]; then + echo "::error::Private Docker install config was not removed." + exit 1 + fi homebrew: name: Update Homebrew formula diff --git a/Dockerfile b/Dockerfile index d3424bd..bf4d071 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,10 @@ # syntax=docker/dockerfile:1 +# CI/release callers pass Bun's Socket Firewall config as a BuildKit secret and +# set SFW_REQUIRED=true. Local Docker builds omit both and keep using Bun's +# normal public-registry configuration. +ARG SFW_REQUIRED=false + # Build stage: compile TypeScript to dist/ from the bun lockfile. # bun.lock pins tree-sitter-kotlin (a transitive devDep via @workos/openapi-spec # -> @workos/oagen) to a git+ssh URL that can't clone inside the image without @@ -9,9 +14,19 @@ # other dependency — including typescript@5.9.3 — stays at its locked version, # keeping the build reproducible. FROM oven/bun:1.3.14 AS builder +ARG SFW_REQUIRED WORKDIR /app COPY package.json bun.lock ./ -RUN sed -i \ +RUN --mount=type=secret,id=sfw_bunfig,target=/run/secrets/.bunfig.toml \ + set -eu; \ + if [ "${SFW_REQUIRED:-false}" = "true" ] && [ ! -s /run/secrets/.bunfig.toml ]; then \ + echo "Socket Firewall Bun config secret is required for Docker dependency installs." >&2; \ + exit 1; \ + fi; \ + if [ -s /run/secrets/.bunfig.toml ]; then \ + export XDG_CONFIG_HOME=/run/secrets; \ + fi; \ + sed -i \ -e 's|git+ssh://git@github.com/fwcd/tree-sitter-kotlin.git#|github:fwcd/tree-sitter-kotlin#|g' \ -e 's/"sha512-onbog[^"]*"/""/g' \ bun.lock \ @@ -24,9 +39,19 @@ RUN bun run build # This stage only needs production deps (no git+ssh transitive devDeps), so # the bun lockfile works correctly. FROM oven/bun:1.3.14 AS deps +ARG SFW_REQUIRED WORKDIR /app COPY package.json bun.lock ./ -RUN bun install --frozen-lockfile --production --ignore-scripts +RUN --mount=type=secret,id=sfw_bunfig,target=/run/secrets/.bunfig.toml \ + set -eu; \ + if [ "${SFW_REQUIRED:-false}" = "true" ] && [ ! -s /run/secrets/.bunfig.toml ]; then \ + echo "Socket Firewall Bun config secret is required for Docker dependency installs." >&2; \ + exit 1; \ + fi; \ + if [ -s /run/secrets/.bunfig.toml ]; then \ + export XDG_CONFIG_HOME=/run/secrets; \ + fi; \ + bun install --frozen-lockfile --production --ignore-scripts # Runtime stage: minimal Node image with only what the emulator needs. FROM node:22-alpine diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index d1df9e7..202440f 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -1,9 +1,27 @@ #!/usr/bin/env bash set -euo pipefail -VERSION="${1:?usage: build-binaries.sh }" -OUT="dist/binaries" -rm -rf "$OUT" && mkdir -p "$OUT" +usage() { + echo "usage: build-binaries.sh [target asset]" >&2 + exit 2 +} + +VERSION="${1:-}" +[[ -n "$VERSION" ]] || usage +REQUESTED_TARGET="${2:-}" +REQUESTED_ASSET="${3:-}" +if [[ -n "$REQUESTED_TARGET" && -z "$REQUESTED_ASSET" ]] || [[ -z "$REQUESTED_TARGET" && -n "$REQUESTED_ASSET" ]]; then + usage +fi + +if [[ -n "$REQUESTED_TARGET" ]]; then + OUT="${BUN_BINARY_OUT_DIR:-dist}" + mkdir -p "$OUT" + rm -f "$OUT/$REQUESTED_ASSET" +else + OUT="${BUN_BINARY_OUT_DIR:-dist/binaries}" + rm -rf "$OUT" && mkdir -p "$OUT" +fi # target:artifact pairs (plain array — macOS /bin/bash 3.2 has no associative arrays) TARGETS=( @@ -17,16 +35,78 @@ TARGETS=( "bun-windows-arm64:workos-emulate-windows-arm64.exe" ) -for entry in "${TARGETS[@]}"; do - target="${entry%%:*}" - artifact="${entry#*:}" +COMPILE_TARGETS_DIR="${BUN_COMPILE_TARGETS_DIR:-.bun-compile-targets}" +rm -rf "$COMPILE_TARGETS_DIR" +mkdir -p "$COMPILE_TARGETS_DIR" + +compile_package_for_target() { + case "$1" in + bun-darwin-arm64) printf '%s\n' '@oven/bun-darwin-aarch64' ;; + bun-darwin-x64-baseline) printf '%s\n' '@oven/bun-darwin-x64-baseline' ;; + bun-linux-x64-baseline) printf '%s\n' '@oven/bun-linux-x64-baseline' ;; + bun-linux-arm64) printf '%s\n' '@oven/bun-linux-aarch64' ;; + bun-linux-x64-musl-baseline) printf '%s\n' '@oven/bun-linux-x64-musl-baseline' ;; + bun-linux-arm64-musl) printf '%s\n' '@oven/bun-linux-aarch64-musl' ;; + bun-windows-x64-baseline) printf '%s\n' '@oven/bun-windows-x64-baseline' ;; + bun-windows-arm64) printf '%s\n' '@oven/bun-windows-aarch64' ;; + *) + echo "Unsupported Bun compile target: $1" >&2 + return 1 + ;; + esac +} + +compile_binary_for_target() { + case "$1" in + bun-windows-*) printf '%s\n' 'bun.exe' ;; + *) printf '%s\n' 'bun' ;; + esac +} + +unpack_compile_target() { + local target="$1" + local package="$2" + local destination="$COMPILE_TARGETS_DIR/$target" + local tarball + + mkdir -p "$destination" + npm pack --pack-destination "$destination" "${package}@$(bun --version)" + tarball="$(find "$destination" -maxdepth 1 -name '*.tgz' -print -quit)" + test -n "$tarball" + tar -xzf "$tarball" -C "$destination" +} + +build_target() { + local target="$1" + local artifact="$2" + local compile_package compile_binary compile_path + + compile_package="$(compile_package_for_target "$target")" + compile_binary="$(compile_binary_for_target "$target")" + compile_path="$COMPILE_TARGETS_DIR/$target/package/bin/$compile_binary" + + unpack_compile_target "$target" "$compile_package" + test -f "$compile_path" bun build \ --compile \ + --compile-executable-path "$compile_path" \ --no-compile-autoload-dotenv \ --no-compile-autoload-bunfig \ --target="$target" \ ./src/cli.ts \ --outfile "$OUT/$artifact" +} + +if [[ -n "$REQUESTED_TARGET" ]]; then + build_target "$REQUESTED_TARGET" "$REQUESTED_ASSET" + echo "Built $REQUESTED_ASSET for $VERSION" + exit 0 +fi + +for entry in "${TARGETS[@]}"; do + target="${entry%%:*}" + artifact="${entry#*:}" + build_target "$target" "$artifact" done (cd "$OUT" && shasum -a 256 workos-emulate-* > checksums.txt)