From fcd6e874c3e3d39d723dafcccaa0bc85da566713 Mon Sep 17 00:00:00 2001 From: Brett Bonner Date: Tue, 10 Mar 2026 17:04:23 -0700 Subject: [PATCH] Mitigate persistent Hex tarball failures with cache and stronger retries --- .github/workflows/test.yml | 11 ++++- scripts/check.sh | 1 + scripts/ci/retry.sh | 38 +++++++++++++++ src/weft_lustre_ui/headless/input_otp.gleam | 52 +++++++++++++++++---- test/input_otp_test.gleam | 14 ++++++ 5 files changed, 105 insertions(+), 11 deletions(-) create mode 100755 scripts/ci/retry.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e603e63..3b46703 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,15 @@ jobs: otp-version: "28" gleam-version: "1.14.0" rebar3-version: "3" + - name: Cache Gleam dependency artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cache/gleam + weft_lustre_ui/build/packages + key: ${{ runner.os }}-gleam-deps-${{ hashFiles('weft_lustre_ui/gleam.toml', 'weft_lustre_ui/manifest.toml') }} + restore-keys: | + ${{ runner.os }}-gleam-deps- - run: sudo apt-get update && sudo apt-get install -y imagemagick - id: hex_deps name: Check Hex dependency semver availability @@ -61,7 +70,7 @@ jobs: bash scripts/ci/run_full_checks_with_local_overrides.sh working-directory: weft_lustre_ui - if: steps.hex_deps.outputs.deps_ready == 'true' - run: gleam deps download + run: bash scripts/ci/retry.sh 12 10 gleam deps download working-directory: weft_lustre_ui - if: steps.hex_deps.outputs.deps_ready == 'true' run: | diff --git a/scripts/check.sh b/scripts/check.sh index 2fe343f..a71c6cd 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -6,6 +6,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" bash scripts/grep-gates.sh +bash scripts/ci/retry.sh 12 10 gleam deps download gleam format --check src test gleam build --target erlang --warnings-as-errors gleam build --target javascript --warnings-as-errors diff --git a/scripts/ci/retry.sh b/scripts/ci/retry.sh new file mode 100755 index 0000000..0d42e92 --- /dev/null +++ b/scripts/ci/retry.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ "$#" -lt 3 ]; then + echo "usage: retry.sh " >&2 + exit 2 +fi + +attempts="$1" +shift +sleep_seconds="$1" +shift + +if ! [[ "$attempts" =~ ^[0-9]+$ ]] || [ "$attempts" -lt 1 ]; then + echo "attempts must be a positive integer" >&2 + exit 2 +fi + +if ! [[ "$sleep_seconds" =~ ^[0-9]+$ ]] || [ "$sleep_seconds" -lt 0 ]; then + echo "sleep_seconds must be a non-negative integer" >&2 + exit 2 +fi + +command=("$@") + +for attempt in $(seq 1 "$attempts"); do + echo "Attempt ${attempt}/${attempts}: ${command[*]}" >&2 + if "${command[@]}"; then + exit 0 + fi + + if [ "$attempt" -lt "$attempts" ]; then + sleep "$sleep_seconds" + fi +done + +exit 1 diff --git a/src/weft_lustre_ui/headless/input_otp.gleam b/src/weft_lustre_ui/headless/input_otp.gleam index 34a390d..54311fd 100644 --- a/src/weft_lustre_ui/headless/input_otp.gleam +++ b/src/weft_lustre_ui/headless/input_otp.gleam @@ -116,13 +116,6 @@ fn normalized_chars(value: String, length: Int) -> List(String) { } } -fn first_grapheme_or_empty(value: String) -> String { - case string.to_graphemes(value) { - [head, ..] -> head - [] -> "" - } -} - fn replace_slot( chars chars: List(String), index index: Int, @@ -138,6 +131,47 @@ fn replace_slot( |> string.join(with: "") } +fn replace_slots_from_index( + chars chars: List(String), + index index: Int, + values values: List(String), +) -> String { + chars + |> list.index_map(with: fn(char, i) { + let offset = i - index + case offset < 0 { + True -> char + False -> { + case list.at(values, offset) { + Ok(value) -> value + Error(Nil) -> char + } + } + } + }) + |> string.join(with: "") +} + +/// Internal helper used by slot-level `on_input` handlers. +/// +/// Single-character updates replace only one slot. Multi-character updates +/// (such as paste events) fan out from the current index across subsequent +/// slots. +@internal +pub fn input_otp_apply_slot_input( + chars chars: List(String), + index index: Int, + input input: String, +) -> String { + let next_chars = string.to_graphemes(input) + + case next_chars { + [] -> replace_slot(chars, index, "") + [single] -> replace_slot(chars, index, single) + many -> replace_slots_from_index(chars, index, many) + } +} + /// Render an input-otp root. pub fn input_otp(config config: InputOtpConfig(msg)) -> weft_lustre.Element(msg) { case config { @@ -159,8 +193,7 @@ pub fn input_otp(config config: InputOtpConfig(msg)) -> weft_lustre.Element(msg) index: index, value: char, on_input: fn(input) { - let next = first_grapheme_or_empty(input) - on_change(replace_slot(chars, index, next)) + on_change(input_otp_apply_slot_input(chars, index, input)) }, disabled: disabled, attrs: slot_attrs, @@ -228,7 +261,6 @@ pub fn input_otp_slot( list.flatten([ [weft_lustre.html_attribute(attribute.type_("text"))], [weft_lustre.html_attribute(attribute.inputmode("numeric"))], - [weft_lustre.html_attribute(attribute.attribute("maxlength", "1"))], [ weft_lustre.html_attribute(attribute.attribute( "data-slot", diff --git a/test/input_otp_test.gleam b/test/input_otp_test.gleam index f953bcc..f665bbd 100644 --- a/test/input_otp_test.gleam +++ b/test/input_otp_test.gleam @@ -30,6 +30,17 @@ pub fn input_otp_tests() { string.contains(rendered, "data-index=\"3\"") |> expect.to_equal(expected: True) }), + it("fans pasted values across subsequent slots", fn() { + let next = + headless_input_otp.input_otp_apply_slot_input( + chars: ["0", "", "", ""], + index: 1, + input: "789", + ) + + next + |> expect.to_equal(expected: "0789") + }), ]), describe("headless rendering", [ it("renders group/slot/separator slot markers", fn() { @@ -55,6 +66,9 @@ pub fn input_otp_tests() { string.contains(rendered, "data-slot=\"input-otp-slot\"") |> expect.to_equal(expected: True) + string.contains(rendered, "maxlength=\"1\"") + |> expect.to_equal(expected: False) + string.contains(rendered, "data-slot=\"input-otp-separator\"") |> expect.to_equal(expected: True) }),