From f9b5803cca6177a548d4ca2e0a60c7782a44607c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 29 Sep 2026 19:05:34 -0500 Subject: [PATCH 1/2] Docs(fix): Record native build provenance why: Native docs need the same source and artifact verification as shared builds before the publisher receives AWS credentials. what: - Keep source, native exporter and shared docs checkouts separate. - Record clean input revisions before generation and upload an exact artifact descriptor for the reviewed shared publisher. - Disable executable caches for selected source builds and cover the snapshot, descriptor and paired workflow pins in focused regressions. --- .github/workflows/docs.yml | 84 +++++++++++++++++++++++++-------- test/unit/docs_workflow_test.rb | 47 ++++++++++++++++++ 2 files changed, 111 insertions(+), 20 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 922319e..a16189b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -176,7 +176,14 @@ jobs: build: needs: identity - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + LIBTMUX_DOCS_PORT: ruby + LIBTMUX_DOCS_VERSION: ${{ matrix.version }} + LIBTMUX_DOCS_SOURCE_REPOSITORY: ${{ needs.identity.outputs.source-repository }} + LIBTMUX_DOCS_CHECKOUT_RUBY: ${{ github.workspace }}/port + LIBTMUX_DOCS_GENERATOR_CHECKOUT: ${{ github.workspace }}/docs-generator strategy: fail-fast: false matrix: ${{ fromJSON(needs.identity.outputs.matrix) }} @@ -186,9 +193,11 @@ jobs: repository: ${{ needs.identity.outputs.source-repository }} ref: ${{ needs.identity.outputs.source-ref }} fetch-depth: 0 + path: port persist-credentials: false - id: source + working-directory: port env: SELECTED_REF: ${{ needs.identity.outputs.source-ref }} run: | @@ -204,40 +213,58 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} - path: .docs-generator + path: docs-generator persist-credentials: false - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: libtmux/docs - ref: 8fda4b89071621b9ed2c4722a68d61f6ecf9c1c4 - path: .site + ref: 42dd5b71e6eefd785b9bf8ce11d34e8f1a058a22 + path: docs persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '26' + + - name: Snapshot source and exporter before native generation + working-directory: docs + env: + LIBTMUX_DOCS_SOURCE_SHA: ${{ steps.source.outputs.sha }} + run: node scripts/publication-provenance.mjs snapshot "$RUNNER_TEMP/build-inputs.json" "$PWD" + - uses: ruby/setup-ruby@762794c140bbeda0f1224786aa33b4b46783a6c1 # v1.326.0 with: ruby-version: '4.0.7' - bundler-cache: true + working-directory: port + bundler-cache: false + + - name: Install the selected Ruby bundle + working-directory: port + env: + BUNDLE_FROZEN: 'true' + run: bundle install + + - name: Install tmux for native MCP discovery + run: | + sudo apt-get update -qq + sudo apt-get install --no-install-recommends -y -qq tmux - name: Export the selected Ruby source + working-directory: port run: | - bundle exec ruby .docs-generator/scripts/export-docs \ - --source "$GITHUB_WORKSPACE" \ - --output "$GITHUB_WORKSPACE/docs/_build" + bundle exec ruby "$GITHUB_WORKSPACE/docs-generator/scripts/export-docs" \ + --source "$GITHUB_WORKSPACE/port" \ + --output "$GITHUB_WORKSPACE/port/docs/_build" - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: - package_json_file: .site/package.json - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '26' - cache: pnpm - cache-dependency-path: .site/pnpm-lock.yaml + package_json_file: docs/package.json - run: pnpm install --frozen-lockfile - working-directory: .site + working-directory: docs - name: Build the selected Ruby documentation tree - working-directory: .site + working-directory: docs env: LIBTMUX_DOCS_PORT: ruby LIBTMUX_DOCS_VERSION: ${{ matrix.version }} @@ -246,13 +273,30 @@ jobs: LIBTMUX_DOCS_RESOLVES_TO: ${{ matrix.resolvesTo }} LIBTMUX_DOCS_SOURCE_REF: ${{ needs.identity.outputs.source-ref }} LIBTMUX_DOCS_SOURCE_SHA: ${{ steps.source.outputs.sha }} - LIBTMUX_DOCS_CHECKOUT_RUBY: ${{ github.workspace }} + LIBTMUX_DOCS_INPUT_SNAPSHOT: ${{ runner.temp }}/build-inputs.json run: ./scripts/build-site.sh --ports ruby --versions "${{ matrix.version }}" --skip-refs --skip-pagefind - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - id: content + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-ruby-${{ matrix.version }} - path: .site/_site/en/ruby/${{ matrix.version }} + path: docs/_site/en/ruby/${{ matrix.version }} + if-no-files-found: error + retention-days: 1 + include-hidden-files: true + + - name: Describe the uploaded artifact + working-directory: docs + env: + ARTIFACT_ID: ${{ steps.content.outputs.artifact-id }} + ARTIFACT_DIGEST: ${{ steps.content.outputs.artifact-digest }} + ARTIFACT_NAME: docs-ruby-${{ matrix.version }} + SOURCE_SHA: ${{ steps.source.outputs.sha }} + run: node scripts/publication-provenance.mjs descriptor "$RUNNER_TEMP/publication.json" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: docs-ruby-${{ matrix.version }}-publication + path: ${{ runner.temp }}/publication.json if-no-files-found: error retention-days: 1 @@ -265,7 +309,7 @@ jobs: permissions: contents: read id-token: write - uses: libtmux/docs/.github/workflows/reusable-deploy.yml@8fda4b89071621b9ed2c4722a68d61f6ecf9c1c4 + uses: libtmux/docs/.github/workflows/reusable-deploy.yml@42dd5b71e6eefd785b9bf8ce11d34e8f1a058a22 with: path-prefix: ruby/${{ matrix.version }} artifact: docs-ruby-${{ matrix.version }} diff --git a/test/unit/docs_workflow_test.rb b/test/unit/docs_workflow_test.rb index b961756..15756ee 100644 --- a/test/unit/docs_workflow_test.rb +++ b/test/unit/docs_workflow_test.rb @@ -1,12 +1,59 @@ # frozen_string_literal: true require "minitest/autorun" +require "yaml" class DocsWorkflowTest < Minitest::Test + def workflow + YAML.load_file(File.expand_path("../../.github/workflows/docs.yml", __dir__)) + end + def test_preview_publication_uses_the_preview_role workflow = File.read(File.expand_path("../../.github/workflows/docs.yml", __dir__)) assert_includes workflow, "role-arn: ${{ matrix.environment == 'docs-preview' && secrets.LIBTMUX_DOCS_PREVIEW_ROLE_ARN || secrets.LIBTMUX_DOCS_ROLE_ARN }}" end + + def test_source_exporter_and_docs_use_separate_checkouts + build = workflow.fetch("jobs").fetch("build") + steps = build.fetch("steps") + checkouts = steps.select { |step| step.fetch("uses", "").start_with?("actions/checkout@") } + assert_equal %w[port docs-generator docs], checkouts.map { |step| step.fetch("with").fetch("path") } + assert_equal "${{ github.workspace }}/port", build.fetch("env").fetch("LIBTMUX_DOCS_CHECKOUT_RUBY") + assert_equal "${{ github.workspace }}/docs-generator", build.fetch("env").fetch("LIBTMUX_DOCS_GENERATOR_CHECKOUT") + snapshot = steps.index { |step| step.fetch("run", "").include?("publication-provenance.mjs snapshot") } + exporter = steps.index { |step| step.fetch("name", "").start_with?("Export the selected") } + assert snapshot && exporter && snapshot < exporter + assert_equal "port", steps.fetch(exporter).fetch("working-directory") + assert_includes steps.fetch(exporter).fetch("run"), '--source "$GITHUB_WORKSPACE/port"' + tree = steps.find { |step| step.fetch("name", "").start_with?("Build the selected") } + assert_equal "${{ runner.temp }}/build-inputs.json", tree.fetch("env").fetch("LIBTMUX_DOCS_INPUT_SNAPSHOT") + assert_equal "${{ steps.source.outputs.sha }}", tree.fetch("env").fetch("LIBTMUX_DOCS_SOURCE_SHA") + end + + def test_descriptor_uses_the_exact_content_upload + jobs = workflow.fetch("jobs") + steps = jobs.fetch("build").fetch("steps") + content = steps.find { |step| step["id"] == "content" }.fetch("with") + assert_equal true, content.fetch("include-hidden-files") + descriptor = steps.find { |step| step.fetch("run", "").include?("publication-provenance.mjs descriptor") } + assert_equal "${{ steps.content.outputs.artifact-id }}", descriptor.fetch("env").fetch("ARTIFACT_ID") + assert_equal "${{ steps.content.outputs.artifact-digest }}", descriptor.fetch("env").fetch("ARTIFACT_DIGEST") + assert_equal content.fetch("name"), descriptor.fetch("env").fetch("ARTIFACT_NAME") + assert steps.any? { |step| step.dig("with", "name") == "#{content.fetch('name')}-publication" } + docs = steps.find { |step| step.dig("with", "repository") == "libtmux/docs" } + assert_match(/\A[0-9a-f]{40}\z/, docs.fetch("with").fetch("ref")) + assert jobs.fetch("publish").fetch("uses").end_with?("@#{docs.fetch('with').fetch('ref')}") + end + + def test_arbitrary_source_builds_do_not_restore_caches + steps = workflow.fetch("jobs").fetch("build").fetch("steps") + ruby = steps.find { |step| step.fetch("uses", "").start_with?("ruby/setup-ruby@") } + assert_equal false, ruby.fetch("with").fetch("bundler-cache") + node = steps.find { |step| step.fetch("uses", "").start_with?("actions/setup-node@") } + refute node.fetch("with").key?("cache") + bundle = steps.find { |step| step["run"] == "bundle install" } + assert_equal "true", bundle.fetch("env").fetch("BUNDLE_FROZEN") + end end From 2a9fe925f8da2fbe918b7899e1a4fa8824a4be0e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 29 Sep 2026 20:19:43 -0500 Subject: [PATCH 2/2] Docs(fix[publisher]): Own discovery daemon why: Ruby MCP discovery queries a live server. The previous shared generator created a private socket directory but did not start its daemon, so an isolated hosted build failed before publication. what: - Pin both the docs checkout and publisher to the reviewed generator fix - Keep source selection and artifact publication contracts unchanged - Verify the four workflow cases and full outer gate in 37.05 seconds The publisher revision is approved in all 12 live IAM trust policies. Hosted publication remains to be checked at this caller revision. --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a16189b..f6abe6d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -219,7 +219,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: libtmux/docs - ref: 42dd5b71e6eefd785b9bf8ce11d34e8f1a058a22 + ref: 99532a7ec07ae3af3ef399941e81a1d6424720cb path: docs persist-credentials: false @@ -309,7 +309,7 @@ jobs: permissions: contents: read id-token: write - uses: libtmux/docs/.github/workflows/reusable-deploy.yml@42dd5b71e6eefd785b9bf8ce11d34e8f1a058a22 + uses: libtmux/docs/.github/workflows/reusable-deploy.yml@99532a7ec07ae3af3ef399941e81a1d6424720cb with: path-prefix: ruby/${{ matrix.version }} artifact: docs-ruby-${{ matrix.version }}