diff --git a/.github/workflows/source-lints.yml b/.github/workflows/source-lints.yml index fa4f3046083e..3aa1c0e18a0f 100644 --- a/.github/workflows/source-lints.yml +++ b/.github/workflows/source-lints.yml @@ -33,6 +33,7 @@ on: - "test/internal/source-lints/**" - ".github/workflows/source-lints.yml" - ".github/workflows/comment-cop.yml" + - ".github/workflows/update-*.yml" - ".github/actions/setup-bun/**" pull_request: paths: @@ -57,6 +58,7 @@ on: - "test/internal/source-lints/**" - ".github/workflows/source-lints.yml" - ".github/workflows/comment-cop.yml" + - ".github/workflows/update-*.yml" - ".github/actions/setup-bun/**" merge_group: diff --git a/.github/workflows/update-lolhtml.yml b/.github/workflows/update-lolhtml.yml deleted file mode 100644 index da3aca5560d1..000000000000 --- a/.github/workflows/update-lolhtml.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Update lolhtml - -permissions: - contents: read - -on: - schedule: - - cron: "0 1 * * 0" - workflow_dispatch: - -jobs: - check-update: - if: github.repository == 'oven-sh/bun' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Check lolhtml version - id: check-version - run: | - set -euo pipefail - - DEP_FILE=scripts/build/deps/lolhtml.ts - CURRENT_VERSION=$(sed -nE 's/^const LOLHTML_COMMIT = "([0-9a-f]{40})";$/\1/p' "$DEP_FILE") - - if [ -z "$CURRENT_VERSION" ]; then - echo "Error: Could not find LOLHTML_COMMIT in $DEP_FILE" - exit 1 - fi - - # Validate that it looks like a git hash - if ! [[ $CURRENT_VERSION =~ ^[0-9a-f]{40}$ ]]; then - echo "Error: Invalid git hash format in $DEP_FILE" - echo "Found: $CURRENT_VERSION" - echo "Expected: 40 character hexadecimal string" - exit 1 - fi - - echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT - - LATEST_RELEASE=$(curl -sL https://api.github.com/repos/cloudflare/lol-html/releases/latest) - if [ -z "$LATEST_RELEASE" ]; then - echo "Error: Failed to fetch latest release from GitHub API" - exit 1 - fi - - LATEST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tag_name') - if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then - echo "Error: Could not extract tag name from GitHub API response" - exit 1 - fi - - # Get the commit SHA that the tag points to - # This handles both lightweight tags (direct commit refs) and annotated tags (tag objects) - TAG_REF_RESPONSE=$(curl -sL "https://api.github.com/repos/cloudflare/lol-html/git/refs/tags/$LATEST_TAG") - LATEST_TAG_SHA=$(echo "$TAG_REF_RESPONSE" | jq -r '.object.sha') - TAG_OBJECT_TYPE=$(echo "$TAG_REF_RESPONSE" | jq -r '.object.type') - - if [ -z "$LATEST_TAG_SHA" ] || [ "$LATEST_TAG_SHA" = "null" ]; then - echo "Error: Could not fetch SHA for tag $LATEST_TAG" - exit 1 - fi - - if [ "$TAG_OBJECT_TYPE" = "tag" ]; then - # This is an annotated tag, we need to get the commit it points to - LATEST_SHA=$(curl -sL "https://api.github.com/repos/cloudflare/lol-html/git/tags/$LATEST_TAG_SHA" | jq -r '.object.sha') - if [ -z "$LATEST_SHA" ] || [ "$LATEST_SHA" = "null" ]; then - echo "Error: Could not fetch commit SHA for annotated tag $LATEST_TAG @ $LATEST_TAG_SHA" - exit 1 - fi - else - # This is a lightweight tag pointing directly to a commit - LATEST_SHA="$LATEST_TAG_SHA" - fi - - if ! [[ $LATEST_SHA =~ ^[0-9a-f]{40}$ ]]; then - echo "Error: Invalid SHA format received from GitHub" - echo "Found: $LATEST_SHA" - echo "Expected: 40 character hexadecimal string" - exit 1 - fi - - echo "latest=$LATEST_SHA" >> $GITHUB_OUTPUT - echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT - - - name: Update version if needed - if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest - env: - LATEST: ${{ steps.check-version.outputs.latest }} - run: | - set -euo pipefail - sed -i -E 's/^(const LOLHTML_COMMIT = ")[0-9a-f]{40}(";)$/\1'"$LATEST"'\2/' scripts/build/deps/lolhtml.ts - - - name: Create Pull Request - if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - add-paths: | - scripts/build/deps/lolhtml.ts - commit-message: "deps: update lolhtml to ${{ steps.check-version.outputs.tag }} (${{ steps.check-version.outputs.latest }})" - title: "deps: update lolhtml to ${{ steps.check-version.outputs.tag }}" - delete-branch: true - branch: deps/update-lolhtml - body: | - ## What does this PR do? - - Updates lolhtml to version ${{ steps.check-version.outputs.tag }} - - Compare: https://github.com/cloudflare/lol-html/compare/${{ steps.check-version.outputs.current }}...${{ steps.check-version.outputs.latest }} - - Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-lolhtml.yml) diff --git a/scripts/build/deps/README.md b/scripts/build/deps/README.md index 458b3617e550..dafdde4e344c 100644 --- a/scripts/build/deps/README.md +++ b/scripts/build/deps/README.md @@ -41,6 +41,16 @@ The `.github/workflows/update-.yml` jobs do this automatically by sed'ing the `const _COMMIT = "..."` line. If you rename that constant, update the workflow too. +A dep that is fetched from an oven-sh fork (boringssl, libuv, lolhtml, +mimalloc, tinycc) has no such workflow, and must not get one: its pin is +a commit of the fork, so a job that follows upstream releases proposes a +commit that cannot build. Bump those by hand, after the rebase in the +fork. + +`test/internal/source-lints/update-workflows.test.ts` checks both rules: +each job's sed patterns match the pin line of its dep file, and the job +asks the repository that the dep is fetched from. + **For `direct` deps:** the source list is hardcoded, so a bump that adds or removes a `.c`/`.cpp` upstream needs a matching list edit here. CI catches a missed addition (link error on the unresolved symbol); a diff --git a/scripts/build/deps/lolhtml.ts b/scripts/build/deps/lolhtml.ts index 499ddf47ec7e..7a98fab1ef74 100644 --- a/scripts/build/deps/lolhtml.ts +++ b/scripts/build/deps/lolhtml.ts @@ -17,6 +17,10 @@ * This dep entry exists only to FETCH the source into `vendor/lolhtml/` — * `emitRust` in `rust.ts` waits on its `.ref` stamp so cargo never sees a * missing path dependency. + * + * The source is a fork (see LOLHTML_COMMIT below), so lolhtml has no + * `.github/workflows/update-lolhtml.yml`: the pin is a commit of the fork, + * never of upstream, and a bump starts with a rebase in the fork. */ import type { Dependency } from "../source.ts"; diff --git a/test/internal/source-lints/update-workflows.test.ts b/test/internal/source-lints/update-workflows.test.ts new file mode 100644 index 000000000000..5fbeae64f698 --- /dev/null +++ b/test/internal/source-lints/update-workflows.test.ts @@ -0,0 +1,55 @@ +/** + * Each .github/workflows/update-.yml asks GitHub for the latest release + * of a repository. It reads the `_COMMIT` pin in + * scripts/build/deps/.ts with sed, and opens a pull request that rewrites + * the pin, also with sed, to the commit of that release. That only works when: + * + * - The repository it asks is the one the dep is fetched from. update-lolhtml.yml + * broke this way. The dep moved to the oven-sh/lol-html fork, the workflow + * still asked cloudflare/lol-html, and each weekly run proposed an upstream + * commit that bun cannot build. A dep that is fetched from a fork has no + * update workflow. + * - Its sed patterns match the pin line. A renamed constant, or a comment at the + * end of the line, makes each weekly run fail, and nobody watches those runs. + */ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const repoRoot = join(import.meta.dir, "..", "..", ".."); +const workflowsDir = join(repoRoot, ".github", "workflows"); + +test("each update workflow agrees with its dep file", () => { + const checked: string[] = []; + for (const workflow of new Bun.Glob("update-*.yml").scanSync(workflowsDir)) { + const text = readFileSync(join(workflowsDir, workflow), "utf8"); + const depFile = text.match(/^\s*DEP_FILE=(scripts\/build\/deps\/[\w-]+\.ts)$/m)?.[1]; + if (!depFile) { + // update-sqlite3.yml and update-vendor.yml do not bump a pin in scripts/build/deps/. + expect({ workflow, readsADepFile: text.includes("scripts/build/deps/") }).toEqual({ + workflow, + readsADepFile: false, + }); + continue; + } + const dep = readFileSync(join(repoRoot, depFile), "utf8"); + + const asks = new Set(Array.from(text.matchAll(/api\.github\.com\/repos\/([\w.-]+\/[\w.-]+)\//g), m => m[1])); + // The `s//` of each sed call: one reads the pin, one rewrites it. + const sedPatterns = Array.from(text.matchAll(/\bsed [^'\n]*'s\/([^/\n]+)\//g), m => m[1]); + + expect({ + workflow, + asks: [...asks], + sedCalls: sedPatterns.length, + sedPatternsThatMissThePin: sedPatterns.filter(pattern => !new RegExp(pattern, "m").test(dep)), + }).toEqual({ + workflow, + asks: [dep.match(/^\s*repo: "([^"]+)",$/m)?.[1]], + sedCalls: 2, + sedPatternsThatMissThePin: [], + }); + checked.push(workflow); + } + expect(checked.length).toBeGreaterThan(0); +});