Skip to content

Rebuild container runtime around process RPC #1508

Rebuild container runtime around process RPC

Rebuild container runtime around process RPC #1508

Workflow file for this run

name: PR Privileged
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
permissions: {}
concurrency:
group: pr-privileged-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
CF_ACCOUNT_SUBDOMAIN: sandbox-sdk-ci
jobs:
# --- Authorization gate (includes label management for fork PRs) ---
policy:
runs-on: ubuntu-latest
permissions:
pull-requests: read
timeout-minutes: 1
outputs:
authorized: ${{ steps.check.outputs.authorized }}
run_privileged: ${{ steps.check.outputs.run_privileged }}
steps:
- name: Generate app token for label management
id: app-token
if: >
github.event.action == 'synchronize' &&
github.event.pull_request.head.repo.full_name != github.repository
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Remove ok-to-test label on new pushes (fork PRs only)
if: >
github.event.action == 'synchronize' &&
github.event.pull_request.head.repo.full_name != github.repository
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
name: 'ok-to-test'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: 'New commits pushed -- `ok-to-test` label removed. A maintainer must re-review and re-apply the label to run the full CI pipeline.'
});
} catch (error) {
if (error.status === 404) {
core.info('ok-to-test label not present — nothing to remove');
} else {
throw error;
}
}
- name: Check authorization
id: check
env:
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
# Internal PRs are always authorized and run privileged CI
if [[ "$IS_FORK" == "false" ]]; then
echo "authorized=true" >> $GITHUB_OUTPUT
echo "run_privileged=true" >> $GITHUB_OUTPUT
echo "::notice::Authorized — internal PR"
exit 0
fi
# Fail-closed docs-only classification: any file NOT in the allowlist = privileged
# Capture file list first — if the API call fails, set -e aborts the step
FILES=$(gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')
if [[ -z "$FILES" ]]; then
echo "::error::GitHub API returned no files — refusing to classify (fail-closed)"
exit 1
fi
NEEDS_PRIVILEGED=false
while IFS= read -r file; do
case "$file" in
docs/*|*.md|LICENSE|.github/ISSUE_TEMPLATE/*|.github/PULL_REQUEST_TEMPLATE/*|.github/CODEOWNERS|.github/FUNDING.yml|.changeset/*.md)
;; # docs/config-only, safe
*)
NEEDS_PRIVILEGED=true
echo "::notice::Privileged file detected: $file"
break
;;
esac
done <<< "$FILES"
# Fork PRs with only docs/config changes are auto-authorized but skip privileged jobs
if [[ "$NEEDS_PRIVILEGED" == "false" ]]; then
echo "authorized=true" >> $GITHUB_OUTPUT
echo "run_privileged=false" >> $GITHUB_OUTPUT
echo "::notice::Authorized — docs-only PR, no privileged CI needed"
exit 0
fi
# Fork PRs with privileged changes need ok-to-test label
LABELS=$(gh api "repos/$GH_REPO/pulls/$PR_NUMBER" --jq '.labels[].name')
if echo "$LABELS" | grep -qx 'ok-to-test'; then
echo "authorized=true" >> $GITHUB_OUTPUT
echo "run_privileged=true" >> $GITHUB_OUTPUT
echo "::notice::Authorized — ok-to-test label verified via API"
else
echo "authorized=false" >> $GITHUB_OUTPUT
echo "run_privileged=false" >> $GITHUB_OUTPUT
echo "::notice::Not authorized — fork PR without ok-to-test label"
fi
# --- Change detection (only when authorized) ---
detect-changes:
needs: policy
if: needs.policy.outputs.run_privileged == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
timeout-minutes: 1
outputs:
needs-docker: ${{ steps.derived.outputs.needs-docker }}
docker-changed: ${{ steps.derived.outputs.docker-changed }}
needs-e2e: ${{ steps.derived.outputs.needs-e2e }}
any-source: ${{ steps.filter.outputs.any-source }}
docker-hash: ${{ steps.hashes.outputs.docker }}
deploy-hash: ${{ steps.hashes.outputs.deploy }}
steps:
# Checkout base branch (trusted) for filter definitions
- uses: actions/checkout@v6
with:
persist-credentials: false
# Path filter reads trusted .github/path-filters.yml + uses GitHub API for file list
- uses: dorny/paths-filter@v3
id: filter
with:
filters: .github/path-filters.yml
# Checkout fork code for content hashing
- uses: actions/checkout@v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
fetch-depth: 1
- name: Derive conditions
id: derived
run: |
# Did Docker inputs actually change?
DOCKER_CHANGED=false
if [[ "${{ steps.filter.outputs.shared }}" == "true" \
|| "${{ steps.filter.outputs.container }}" == "true" \
|| "${{ steps.filter.outputs.docker-config }}" == "true" \
|| "${{ steps.filter.outputs.build-config }}" == "true" \
|| "${{ steps.filter.outputs.deps }}" == "true" ]]; then
DOCKER_CHANGED=true
fi
echo "docker-changed=$DOCKER_CHANGED" >> $GITHUB_OUTPUT
# Needs E2E? (code changes or run-e2e label)
NEEDS_E2E=false
if [[ "${{ steps.filter.outputs.shared }}" == "true" \
|| "${{ steps.filter.outputs.sdk }}" == "true" \
|| "${{ steps.filter.outputs.container }}" == "true" \
|| "${{ steps.filter.outputs.docker-config }}" == "true" \
|| "${{ steps.filter.outputs.build-config }}" == "true" \
|| "${{ steps.filter.outputs.e2e-tests }}" == "true" \
|| "${{ steps.filter.outputs.ci-config }}" == "true" \
|| "${{ steps.filter.outputs.deps }}" == "true" \
|| "${{ contains(github.event.pull_request.labels.*.name, 'run-e2e') }}" == "true" ]]; then
NEEDS_E2E=true
fi
echo "needs-e2e=$NEEDS_E2E" >> $GITHUB_OUTPUT
# Docker images must exist whenever e2e will run (content-addressed
# cache makes this a fast retag when Docker inputs haven't changed)
NEEDS_DOCKER=$DOCKER_CHANGED
if [[ "$NEEDS_E2E" == "true" ]]; then
NEEDS_DOCKER=true
fi
echo "needs-docker=$NEEDS_DOCKER" >> $GITHUB_OUTPUT
- name: Compute content hashes
id: hashes
run: |
echo "docker=${{ hashFiles(
'packages/sandbox/Dockerfile',
'docker-bake.hcl',
'docker-images.txt',
'.dockerignore',
'package-lock.json',
'package.json',
'turbo.json',
'packages/*/package.json',
'packages/shared/**',
'packages/sandbox-container/**',
'tooling/typescript-config/**',
'biome.json',
'.bun-version',
'tests/e2e/test-worker/Dockerfile.*'
) }}" >> $GITHUB_OUTPUT
echo "deploy=${{ hashFiles(
'packages/sandbox/Dockerfile',
'docker-bake.hcl',
'docker-images.txt',
'.dockerignore',
'package-lock.json',
'package.json',
'turbo.json',
'packages/*/package.json',
'packages/shared/**',
'packages/sandbox-container/**',
'tooling/typescript-config/**',
'biome.json',
'.bun-version',
'tests/e2e/test-worker/Dockerfile.*',
'packages/sandbox/src/**',
'tests/e2e/test-worker/**',
'vitest.e2e.config.ts'
) }}" >> $GITHUB_OUTPUT
# --- Docker build (authorized only) ---
build:
needs: [policy, detect-changes]
if: needs.policy.outputs.run_privileged == 'true'
permissions:
contents: read
packages: write
actions: write
uses: ./.github/workflows/reusable-build.yml
with:
skip_docker: ${{ needs.detect-changes.outputs.needs-docker != 'true' }}
image_tag: pr-${{ github.event.pull_request.number }}
docker_hash: ${{ needs.detect-changes.outputs.docker-hash }}
checkout_ref: ${{ github.event.pull_request.head.sha }}
checkout_repo: ${{ github.event.pull_request.head.repo.full_name }}
cache_readonly: true
secrets: inherit
# --- E2E tests (authorized only, conditional) ---
e2e:
needs: [policy, detect-changes, build]
permissions:
actions: read
contents: read
if: |
needs.policy.outputs.run_privileged == 'true' &&
needs.detect-changes.outputs.needs-e2e == 'true' &&
!contains(github.event.pull_request.title, 'Version Packages')
uses: ./.github/workflows/reusable-e2e.yml
with:
worker_name_prefix: sandbox-e2e-test-worker-pr-${{ github.event.pull_request.number }}
image_tag: ci-${{ needs.detect-changes.outputs.docker-hash }}
images_changed: ${{ needs.detect-changes.outputs.docker-changed }}
deploy_hash: ${{ needs.detect-changes.outputs.deploy-hash }}
checkout_ref: ${{ github.event.pull_request.head.sha }}
checkout_repo: ${{ github.event.pull_request.head.repo.full_name }}
cache_readonly: true
secrets: inherit
# --- Bridge E2E tests (authorized + 'run-e2e-bridge' label) ---
# Opt-in: a maintainer applies the 'run-e2e-bridge' label to deploy the
# cloudflare-sandbox-bridge Worker for this PR and run the integration
# suite against it. The Worker is named with a -bridge-pr-<n> suffix so
# capacity tracking can distinguish bridge deployments from the e2e
# test-worker fleet.
bridge-e2e:
needs: [policy, detect-changes, build]
permissions:
actions: read
contents: read
if: |
needs.policy.outputs.run_privileged == 'true' &&
contains(github.event.pull_request.labels.*.name, 'run-e2e-bridge') &&
!contains(github.event.pull_request.title, 'Version Packages')
uses: ./.github/workflows/reusable-bridge-e2e.yml
with:
worker_name: sandbox-e2e-test-worker-bridge-pr-${{ github.event.pull_request.number }}
image_tag: ci-${{ needs.detect-changes.outputs.docker-hash }}
checkout_ref: ${{ github.event.pull_request.head.sha }}
checkout_repo: ${{ github.event.pull_request.head.repo.full_name }}
cache_readonly: true
secrets: inherit
# --- Publish preview packages + Docker images (authorized only) ---
publish-preview:
needs: [policy, detect-changes, build]
if: |
needs.policy.outputs.run_privileged == 'true' &&
needs.detect-changes.outputs.any-source == 'true' &&
!contains(github.event.pull_request.title, 'Version Packages')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: 24
- uses: actions/cache/restore@v5
id: nm-cache
with:
path: |
node_modules
packages/*/node_modules
examples/*/node_modules
sites/*/node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: steps.nm-cache.outputs.cache-hit != 'true'
- uses: actions/download-artifact@v8
with:
name: build-${{ github.sha }}
path: packages
- name: Set preview version
id: version
run: |
VERSION="0.0.0-pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD)"
node -e "const fs=require('fs'); const p='./packages/sandbox/package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version='${VERSION}'; fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- run: npx tsx .github/resolve-workspace-versions.ts
- name: Install crane
run: |
VERSION=0.20.3
TARBALL="go-containerregistry_Linux_x86_64.tar.gz"
curl -sLO "https://github.com/google/go-containerregistry/releases/download/v${VERSION}/${TARBALL}"
curl -sLO "https://github.com/google/go-containerregistry/releases/download/v${VERSION}/checksums.txt"
grep "${TARBALL}" checksums.txt | sha256sum -c
tar xzf "${TARBALL}" crane
sudo mv crane /usr/local/bin/
rm -f "${TARBALL}" checksums.txt
- name: Login to CF registry
if: ${{ needs.detect-changes.outputs.docker-changed == 'true' }}
run: |
source .github/cf-registry-login.sh
cf_registry_credentials 15 '["pull"]'
echo "$CF_REGISTRY_PASSWORD" | docker login registry.cloudflare.com \
-u "$CF_REGISTRY_USERNAME" --password-stdin
echo "$CF_REGISTRY_PASSWORD" | crane auth login registry.cloudflare.com \
--password-stdin -u "$CF_REGISTRY_USERNAME"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Copy Docker images to Docker Hub
if: ${{ needs.detect-changes.outputs.docker-changed == 'true' }}
run: |
source .github/load-docker-images.sh
BUILD_TAG="pr-${{ github.event.pull_request.number }}"
PREVIEW="${{ steps.version.outputs.version }}"
echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | crane auth login index.docker.io --password-stdin -u "${{ secrets.DOCKER_HUB_USERNAME }}"
for image in "${DOCKER_IMAGES[@]}"; do
[[ "$image" == "sandbox-standalone" ]] && continue # CF registry only
suffix="${image#sandbox}"
crane copy \
registry.cloudflare.com/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/${image}:${BUILD_TAG} \
docker.io/cloudflare/sandbox:${PREVIEW}${suffix}
done
- name: Extract standalone binary
if: ${{ needs.detect-changes.outputs.docker-changed == 'true' }}
run: |
BUILD_TAG="pr-${{ github.event.pull_request.number }}"
docker pull registry.cloudflare.com/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/sandbox:${BUILD_TAG}
CID=$(docker create registry.cloudflare.com/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/sandbox:${BUILD_TAG})
docker cp $CID:/container-server/sandbox ./sandbox-linux-x64
docker rm $CID
chmod +x ./sandbox-linux-x64
- uses: actions/upload-artifact@v7
if: ${{ needs.detect-changes.outputs.docker-changed == 'true' }}
with:
name: sandbox-binary
path: ./sandbox-linux-x64
retention-days: 3
- run: npx pkg-pr-new publish './packages/sandbox'
# PR comment with preview info
- uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const version = '${{ steps.version.outputs.version }}';
const dockerChanged = '${{ needs.detect-changes.outputs.docker-changed }}' === 'true';
const prNumber = '${{ github.event.pull_request.number }}';
const templatePath = dockerChanged
? '.github/templates/pr-preview-comment.md'
: '.github/templates/pr-preview-main-comment.md';
if (fs.existsSync(templatePath)) {
let body = fs.readFileSync(templatePath, 'utf8');
body = body.replace(/\{\{VERSION\}\}/g, version);
body = body.replace(/\{\{PR_NUMBER\}\}/g, prNumber);
if (dockerChanged) {
const runId = '${{ github.run_id }}';
const defaultTag = `cloudflare/sandbox:${version}`;
body = body
.replace(/\{\{RUN_ID\}\}/g, runId)
.replace(/\{\{DEFAULT_TAG\}\}/g, defaultTag)
.replace(/\{\{PYTHON_TAG\}\}/g, `cloudflare/sandbox:${version}-python`)
.replace(/\{\{OPENCODE_TAG\}\}/g, `cloudflare/sandbox:${version}-opencode`)
.replace(/\{\{MUSL_TAG\}\}/g, `cloudflare/sandbox:${version}-musl`);
}
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number
});
const existing = comments.find(c =>
c.user.type === 'Bot' && c.body?.includes('<!-- sandbox-preview -->')
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: existing.id, body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number, body
});
}
}
# --- Gate job (required check for branch protection) ---
gate:
name: ci/gate
if: always()
needs: [policy, detect-changes, build, e2e, bridge-e2e, publish-preview]
runs-on: ubuntu-latest
permissions: {}
timeout-minutes: 1
steps:
- name: Evaluate gate
run: |
echo "policy.authorized: ${{ needs.policy.outputs.authorized }}"
echo "policy.run_privileged: ${{ needs.policy.outputs.run_privileged }}"
echo "detect-changes: ${{ needs.detect-changes.result }}"
echo "build: ${{ needs.build.result }}"
echo "e2e: ${{ needs.e2e.result }}"
echo "bridge-e2e: ${{ needs.bridge-e2e.result }}"
echo "publish-preview: ${{ needs.publish-preview.result }}"
# Gate 1: Authorization
if [[ "${{ needs.policy.outputs.authorized }}" != "true" ]]; then
echo "::error::Fork PR requires 'ok-to-test' label. A maintainer must review the code and add the label to run the full CI pipeline."
exit 1
fi
# Gate 2: When privileged CI ran, detect-changes must have succeeded
if [[ "${{ needs.policy.outputs.run_privileged }}" == "true" ]]; then
if [[ "${{ needs.detect-changes.result }}" != "success" ]]; then
echo "::error::Change detection failed or was cancelled"
exit 1
fi
fi
# Gate 3: Privileged jobs must not have failed or been cancelled
# (skipped is OK — means change detection determined they weren't needed)
for result in "${{ needs.build.result }}" "${{ needs.e2e.result }}" "${{ needs.bridge-e2e.result }}" "${{ needs.publish-preview.result }}"; do
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "::error::One or more privileged CI jobs failed or were cancelled"
exit 1
fi
done
echo "All privileged checks passed"