From 70ce9470fa1fa4ce313196c176877f4a900e0502 Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Wed, 3 Jun 2026 16:11:23 +0700 Subject: [PATCH 1/6] Re-enabling slack notificatiosn --- .github/workflows/build-docker.yml | 98 +++++++++++++++--------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index b806582..5ba50b9 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -42,24 +42,24 @@ jobs: status: FAILED color: danger - # notify_slack: - # name: Notify slack of build - # runs-on: ubuntu-latest - # needs: - # - test - # outputs: - # slack_message_id: ${{ steps.slack.outputs.message_id }} - # steps: - # - name: Notify slack - build starting - # uses: zuplo/github-action-slack-notify-build@v2 - # if: success() - # id: slack - # env: - # SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - # with: - # channel: build - # status: STARTING - # color: warning + notify_slack: + name: Notify slack of build + runs-on: ubuntu-latest + needs: + - test + outputs: + slack_message_id: ${{ steps.slack.outputs.message_id }} + steps: + - name: Notify slack - build starting + uses: zuplo/github-action-slack-notify-build@v2 + if: success() + id: slack + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + with: + channel: build + status: STARTING + color: warning determine_environment: name: Determine environment @@ -158,16 +158,16 @@ jobs: if: ${{ always() }} run: docker logout - # - name: Notify slack - build result failed - # uses: zuplo/github-action-slack-notify-build@v2 - # if: failure() - # env: - # SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - # with: - # message_id: ${{ needs.notify_slack.outputs.slack_message_id }} - # channel: build - # status: FAILED - # color: danger + - name: Notify slack - build result failed + uses: zuplo/github-action-slack-notify-build@v2 + if: failure() + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + with: + message_id: ${{ needs.notify_slack.outputs.slack_message_id }} + channel: build + status: FAILED + color: danger # Join the two layers back together merge: @@ -218,24 +218,24 @@ jobs: if: ${{ always() }} run: docker logout - # - name: Notify slack - build result successful - # uses: zuplo/github-action-slack-notify-build@v2 - # if: success() - # env: - # SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - # with: - # message_id: ${{ needs.notify_slack.outputs.slack_message_id }} - # channel: build - # status: SUCCESS - # color: good - - # - name: Notify slack - build result failed - # uses: zuplo/github-action-slack-notify-build@v2 - # if: failure() - # env: - # SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - # with: - # message_id: ${{ needs.notify_slack.outputs.slack_message_id }} - # channel: build - # status: FAILED - # color: danger + - name: Notify slack - build result successful + uses: zuplo/github-action-slack-notify-build@v2 + if: success() + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + with: + message_id: ${{ needs.notify_slack.outputs.slack_message_id }} + channel: build + status: SUCCESS + color: good + + - name: Notify slack - build result failed + uses: zuplo/github-action-slack-notify-build@v2 + if: failure() + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + with: + message_id: ${{ needs.notify_slack.outputs.slack_message_id }} + channel: build + status: FAILED + color: danger From 258eb0ea462a0f4a5b6839d5ca5f1d4ae0b27a7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:22:48 +0000 Subject: [PATCH 2/6] Bump voxmedia/github-action-slack-notify-build from 1 to 2 Bumps [voxmedia/github-action-slack-notify-build](https://github.com/voxmedia/github-action-slack-notify-build) from 1 to 2. - [Release notes](https://github.com/voxmedia/github-action-slack-notify-build/releases) - [Changelog](https://github.com/voxmedia/github-action-slack-notify-build/blob/main/CHANGELOG.md) - [Commits](https://github.com/voxmedia/github-action-slack-notify-build/compare/v1...v2) --- updated-dependencies: - dependency-name: voxmedia/github-action-slack-notify-build dependency-version: '2' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2e1cac..e184694 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: fi - name: Notify slack - test results - uses: voxmedia/github-action-slack-notify-build@v1 + uses: voxmedia/github-action-slack-notify-build@v2 if: failure() env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} From 1c794273ccdca97d39fc057d9d8379c2a1facb10 Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Tue, 23 Jun 2026 16:33:45 +0100 Subject: [PATCH 3/6] Stop timestamping blockquotes; one timestamp per list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transcript blockquotes are callouts rather than spoken-at-a-timestamp content, yet apply! stamped every paragraph inside one — including the quote's first paragraph, which the carolus CSS positions out in the left gutter, and any list nested in the quote. Lists were also stamped once per item, which is noisy when the instructor reads a list as a unit. Skip any paragraph with a blockquote ancestor (walking the full chain so nested list items are caught too), and for lists only stamp the first paragraph of the first item so the list carries a single timestamp. Adds the first tests to the repo: an integration test that runs the real timestamper against a real WebVTT fixture. Run with: bundle exec ruby test/renderer/markdown_timestamper_test.rb Co-Authored-By: Claude Opus 4.8 (1M context) --- app/lib/renderer/markdown_timestamper.rb | 36 ++++++++++ test/fixtures/transcript.vtt | 10 +++ test/renderer/markdown_timestamper_test.rb | 76 ++++++++++++++++++++++ test/test_helper.rb | 13 ++++ 4 files changed, 135 insertions(+) create mode 100644 test/fixtures/transcript.vtt create mode 100644 test/renderer/markdown_timestamper_test.rb create mode 100644 test/test_helper.rb diff --git a/app/lib/renderer/markdown_timestamper.rb b/app/lib/renderer/markdown_timestamper.rb index b487580..c5308f9 100644 --- a/app/lib/renderer/markdown_timestamper.rb +++ b/app/lib/renderer/markdown_timestamper.rb @@ -19,6 +19,15 @@ def apply! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/Cycl # We only care about paragraph nodes next unless node.type == :paragraph + # Blockquotes are callouts, not spoken-at-a-timestamp content. Skip every + # paragraph (and nested list item) inside one, so we don't render a + # timestamp in the gutter outside the quote or alongside its contents. + next if inside_blockquote?(node) + + # Lists get a single timestamp for the whole list rather than one per + # item, so only stamp the first paragraph of the first item. + next if list_item_paragraph?(node) && !first_list_paragraph?(node) + # Get the plain text for this paragraph and tidy it up paragraph = node.to_plaintext.gsub("\n", ' ').strip @@ -57,5 +66,32 @@ def apply! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/Cycl def vtt @vtt ||= WebVTT.read(vtt_path) if vtt_path.present? end + + private + + # True if any ancestor of the node is a blockquote (handles paragraphs and + # list items nested at any depth inside the quote). + def inside_blockquote?(node) + ancestor = node.parent + while ancestor + return true if ancestor.type == :blockquote + + ancestor = ancestor.parent + end + false + end + + def list_item_paragraph?(node) + node.parent&.type == :list_item + end + + # The very first paragraph of the very first item of its list. Loose list + # items can hold several paragraphs, so we check both the item and the + # paragraph position to guarantee exactly one timestamp per list. + def first_list_paragraph?(node) + list_item = node.parent + list = list_item.parent + list_item == list.first_child && list_item.first_child == node + end end end diff --git a/test/fixtures/transcript.vtt b/test/fixtures/transcript.vtt new file mode 100644 index 0000000..0b1b272 --- /dev/null +++ b/test/fixtures/transcript.vtt @@ -0,0 +1,10 @@ +WEBVTT + +00:00:01.000 --> 00:00:04.000 +Alright at this point we have created the bones of our user interface + +00:00:05.000 --> 00:00:09.000 +A normal top level list the instructor reads aloud one two three + +00:00:10.000 --> 00:00:14.000 +And finally a closing thought to round things out nicely diff --git a/test/renderer/markdown_timestamper_test.rb b/test/renderer/markdown_timestamper_test.rb new file mode 100644 index 0000000..39b3c86 --- /dev/null +++ b/test/renderer/markdown_timestamper_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require_relative '../test_helper' + +module Renderer + # Exercises the real timestamper against a real WebVTT fixture (no stubbing). + # The fixture only needs to contain a few cues — the Levenshtein matcher will + # assign *some* timestamp to every paragraph we don't deliberately skip, so the + # assertions here are about which nodes get a `$[t=...]` marker, not which one. + class MarkdownTimestamperTest < Minitest::Test + include TestHelpers + + TIMESTAMP = /\$\[t=[\d:.]+\]/ + + def render(markdown) + doc = CommonMarker.render_doc(markdown, %i[SMART], %i[table strikethrough autolink]) + MarkdownTimestamper.new(doc, fixture_path('transcript.vtt')).apply! + doc.to_html + end + + def test_spoken_paragraph_is_timestamped + html = render("Alright, at this point we have created the bones.\n") + + assert_match TIMESTAMP, html + end + + def test_blockquote_paragraphs_are_not_timestamped + html = render(<<~MD) + > **Update Notes:** a callout the instructor reads aloud. + > + > Apple's Human Interface Guidelines are worth a read. + MD + + refute_match TIMESTAMP, html, 'blockquote paragraphs should never be timestamped' + end + + def test_nested_list_inside_a_blockquote_is_not_timestamped + html = render(<<~MD) + > To open the Modifiers Library: + > + > - Click View - Show Library and click the second button, or + > - Press Shift-Command-L and click the second button. + MD + + refute_match TIMESTAMP, html, 'a list nested in a blockquote should never be timestamped' + end + + def test_a_list_gets_a_single_timestamp_rather_than_one_per_item + html = render(<<~MD) + A normal top level list the instructor reads: + + - One + - Two + - Three + MD + + list = html[%r{
    .*
}m] + + assert_equal 1, list.scan(TIMESTAMP).length, 'a list should carry exactly one timestamp' + assert_match %r{
  • #{TIMESTAMP.source}\s*One
  • }, list, 'the timestamp belongs on the first item' + end + + def test_loose_list_item_with_several_paragraphs_still_gets_one_timestamp + html = render(<<~MD) + - First item paragraph one. + + First item paragraph two, still the first item. + - Second item + MD + + list = html[%r{
      .*
    }m] + + assert_equal 1, list.scan(TIMESTAMP).length, 'a loose list should still carry exactly one timestamp' + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..ab3db18 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Boot the full robles environment (Bundler + Zeitwerk autoloading) so tests can +# exercise the real classes against their real dependencies. +require_relative '../config/application' + +require 'minitest/autorun' + +module TestHelpers + def fixture_path(name) + File.expand_path(File.join('fixtures', name), __dir__) + end +end From 518dcdf901143e8706ce8c5b08e02c8f8b5ac8a0 Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Tue, 23 Jun 2026 17:25:36 +0100 Subject: [PATCH 4/6] Upgreading gems --- Gemfile.lock | 51 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 580dfb6..96f74a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,8 +27,8 @@ GEM public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1255.0) - aws-sdk-core (3.250.0) + aws-partitions (1.1262.0) + aws-sdk-core (3.252.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -36,10 +36,10 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.128.0) + aws-sdk-kms (1.129.0) aws-sdk-core (~> 3, >= 3.248.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.224.0) + aws-sdk-s3 (1.226.0) aws-sdk-core (~> 3, >= 3.248.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -51,8 +51,10 @@ GEM bigdecimal (4.1.2) cli-ui (2.7.0) coderay (1.1.3) + commander (5.0.0) + highline (~> 3.0.0) commonmarker (0.23.12) - concurrent-ruby (1.3.6) + concurrent-ruby (1.3.7) connection_pool (3.0.2) diff-lcs (1.6.2) drb (2.2.3) @@ -60,11 +62,11 @@ GEM eventmachine (>= 0.12.9) http_parser.rb (~> 0) eventmachine (1.2.7) - faraday (2.14.2) + faraday (2.14.3) faraday-net_http (>= 2.0, < 3.5) json logger - faraday-net_http (3.4.3) + faraday-net_http (3.4.4) net-http (~> 0.5) faraday-retry (2.4.0) faraday (~> 2.0) @@ -82,7 +84,7 @@ GEM addressable (~> 2.8) process_executer (~> 4.0) rchardet (~> 1.9) - google-protobuf (4.35.0) + google-protobuf (4.35.1) bigdecimal rake (~> 13.3) guard (2.20.1) @@ -101,13 +103,14 @@ GEM guard (~> 2.8) guard-compat (~> 1.0) multi_json (~> 1.8) + highline (3.0.1) http_parser.rb (0.8.1) - i18n (1.14.8) + i18n (1.15.2) concurrent-ruby (~> 1.0) io-console (0.8.2) jaro_winkler (1.7.0) jmespath (1.6.2) - json (2.19.7) + json (2.20.0) kramdown (2.5.2) rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) @@ -133,7 +136,7 @@ GEM net-http (0.9.1) uri (>= 0.11.1) nio4r (2.7.5) - nokogiri (1.19.3) + nokogiri (1.19.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) notiffany (0.1.3) @@ -146,6 +149,11 @@ GEM open3 (0.2.1) ostruct (0.6.3) parallel (2.1.0) + parlour (9.1.2) + commander (~> 5.0) + parser + rainbow (~> 3.0) + sorbet-runtime (>= 0.5) parser (3.3.11.1) ast (~> 2.4.1) racc @@ -181,7 +189,7 @@ GEM ffi (~> 1.0) rbnacl (7.1.2) ffi (~> 1) - rbs (4.0.2) + rbs (4.0.3) logger prism (>= 1.6.0) tsort @@ -192,7 +200,7 @@ GEM reverse_markdown (3.0.2) nokogiri rexml (3.4.4) - rubocop (1.87.0) + rubocop (1.88.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -207,7 +215,7 @@ GEM parser (>= 3.3.7.2) prism (~> 1.7) ruby-progressbar (1.13.0) - sass-embedded (1.100.0) + sass-embedded (1.101.0) google-protobuf (~> 4.31) rake (>= 13) sawyer (0.9.3) @@ -223,7 +231,7 @@ GEM rack-session (>= 2.0.0, < 3) tilt (~> 2.0) slack-notifier (2.4.0) - solargraph (0.59.2) + solargraph (0.60.1) ast (~> 2.4.3) backport (~> 1.2) benchmark (~> 0.4) @@ -241,11 +249,20 @@ GEM rbs (>= 3.10.0) reverse_markdown (~> 3.0) rubocop (~> 1.76) + sord (~> 7.0) thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) yard-activesupport-concern (~> 0.0) yard-solargraph (~> 0.1) + sorbet-runtime (0.6.13310) + sord (7.1.0) + commander (~> 5.0) + parlour (~> 9.1) + parser + rbs (>= 3.0, < 5) + sorbet-runtime + yard thor (1.5.0) tilt (2.7.0) track_open_instances (0.1.15) @@ -257,7 +274,7 @@ GEM unicode-emoji (4.2.0) uri (1.1.1) webrick (1.9.2) - websocket-driver (0.8.0) + websocket-driver (0.8.2) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -305,4 +322,4 @@ DEPENDENCIES zeitwerk (~> 2.3) BUNDLED WITH - 4.0.12 + 4.0.14 From 39a960399d8ce1afa0aec15bc044980de5acdb26 Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Tue, 23 Jun 2026 17:25:40 +0100 Subject: [PATCH 5/6] Rubocop fixes --- app/models/choices_validator.rb | 2 +- app/models/parts_validator.rb | 2 +- app/server/robles_book_server.rb | 2 +- app/server/robles_content_module_server.rb | 2 +- app/server/robles_pablo_server.rb | 2 +- app/server/robles_video_server.rb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/choices_validator.rb b/app/models/choices_validator.rb index 1ad2d9e..65430c4 100644 --- a/app/models/choices_validator.rb +++ b/app/models/choices_validator.rb @@ -19,7 +19,7 @@ def check_correct_class(record, attribute, value) def check_unique_refs(record, attribute, value) return unless value.is_a?(Array) - ref_counts = value.map(&:ref).each_with_object(Hash.new(0)) { |ref, counts| counts[ref] += 1 } + ref_counts = value.map(&:ref).tally ref_counts.each do |ref, count| next if count == 1 diff --git a/app/models/parts_validator.rb b/app/models/parts_validator.rb index 13b7419..a9548a4 100644 --- a/app/models/parts_validator.rb +++ b/app/models/parts_validator.rb @@ -20,7 +20,7 @@ def check_unique_refs(record, attribute, value) episodes = value.flat_map(&:episodes) - ref_counts = episodes.map(&:ref).each_with_object(Hash.new(0)) { |ref, counts| counts[ref] += 1 } + ref_counts = episodes.map(&:ref).tally ref_counts.each do |ref, count| next if count == 1 diff --git a/app/server/robles_book_server.rb b/app/server/robles_book_server.rb index f0758d0..a1aaee3 100644 --- a/app/server/robles_book_server.rb +++ b/app/server/robles_book_server.rb @@ -9,7 +9,7 @@ class RoblesBookServer < Sinatra::Application set :bind, '0.0.0.0' set :views, "#{__dir__}/views" set :public_folder, "#{__dir__}/public" - set :static_cache_control, [max_age: 0] + set :static_cache_control, [{ max_age: 0 }] use Rack::LiveReload, host: 'localhost', source: :vendored diff --git a/app/server/robles_content_module_server.rb b/app/server/robles_content_module_server.rb index d3adb21..d051276 100644 --- a/app/server/robles_content_module_server.rb +++ b/app/server/robles_content_module_server.rb @@ -7,7 +7,7 @@ class RoblesContentModuleServer < Sinatra::Application # rubocop:disable Metrics set :bind, '0.0.0.0' set :views, "#{__dir__}/views" set :public_folder, "#{__dir__}/public" - set :static_cache_control, [max_age: 0] + set :static_cache_control, [{ max_age: 0 }] use Rack::LiveReload, host: 'localhost', source: :vendored diff --git a/app/server/robles_pablo_server.rb b/app/server/robles_pablo_server.rb index be481f1..57cc423 100644 --- a/app/server/robles_pablo_server.rb +++ b/app/server/robles_pablo_server.rb @@ -7,7 +7,7 @@ class RoblesPabloServer < Sinatra::Application set :bind, '0.0.0.0' set :views, "#{__dir__}/views" set :public_folder, "#{__dir__}/public" - set :static_cache_control, [max_age: 0] + set :static_cache_control, [{ max_age: 0 }] set :image_extractor, nil set :local, true set :host_authorization, { permitted_hosts: [] } diff --git a/app/server/robles_video_server.rb b/app/server/robles_video_server.rb index f5e9e59..af04914 100644 --- a/app/server/robles_video_server.rb +++ b/app/server/robles_video_server.rb @@ -7,7 +7,7 @@ class RoblesVideoServer < Sinatra::Application set :bind, '0.0.0.0' set :views, "#{__dir__}/views" set :public_folder, "#{__dir__}/public" - set :static_cache_control, [max_age: 0] + set :static_cache_control, [{ max_age: 0 }] use Rack::LiveReload, host: 'localhost', source: :vendored From e0c5ea4cfe64a30713a1fc54489bff9ea7ec6907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:30:08 +0000 Subject: [PATCH 6/6] Bump actions/checkout from 6 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build-docker.yml | 6 +++--- .github/workflows/test.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 5ba50b9..2463ff0 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Run tests run: | @@ -68,7 +68,7 @@ jobs: environment: ${{ steps.set_environment.outputs.environment }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set environment id: set_environment @@ -120,7 +120,7 @@ jobs: type=raw,value=latest,enable=${{ github.ref_name == 'master' }} type=raw,value=staging,enable=${{ needs.determine_environment.outputs.environment == 'staging' }} - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Docker Buildx id: buildx diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2e1cac..2b1ad44 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Run tests run: |