Skip to content

ci: pin workflow actions to full commit SHAs - #7

Closed
robobun wants to merge 4 commits into
bunfrom
robobun/6d0316ee/pin-workflow-actions
Closed

robobun wants to merge 4 commits into
bunfrom
robobun/6d0316ee/pin-workflow-actions

Conversation

@robobun

@robobun robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • Pin every uses: in bun.yml, ci.yml and semgrep.yml to the commit that its tag points to today, with the exact version in a trailing comment. The versions do not change.
  • Pin dtolnay/rust-toolchain to its v1 tag and pass toolchain: stable. Its README requires a pinned commit to be on master, because other commits get garbage-collected. The commit behind @stable is not on master.
  • The bun.yml header documents the rule for the next rebase onto upstream.
  • Verified on this PR: all four jobs get past "Set up job". cargo test passes. semgrep/ci passes but scans nothing here (Notes). Test and Clippy and format fail on defects already on bun, fixed in Fix the defects that upstream's CI reports on the bun branch (cherry-pick from #3) #8.

Background

  • oven-sh/bun builds the lol_html crate from a commit on the bun branch of this fork. A workflow-only commit needs no pin bump there.
  • bun.yml is the workflow of the fork. ci.yml and semgrep.yml come from upstream and also run on every pull request here.
  • "Set up job" downloads each action and applies the policy before a step runs.
Notes

Pins

action commit tag
actions/checkout 11d5960a326750d5838078e36cf38b85af677262 v4.4.0 (what v4 points to)
actions/cache 0057852bfaa89a56745cba8c7296529d2fc39830 v4.3.0 (what v4 points to)
actions/setup-node 49933ea5288caeca8642d1e84afbd3f7d6820020 v4.4.0 (what v4 points to)
obi1kenobi/cargo-semver-checks-action 6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 v2.9 (what v2 points to)
dtolnay/rust-toolchain 6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 v1, plus toolchain: stable

Check of each uses: owner/repo@sha # tag line against repos/<repo>/git/ref/tags/<tag>:

OK   actions/cache v4.3.0 -> 0057852bfaa89a56745cba8c7296529d2fc39830 (commit signature verified: true)
OK   actions/checkout v4.4.0 -> 11d5960a326750d5838078e36cf38b85af677262 (commit signature verified: true)
OK   actions/setup-node v4.4.0 -> 49933ea5288caeca8642d1e84afbd3f7d6820020 (commit signature verified: true)
OK   dtolnay/rust-toolchain v1 -> 6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 (commit signature verified: true)
OK   obi1kenobi/cargo-semver-checks-action v2.9 -> 6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 (commit signature verified: true)

dtolnay/rust-toolchain

  • The README says: "it is required that you pick a SHA that is within the history of the master branch. Any commit that is not within the history of master will eventually get garbage-collected and your workflows will fail."
  • @stable is 6bed0761d98439e5a578e2877258200ad565ba87 today. It is one commit on top of master commit d1031067. That commit only changes action.yml: toolchain gets required: false and default: stable. compare/master...6bed0761 reports diverged, so it is not on master.
  • v1 (6c977a6c) is on master (compare/6c977a6c...master: ahead 6, behind 0). v1...d1031067 changes only scripts/update-revs.sh. So action.yml at v1 plus toolchain: stable does the same as @stable.
  • master HEAD (02cb101e, 2026-09-12) adds --force-non-host to the rustup toolchain install and rustup default calls. It is one day old, so this PR does not use it.
  • Four of the five pinned actions are JavaScript actions. dtolnay/rust-toolchain is a composite action with run: steps only. So no pinned action pulls in a second action by tag.

Relation to #3

@sosukesuzuki wrote the same pins in 6a170ff on the branch of #3 and reverted them in 6166e4c to keep that PR to the transform-layer changes ("can be handled separately"). This PR is that separate change. The commit credits him. The differences: the dtolnay/rust-toolchain pin (6a170ff uses the @stable commit), exact versions in the comments, and the bun.yml header.

What the upstream jobs report after the pin

The runs on #3 with 6a170ff show what ci.yml reports when it can start (run 34204513554, run 34204956393, run 34295940690). All of these are on bun today. This PR does not change them:

  • Check format: rustfmt drift in src/selectors_vm/mod.rs:508 (from ea6125b).
  • Run Rust tests with RUSTFLAGS=-Dwarnings: unused import Cell in the suspension tests (from 0f44e89), and an elided lifetime in the text! macro. The macro is upstream code. 0f44e89 exposed the lint because its tests are the first to use text! inside the crate.
  • Run clippy on C API and the C API tests: c-api does not compile, because set_user_data now needs Send (from 0f44e89).
  • Check semver: enum_variant_added for RewritingError::Suspended. The fork adds API on top of the 2.7.2 release on purpose, so this check can only fail here.

eeea10d, 4afc2ca and 021637b on the branch of #3 fix these four. With them all three workflows were green (run 34296114458). #8 cherry-picks them on top of this PR.

The run of this PR shows the first failure of each job: run 34774752554 (Check format, Run Rust tests). The cargo test job is run 34774752540, on stable 1.98.1.

semgrep/ci on the fork

The job is green but it scans nothing. SEMGREP_APP_TOKEN is Cloudflare's secret, so it is empty here. semgrep ci prints run `semgrep login` before using `semgrep ci` and exits 0. Upstream main replaced the job with semgrep scan --config=auto, which needs no token. The job becomes useful after the next rebase.

Scope

This PR covers bun. The main branch of the fork mirrors upstream and keeps the tag refs, so a run on main still fails at "Set up job".

Rebase cost

Upstream main (v3.0.1) still uses tag refs. It changed two lines of ci.yml that this PR does not touch, and it rewrote semgrep.yml. At the next git rebase --onto <new-tag>, this commit conflicts in semgrep.yml only (checked against v3.0.1). Take the upstream file and pin its uses: lines.

Self-review

A review of #7 and #8 raised 11 points. 8 led to a change or a check: the tag and SHA checks above, the nested-action check, the semgrep/ci note, the wording of the text! lint origin, the bun.yml header wording, the main branch scope note, and the merge and rebase notes in #8. 3 did not: an em dash inside a code comment of 4afc2ca (kept verbatim, it is the author's text), a Dependabot entry for github-actions (a separate change), and a move of the Check semver commit from #8 into this PR (it fixes a defect on bun, not the pin).

Options not taken

  • Pin bun.yml only. Then three of the four checks stay red on every pull request.
  • Disable the CI and Semgrep config workflows in the repository settings. That leaves no trace in git, and the CI jobs found real defects on bun (the list above).
  • Skip semgrep/ci on the fork with if: github.repository == 'cloudflare/lol-html'. A skipped job needs no pin. It edits the same upstream file, so the rebase cost is the same, and one rule ("pin every uses:") is easier to follow.

robobun and others added 3 commits August 2, 2026 02:58
A handler can return Err(SuspensionRequest) to park the current rewritable
unit on the heap and exit write()/end() with the non-poisoning
RewritingError::Suspended; HtmlRewriter::resume() continues from a
StateMachineBookmark.

- take_owned() on each rewritable unit deep-copies borrowed byte slices so
  the unit outlives the write() stack frame
- Arena::shift advances a start offset instead of memmoving the tail (a
  suspended rewrite re-feeds its unconsumed tail on every resume)
- the pending captured-text flush is hoisted from Dispatcher::handle_tag
  into the lexer actions so every suspension point has a uniform shape
- end_mut() is the non-consuming end() for a rewriter that may suspend in a
  document-end handler

Bun uses this so an async JS content handler can suspend the rewrite and
resume it from a promise reaction instead of spinning a nested event loop
inside write(). See oven-sh/bun#36733.
The upstream ci.yml triggers on push: branches: [master] only, so an
additive bun.yml runs cargo test --features=integration_test on pushes/PRs
to bun without touching ci.yml (keeps rebases onto upstream clean).
…epth

The selector-VM open-element stack had two per-token scans that go
quadratic (or worse) on pathological nesting:

  * Stack::pop_up_to did a full rposition() over the open stack for
    every end tag. A stray end tag that matches nothing still walked
    the whole stack, so N unclosed opens plus N stray closes was O(N^2):
    "<a></b>".repeat(100_000) (~700 KB) burned ~25s of CPU.

  * try_exec_hereditary_jumps_without_attrs walked every ancestor and
    re-executed its hereditary_jumps for every start tag. For a selector
    like "div div" over N nested <div>s every item carries the same
    address range, so the walk is O(depth) per tag and O(N^2) overall;
    k-arm chains ("div div div") compound to O(N^k).

Both paths are reachable from HtmlRewriter with any element-handler
registration, so untrusted HTML can pin a CPU core via sub-MB input.
Memory stays linear so max_allowed_memory_usage does not bound it.

Changes:

  * Stack now keeps open_name_counts: HashMap<LocalName, usize> so
    pop_up_to rejects a name not on the stack in O(1). When the name is
    present the rposition scan length equals the subsequent drain
    length, so total scan work over a document is bounded by total
    pushes.

  * Stack now keeps active_hereditary_jumps: Vec<(AddressRange, usize)>,
    the distinct hereditary-jump ranges currently contributed by any
    open item, each paired with the shallowest depth that introduced it.
    Descendant matching iterates this flat list (bounded by program
    size, not depth) and pop_up_to prunes it by depth. The per-item
    has_ancestor_with_hereditary_jumps flag and the
    HereditaryJumpPtr.stack_offset resume coordinate are removed.

  * LocalName gets a manual Hash impl that ASCII-lowercases the Bytes
    arm so it agrees with the existing case-insensitive PartialEq.
    Without this, keying a HashMap on LocalName is unsound for any name
    that falls off LocalNameHash (custom elements, >12 chars, chars
    outside a-zA-Z1-6). This also fixes the same latent contract
    violation in TypedChildCounterMap.

Re-executing the same address range against the same start tag was
already a no-op after the matched-id set union in add_execution_branch,
so dedup-once execution is observationally equivalent.
@robobun

robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

The oven-sh organization only allows actions that are pinned to a
full-length commit SHA. The workflows use tag refs, so every job on
this repository fails at "Set up job" in about 2 seconds:

  The actions actions/checkout@v4, dtolnay/rust-toolchain@stable, and
  actions/cache@v4 are not allowed in oven-sh/lol-html because all
  actions must be pinned to a full-length commit SHA.

Pin each `uses:` in bun.yml, ci.yml and semgrep.yml to the commit that
its tag points to today. The versions do not change.

dtolnay/rust-toolchain is the exception. Its README requires a pinned
commit to be in the history of `master`, because other commits get
garbage-collected. The `stable` rev is one commit off `master` that only
makes `stable` the default of the `toolchain` input. So pin the `v1`
tag, which is on `master`, and pass `toolchain: stable`. Apart from that
default, action.yml at `v1` is identical to action.yml at `stable`.

Co-authored-by: SUZUKI Sosuke <sosuke@bun.com>
@robobun

robobun commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: #10 carries this commit on the new base of bun (v3.0.1). This branch is on the v2.7.2 lineage and does not merge any more. #10 also pins the two uses: lines of the new upstream semgrep.yml and passes the renamed _integration_test feature in bun.yml. The cargo test job passes there.

@robobun robobun closed this Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant