diff --git a/.github/workflows/data-deploy.yml b/.github/workflows/data-deploy.yml index dcb3c55..afc6c42 100644 --- a/.github/workflows/data-deploy.yml +++ b/.github/workflows/data-deploy.yml @@ -14,6 +14,10 @@ name: Deploy # deploy: # uses: relaton/support/.github/workflows/data-deploy.yml@main # +# Optionally, a caller passes the per-flavor branding its `_config.yml` used to +# carry — `title`, `favicon` and `description` (see relaton/relaton#96). Left +# unset, the page carries no favicon and no description, exactly as before. +# # Two source modes (input `source`): # * gem (default) — install the released relaton-cli gem. Ruby only; the gem # ships the precompiled frontend bundle. @@ -33,6 +37,19 @@ on: description: "Index page title (default: ' Index' from the repo name)" required: false type: string + # Both build steps pass these through unconditionally: relaton-cli treats a + # blank value as absent, so the empty default keeps the output identical + # for callers that set neither. + favicon: + description: "Index page favicon URL or output-relative path (default: none)" + required: false + default: "" + type: string + description: + description: "Index page description ( and header subtitle)" + required: false + default: "" + type: string data-dir: description: "Folder of Relaton YAML documents" required: false @@ -129,21 +146,36 @@ jobs: - name: Build index (gem source) if: inputs.source == 'gem' + # The branding values are free-form prose, and `${{ }}` is substituted as + # raw text before bash parses the line — an inlined description reading + # `IEC "TC 1" registry` would word-split into stray argv entries. Passing + # them by env hands the runner's verbatim value to the shell. + env: + FAVICON: ${{ inputs.favicon }} + DESCRIPTION: ${{ inputs.description }} run: | relaton index "${{ inputs.data-dir }}" \ --output _site \ --mode "${{ inputs.mode }}" \ --title "${{ steps.meta.outputs.title }}" \ + --favicon "$FAVICON" \ + --description "$DESCRIPTION" \ --base-url "https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}" - name: Build index (git source) if: inputs.source == 'git' # BUNDLE_GEMFILE is exported to GITHUB_ENV in the build step above. + # FAVICON/DESCRIPTION travel by env for the same reason as the gem step. + env: + FAVICON: ${{ inputs.favicon }} + DESCRIPTION: ${{ inputs.description }} run: | bundle exec relaton index "${{ github.workspace }}/${{ inputs.data-dir }}" \ --output "${{ github.workspace }}/_site" \ --mode "${{ inputs.mode }}" \ --title "${{ steps.meta.outputs.title }}" \ + --favicon "$FAVICON" \ + --description "$DESCRIPTION" \ --base-url "https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}" - uses: actions/configure-pages@v4 diff --git a/spec/data_deploy_workflow_spec.rb b/spec/data_deploy_workflow_spec.rb index 82b40bf..2063515 100644 --- a/spec/data_deploy_workflow_spec.rb +++ b/spec/data_deploy_workflow_spec.rb @@ -1,19 +1,64 @@ -# Guards the reusable Pages workflow so it resolves gems against Gemfile.deploy -# (the jekyll + pubid bundle) rather than the data repo's own crawler Gemfile. +# Guards the reusable Pages workflow's caller contract: it builds the index with +# `relaton index` (support#58, which replaced the Jekyll build) and forwards the +# optional per-flavor branding a data repo used to carry in its `_config.yml` — +# title, favicon and description (relaton#96 added the latter two CLI flags). +# +# The two "Build index" steps (gem source / git source) are the fragile part: +# they are near-duplicate command lines, so a flag added to one and forgotten in +# the other silently drops branding for every repo on the other source mode. RSpec.describe ".github/workflows/data-deploy.yml" do repo_root = File.expand_path("..", __dir__) workflow = YAML.safe_load_file(File.join(repo_root, ".github/workflows/data-deploy.yml")) + # Psych reads the unquoted `on:` key as YAML 1.1 boolean true. + inputs = workflow.fetch(true).fetch("workflow_call").fetch("inputs") build_job = workflow.fetch("jobs").fetch("build_index_page") + index_steps = build_job.fetch("steps").select { |s| s["run"]&.match?(/\brelaton index\b/) } - it "points BUNDLE_GEMFILE at Gemfile.deploy for the build job" do - # setup-ruby's bundler-cache install AND `bundle exec jekyll build` both - # honor this env, so the whole job uses the layered deploy bundle. - bundle_gemfile = build_job.fetch("env").fetch("BUNDLE_GEMFILE") - expect(bundle_gemfile).to end_with("Gemfile.deploy") + it "keeps every input optional, so existing callers need no changes" do + expect(inputs.values).to all(include("required" => false)) end - it "still builds with `bundle exec jekyll build`" do - run_steps = build_job.fetch("steps").filter_map { |s| s["run"] } - expect(run_steps).to include(a_string_matching(/bundle exec jekyll build/)) + %w[favicon description].each do |input| + describe "the #{input} input" do + it "is declared, so callers can pass it" do + # A reusable workflow rejects an undeclared input at parse time + # ("Invalid input, 'favicon' is not defined in the referenced + # workflow"), so a caller cannot work around a missing declaration. + expect(inputs).to have_key(input) + end + + it "is an optional string defaulting to the empty string" do + # The empty default is load-bearing: it lets both build steps pass the + # flag unconditionally. relaton's generator runs `presence(...)` on both + # values, so `--favicon ""` is indistinguishable from not passing it. + expect(inputs.fetch(input)) + .to include("required" => false, "default" => "", "type" => "string") + end + end + end + + it "builds the index in both source modes" do + expect(index_steps.map { |s| s["if"] }) + .to contain_exactly("inputs.source == 'gem'", "inputs.source == 'git'") + end + + %w[favicon description].each do |input| + it "hands #{input} to the shell through the environment, not `${{ }}`" do + # GitHub substitutes `${{ }}` as raw text before bash parses the line, so + # an inlined value is shell source: a description reading + # `IEC "TC 1" registry` would word-split into stray argv entries, and one + # containing $(...) would execute. Via env the runner passes it verbatim. + index_steps.each do |step| + expect(step.fetch("env")).to include(input.upcase => "${{ inputs.#{input} }}") + expect(step.fetch("run")).not_to include("inputs.#{input}") + end + end + end + + %w[--title --favicon --description --base-url].each do |flag| + it "passes #{flag} in every build step" do + # Both invocations must stay in sync — see the note at the top. + expect(index_steps.map { |s| s.fetch("run") }).to all(include("#{flag} ")) + end end end