From adc28e5950e67cf6aa8deee439f76853a6ec0b46 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 14 Nov 2026 09:16:25 +0000 Subject: [PATCH 1/3] Build the website in CI only for API docs it hasn't built yet The website build ran on every pull request: the check for changed docs always found the copied .gitkeep and the website branch's older docs. An empty cache entry keyed on the website commit and the docs hash now records each build, so the ARM job skips about 3.5 minutes when the docs match an earlier build. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gG8vKzkJGrU7AZ1CXQ Signed-off-by: Christoph Knittel --- .github/workflows/ci.yml | 68 +++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abc9f4b528..eef694a49e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -393,38 +393,52 @@ jobs: # Checks that the website builds with the generated API docs. These # steps stay in this job rather than in one that needs build-compiler, - # which would wait for the slowest platform before starting. - - name: Checkout rescript-lang.org + # which would wait for the slowest platform before starting. The build + # depends only on the docs and the website commit, so an empty cache + # entry keyed on both records each pair that built, and a pull request + # whose docs match an earlier build skips it. + - name: Website build key + id: website-build-key if: ${{ matrix.generate_api_docs }} + env: + WEBSITE_BRANCH: ${{ steps.api-docs-plan.outputs.website-branch }} + run: | + sha=$(git ls-remote https://github.com/rescript-lang/rescript-lang.org.git "refs/heads/$WEBSITE_BRANCH" | cut -f1) + if [ -z "$sha" ]; then + echo "::error::The website has no branch $WEBSITE_BRANCH" + exit 1 + fi + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "value=website-build-v1-$sha-${{ hashFiles('scripts/res/apiDocs/**') }}" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Look up website build + id: website-build + if: ${{ matrix.generate_api_docs }} + uses: actions/cache/restore@v6 + with: + path: tmp/website-build + key: ${{ steps.website-build-key.outputs.value }} + lookup-only: true + + - name: Checkout rescript-lang.org + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} uses: actions/checkout@v7 with: repository: rescript-lang/rescript-lang.org - ref: ${{ steps.api-docs-plan.outputs.website-branch }} + ref: ${{ steps.website-build-key.outputs.sha }} path: rescript-lang.org - - name: Check for API docs changes - id: api-docs-diff - if: ${{ matrix.generate_api_docs }} - run: | - cp -R scripts/res/apiDocs/. rescript-lang.org/data/api/ - git -C rescript-lang.org status - if [[ -z "$(git -C rescript-lang.org status --porcelain)" ]]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - shell: bash - # The website is checked out inside this repository, so its Yarn also # reads this repository's .yarnrc.yml and rejects the settings it does # not know yet. The steps that need this config are done. - name: Hide the compiler's Yarn config from the website - if: ${{ matrix.generate_api_docs && steps.api-docs-diff.outputs.changed == 'true' }} + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} run: mv .yarnrc.yml "$RUNNER_TEMP/rescript.yarnrc.yml" shell: bash - name: Use Node.js for website - if: ${{ matrix.generate_api_docs && steps.api-docs-diff.outputs.changed == 'true' }} + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} uses: actions/setup-node@v7 with: cache: yarn @@ -432,11 +446,27 @@ jobs: node-version-file: rescript-lang.org/.node-version - name: Build website with the API docs - if: ${{ matrix.generate_api_docs && steps.api-docs-diff.outputs.changed == 'true' }} + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} run: | + cp -R ../scripts/res/apiDocs/. data/api/ yarn yarn build working-directory: rescript-lang.org + shell: bash + + - name: Record website build + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} + run: mkdir -p tmp/website-build && echo "$KEY" > tmp/website-build/key + env: + KEY: ${{ steps.website-build-key.outputs.value }} + shell: bash + + - name: Save website build record + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6 + with: + path: tmp/website-build + key: ${{ steps.website-build-key.outputs.value }} # Compares the words the compiler allocates on fixed inputs, and the # instructions it executes on some of them, with the base branch From adc30528dc80d00b6fa8a5f598a309f1a0f6a75d Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 14 Nov 2026 09:16:25 +0000 Subject: [PATCH 2/3] Check formatting only on Linux in CI The format check gives the same result on every platform but took 25 s on Windows and 18 s on macOS, both on the critical path. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gG8vKzkJGrU7AZ1CXQ Signed-off-by: Christoph Knittel --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eef694a49e..b6c4860fb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,8 +188,10 @@ jobs: if: matrix.test_commit_records run: yarn test-upsert-comment + # The format check gives the same result everywhere; it runs on Linux + # only, where it is fastest. - name: Run tests - run: node scripts/test.js -all + run: node scripts/test.js ${{ runner.os == 'Linux' && '-all' || '-ounit -mocha -build -docstrings' }} - name: Check for diffs in tests folder run: git diff --ignore-cr-at-eol --exit-code tests From 4f18f1eec4524577cbfbaeb456d56b082100f6d3 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 14 Nov 2026 09:16:25 +0000 Subject: [PATCH 3/3] Poll every 0.1 s in the build system's canonical tests The canonical suite is the slowest part of each platform's rewatch step, and its waits polled once a second, so each of its ~150 waits took up to a second too long. Timeouts stay in seconds. Locally, the passing watch tests took 3.7 s instead of 6.9 s, 0.5 s instead of 4.1 s and so on. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gG8vKzkJGrU7AZ1CXQ Signed-off-by: Christoph Knittel --- rewatch/tests/utils.sh | 88 ++++++------------- .../02-watch-warnings-persist-atomic-save.sh | 15 ++-- rewatch/tests/watch/04-watch-config-change.sh | 25 ++---- ...d-interface-after-failed-implementation.sh | 17 ++-- .../08-watch-overlapping-source-removed.sh | 27 +++--- 5 files changed, 59 insertions(+), 113 deletions(-) diff --git a/rewatch/tests/utils.sh b/rewatch/tests/utils.sh index 62a32ca6c0..9dec90d43c 100644 --- a/rewatch/tests/utils.sh +++ b/rewatch/tests/utils.sh @@ -60,14 +60,21 @@ replace() { fi } -wait_for_pid_gone() { - local pid="$1"; local timeout - timeout=${2:-20} - while process_is_running "$pid" && [ "$timeout" -gt 0 ]; do - sleep 1 - timeout=$((timeout - 1)) +# Runs a command every 0.1 seconds until it succeeds or the timeout in +# seconds (the first argument) has passed. A one-second interval made every +# wait in the suite take up to a second longer than needed. +poll() { + local deadline=$((SECONDS + $1)); shift + until "$@"; do + [ "$SECONDS" -lt "$deadline" ] || return 1 + sleep 0.1 done - ! process_is_running "$pid" +} + +process_is_gone() { ! process_is_running "$1"; } + +wait_for_pid_gone() { + poll "${2:-20}" process_is_gone "$1" } exit_watcher() { @@ -116,62 +123,25 @@ clear_locks() { rm -f lib/rescript/watch.lock lib/rescript/build.lock } -wait_for_file() { - local file="$1"; local timeout - timeout=${2:-60} - while [ "$timeout" -gt 0 ]; do - [ -f "$file" ] && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 -} +file_exists() { [ -f "$1" ]; } +file_is_gone() { [ ! -f "$1" ]; } +file_has_pattern() { grep -q "$2" "$1" 2>/dev/null; } -wait_for_pattern() { - local file="$1"; local pattern="$2"; local timeout - timeout=${3:-60} - while [ "$timeout" -gt 0 ]; do - grep -q "$pattern" "$file" 2>/dev/null && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 +file_lacks_pattern() { + [ -f "$1" ] && ! grep -q "$2" "$1" 2>/dev/null } -wait_for_pattern_count() { - local file="$1"; local pattern="$2"; local expected="$3"; local timeout - timeout=${4:-60} - while [ "$timeout" -gt 0 ]; do - local current_count - current_count=$(grep -c "$pattern" "$file" 2>/dev/null || true) - current_count=${current_count:-0} - [ "$current_count" -ge "$expected" ] && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 +file_has_pattern_count() { + local current_count + current_count=$(grep -c "$2" "$1" 2>/dev/null || true) + [ "${current_count:-0}" -ge "$3" ] } -wait_for_pattern_gone() { - local file="$1"; local pattern="$2"; local timeout - timeout=${3:-60} - while [ "$timeout" -gt 0 ]; do - if [ -f "$file" ] && ! grep -q "$pattern" "$file" 2>/dev/null; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 -} +wait_for_file() { poll "${2:-60}" file_exists "$1"; } +wait_for_file_gone() { poll "${2:-60}" file_is_gone "$1"; } +wait_for_pattern() { poll "${3:-60}" file_has_pattern "$1" "$2"; } +wait_for_pattern_gone() { poll "${3:-60}" file_lacks_pattern "$1" "$2"; } -wait_for_file_gone() { - local file="$1"; local timeout - timeout=${2:-60} - while [ "$timeout" -gt 0 ]; do - [ ! -f "$file" ] && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 +wait_for_pattern_count() { + poll "${4:-60}" file_has_pattern_count "$1" "$2" "$3" } diff --git a/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh b/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh index a41f7b10b8..489a2e6ca2 100755 --- a/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh +++ b/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh @@ -15,17 +15,12 @@ else exit 1 fi +completed_log_changed() { + [ -f "$1" ] && ! cmp -s "$1" "$2" && grep -q "#Done(" "$1" 2>/dev/null +} + wait_for_changed_completed_log() { - local file="$1"; local baseline="$2"; local timeout - timeout=${3:-60} - while [ "$timeout" -gt 0 ]; do - if [ -f "$file" ] && ! cmp -s "$file" "$baseline" && grep -q "#Done(" "$file" 2>/dev/null; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll "${3:-60}" completed_log_changed "$1" "$2" } COMPILER_LOG="./packages/watch-warnings/lib/rescript/.compiler.log" diff --git a/rewatch/tests/watch/04-watch-config-change.sh b/rewatch/tests/watch/04-watch-config-change.sh index 15e79e6a8f..209e7c456a 100755 --- a/rewatch/tests/watch/04-watch-config-change.sh +++ b/rewatch/tests/watch/04-watch-config-change.sh @@ -42,17 +42,12 @@ wait_for_next_build() { "$completed_builds" 30 } +tracked_outputs_exist() { + [ -z "$(git ls-files --deleted -- '*.mjs')" ] +} + wait_for_tracked_outputs() { - local timeout - timeout=60 - while [ "$timeout" -gt 0 ]; do - if [ -z "$(git ls-files --deleted -- '*.mjs')" ]; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll 60 tracked_outputs_exist } # Change the suffix in rescript.json (same approach as suffix test) @@ -118,12 +113,10 @@ else fi # The rebuild after the restore also removes the outputs with the new suffix. -timeout=30 -while [ "$timeout" -gt 0 ] && [ -n "$(find . -name '*.res.mjs' -print -quit)" ]; do - sleep 1 - timeout=$((timeout - 1)) -done -if [ -n "$(find . -name '*.res.mjs' -print -quit)" ]; then +suffixed_outputs_gone() { + [ -z "$(find . -name '*.res.mjs' -print -quit)" ] +} +if ! poll 30 suffixed_outputs_gone; then error "Outputs with the restored configuration's previous suffix were not removed" find . -name '*.res.mjs' git restore --worktree -- ./src/Test.res diff --git a/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh b/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh index 7de72226c2..90f8efa548 100755 --- a/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh +++ b/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh @@ -44,18 +44,13 @@ REWATCH_COMPILER_DOMAINS=1 "$REWATCH_EXECUTABLE" watch > rewatch.log 2>&1 & watch_pid=$! compiler_log=lib/rescript/.compiler.log +compiler_log_changed() { + [ -f "$compiler_log" ] && ! cmp -s "$compiler_log" "$1" && + grep -q '#Done(' "$compiler_log" +} + wait_for_changed_compiler_log() { - local baseline="$1"; local timeout - timeout=60 - while [ "$timeout" -gt 0 ]; do - if [ -f "$compiler_log" ] && ! cmp -s "$compiler_log" "$baseline" && - grep -q '#Done(' "$compiler_log"; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll 60 compiler_log_changed "$1" } if ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 60; then diff --git a/rewatch/tests/watch/08-watch-overlapping-source-removed.sh b/rewatch/tests/watch/08-watch-overlapping-source-removed.sh index e8c8066b57..0b6b594708 100755 --- a/rewatch/tests/watch/08-watch-overlapping-source-removed.sh +++ b/rewatch/tests/watch/08-watch-overlapping-source-removed.sh @@ -42,18 +42,13 @@ retained_output() { rewatch_bg watch > rewatch.log 2>&1 & success "Watcher Started" +retained_output_has() { + output=$(retained_output) + [ -n "$output" ] && grep -q "$1" "$output" +} + wait_for_retained_value() { - local value=$1 - local timeout=30 - while [ "$timeout" -gt 0 ]; do - output=$(retained_output) - if [ -n "$output" ] && grep -q "$value" "$output"; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll 30 retained_output_has "$1" } if ! wait_for_retained_value "= 1"; then @@ -70,12 +65,10 @@ success "Initial build completed" # watched on its own mkdir "$RETAINED/nested" echo 'let value = 1' > "$RETAINED/nested/Nested.res" -timeout=30 -until [ -n "$(find "$RETAINED/nested" -name 'Nested.*js')" ] || [ "$timeout" -le 0 ]; do - sleep 1 - timeout=$((timeout - 1)) -done -if [ -n "$(find "$RETAINED/nested" -name 'Nested.*js')" ]; then +nested_output_exists() { + [ -n "$(find "$RETAINED/nested" -name 'Nested.*js')" ] +} +if poll 30 nested_output_exists; then success "A folder created inside the retained folder is watched" else error "A folder created inside the retained folder is not watched"