Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/data-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -33,6 +37,19 @@ on:
description: "Index page title (default: '<FLAVOR> 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 (<meta name=\"description\"> and header subtitle)"
required: false
default: ""
type: string
data-dir:
description: "Folder of Relaton YAML documents"
required: false
Expand Down Expand Up @@ -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
Expand Down
65 changes: 55 additions & 10 deletions spec/data_deploy_workflow_spec.rb
Original file line number Diff line number Diff line change
@@ -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