Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CI, publishing, dependencies, and runner image changes need maintainer review.
/.github/ @biw
/docker/ @biw
/scripts/prepare.mjs @biw
/package.json @biw
/pnpm-lock.yaml @biw
/skills-lock.json @biw
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
107 changes: 17 additions & 90 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,15 @@ name: CI
on:
push:
branches: [main]
# Use the workflow from protected `main`, so a pull request cannot remove
# the approval gate before it is allowed to use paid Cloudflare compute.
pull_request_target:
pull_request:
branches: [main]

permissions:
actions: read
contents: read

jobs:
select-cloudflare-ci-environment:
name: Select Cloudflare CI approval
runs-on: ubuntu-latest
outputs:
name: ${{ steps.select.outputs.name }}
requires_approval: ${{ steps.select.outputs.requires_approval }}
steps:
- id: select
env:
EVENT_NAME: ${{ github.event_name }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

if [ "$EVENT_NAME" != "pull_request_target" ]; then
echo 'name=cloudflare-ci' >> "$GITHUB_OUTPUT"
echo 'requires_approval=false' >> "$GITHUB_OUTPUT"
exit 0
fi

permission="$(gh api \
-H 'Accept: application/vnd.github+json' \
"/repos/$GITHUB_REPOSITORY/collaborators/$PR_AUTHOR/permission" \
--jq '.permission' 2>/dev/null || true)"

if [ "$permission" = 'admin' ]; then
echo 'name=cloudflare-ci' >> "$GITHUB_OUTPUT"
echo 'requires_approval=false' >> "$GITHUB_OUTPUT"
else
# Fail closed: an API error or a non-admin author is reviewed by a
# repository admin through the protected environment.
echo 'name=cloudflare-ci-approval' >> "$GITHUB_OUTPUT"
echo 'requires_approval=true' >> "$GITHUB_OUTPUT"
fi

approve-cloudflare-ci:
name: Approve Cloudflare CI
needs: select-cloudflare-ci-environment
runs-on: ubuntu-latest
environment:
name: ${{ needs.select-cloudflare-ci-environment.outputs.name }}
# This is a CI admission gate, not a deployment record.
deployment: false
steps:
- name: Verify the approval protection rule
if: needs.select-cloudflare-ci-environment.outputs.requires_approval == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
reviewer_count="$(gh api \
-H 'Accept: application/vnd.github+json' \
"/repos/$GITHUB_REPOSITORY/environments/cloudflare-ci-approval" \
--jq '[.protection_rules[] | select(.type == "required_reviewers") | .reviewers[]?] | length')"

if [ "$reviewer_count" -eq 0 ]; then
echo 'Cloudflare CI is blocked: cloudflare-ci-approval must have a repository administrator configured as a required reviewer.' >&2
exit 1
fi

- env:
REQUIRES_APPROVAL: ${{ needs.select-cloudflare-ci-environment.outputs.requires_approval }}
run: |
if [ "$REQUIRES_APPROVAL" = 'true' ]; then
echo 'A repository administrator approved this pull request for Cloudflare CI.' >> "$GITHUB_STEP_SUMMARY"
else
echo 'The pull-request author is a repository administrator; Cloudflare CI is authorized.' >> "$GITHUB_STEP_SUMMARY"
fi

ci:
name: CI (${{ matrix.runner_index }})
needs: approve-cloudflare-ci
strategy:
fail-fast: false
max-parallel: 2
Expand All @@ -94,32 +20,35 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# `pull_request_target` uses this trusted workflow from `main`. Check
# out the pull-request merge ref only after the approval gate above.
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
ref: ${{ github.event_name == 'pull_request_target' && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
- name: Enable pnpm
run: corepack enable
node-version: "26"
package-manager-cache: false
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6
- name: Resolve pnpm store
id: pnpm-store
run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm cache
id: pnpm-cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-linux-x64-node-22-${{ hashFiles('pnpm-lock.yaml') }}
key: pnpm-linux-x64-node-26-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-linux-x64-node-22-
pnpm-linux-x64-node-26-
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Save pnpm cache
if: matrix.runner_index == 1 && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-linux-x64-node-22-${{ hashFiles('pnpm-lock.yaml') }}
key: pnpm-linux-x64-node-26-${{ hashFiles('pnpm-lock.yaml') }}
- name: Check
run: pnpm run check
- name: Test
Expand All @@ -129,14 +58,12 @@ jobs:

runner-image-size:
name: Runner image under 1.5 GB
needs: approve-cloudflare-ci
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# Match the revision exercised by the parallel CI jobs.
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.event_name == 'pull_request_target' && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
persist-credentials: false
- name: Build and check runner image size
run: ./tests/runner-image-size.sh

Expand Down
148 changes: 5 additions & 143 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,154 +6,16 @@ on:
permissions:
contents: read

concurrency:
group: npm-publish
cancel-in-progress: false

jobs:
check-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check.outputs.version }}
should_publish: ${{ steps.check.outputs.should_publish }}

steps:
- name: Checkout the CI-tested revision
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.sha }}

- name: Verify the tested revision is still main
id: revision
env:
TESTED_SHA: ${{ github.sha }}
run: |
if [ "$(git rev-parse HEAD)" != "$TESTED_SHA" ]; then
echo "Checked out revision does not match the successful CI run" >&2
exit 1
fi

git fetch --no-tags origin main

if [ "$(git rev-parse origin/main)" != "$TESTED_SHA" ]; then
echo "is_current=false" >> "$GITHUB_OUTPUT"
echo "Skipping stale successful CI revision $TESTED_SHA"
exit 0
fi

echo "is_current=true" >> "$GITHUB_OUTPUT"

- name: Setup Node.js
if: steps.revision.outputs.is_current == 'true'
uses: actions/setup-node@v7
with:
node-version: "24"

- name: Check if version should be published
if: steps.revision.outputs.is_current == 'true'
id: check
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
PACKAGE_NAME=$(node -p "require('./package.json').name")

echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
echo "Current package: $PACKAGE_NAME@$CURRENT_VERSION"

if npm view "$PACKAGE_NAME" versions --json > published-versions.json 2> npm-view-error.log; then
:
elif grep --quiet 'E404' npm-view-error.log; then
printf '[]\n' > published-versions.json
else
cat npm-view-error.log >&2
exit 1
fi

if node -e "
const versions = require('./published-versions.json')
const list = Array.isArray(versions) ? versions : [versions]
process.exit(list.includes(process.argv[1]) ? 0 : 1)
" "$CURRENT_VERSION"; then
echo "Version $CURRENT_VERSION is already published"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
echo "Version $CURRENT_VERSION has not been published"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
fi
uses: biw/npm-trusted-publish-workflows/.github/workflows/check.yml@v1

publish:
needs: check-version
if: needs.check-version.outputs.should_publish == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- name: Checkout the CI-tested revision
uses: actions/checkout@v7
with:
ref: ${{ github.sha }}

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false

- name: Enable pnpm
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Verify package
run: pnpm run prepublishOnly && npm pack --dry-run
- name: Verify npm supports trusted publishing
run: |
NPM_VERSION=$(npm --version)
echo "npm version: $NPM_VERSION"

node -e "
const version = process.argv[1].split('.').map(Number)
const minimum = [11, 5, 1]

for (let i = 0; i < minimum.length; i++) {
if (version[i] > minimum[i]) process.exit(0)
if (version[i] < minimum[i]) process.exit(1)
}
" "$NPM_VERSION"
- name: Reconfirm the published revision
env:
TESTED_SHA: ${{ github.sha }}
run: |
git fetch --no-tags origin main

if [ "$(git rev-parse HEAD)" != "$TESTED_SHA" ] || [ "$(git rev-parse origin/main)" != "$TESTED_SHA" ]; then
echo "Refusing to publish a revision that is no longer the tip of main" >&2
exit 1
fi
- name: Publish to npm
run: npm publish

release:
needs:
- check-version
- publish
if: >-
needs.check-version.result == 'success' &&
needs.check-version.outputs.should_publish == 'true' &&
needs.publish.result == 'success'
runs-on: ubuntu-latest
uses: biw/npm-trusted-publish-workflows/.github/workflows/publish.yml@v1
with:
tested-sha: ${{ github.sha }}
permissions:
contents: write

steps:
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ needs.check-version.outputs.version }}
name: v${{ needs.check-version.outputs.version }}
target_commitish: ${{ github.sha }}
draft: false
prerelease: false
generate_release_notes: true
id-token: write
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"deploy:dry-run": "vp exec tsx scripts/deploy.ts --dry-run",
"resource-traces": "vp exec tsx scripts/resource-metrics.ts",
"cf-typegen": "vp exec wrangler types",
"prepare": "pnpx skills experimental_install",
"prepare": "node scripts/prepare.mjs",
"prepack": "vp pack",
"prepublishOnly": "vp check && vp test"
},
Expand Down
17 changes: 17 additions & 0 deletions scripts/prepare.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { spawnSync } from "node:child_process";

if (process.env.CI || process.env.GITHUB_ACTIONS) {
console.log("Skipping agent skill installation in CI.");
process.exit(0);
}

const result = spawnSync("pnpx", ["skills", "experimental_install"], {
stdio: "inherit",
shell: process.platform === "win32",
});

if (result.error) {
throw result.error;
}

process.exitCode = result.status ?? 1;
Loading
Loading