Skip to content
Closed
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
78 changes: 78 additions & 0 deletions .github/workflows/screens-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Screens comment

# Second half of the screenshot workflow. Screens runs in the pull request's
# context (read-only, fork-safe) and leaves an artifact; this one runs in the
# repository's context, so it can publish the images and write the comment.
#
# The images go to a prerelease tagged `screens` — release assets are not part
# of the git history, so nothing lands in the repository or in a branch, and a
# clone does not carry them.
on:
workflow_run:
workflows: ["Screens"]
types: [completed]

permissions:
contents: write
pull-requests: write
actions: read

jobs:
publish:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: screens
path: screens
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Read the PR number
id: pr
run: echo "number=$(cat screens/pr-number)" >> "$GITHUB_OUTPUT"

- name: Publish the images as release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ steps.pr.outputs.number }}
run: |
set -euo pipefail
gh release view screens >/dev/null 2>&1 || \
gh release create screens --prerelease --title "Screenshots" \
--notes "Images referenced from pull request comments. Not part of the repository."

mkdir -p upload
for side in before after; do
for file in screens/$side/*.png; do
cp "$file" "upload/pr-$PR-$side-$(basename "$file")"
done
done
gh release upload screens upload/*.png --clobber

- name: Build and post the comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ steps.pr.outputs.number }}
BASE: ${{ github.event.workflow_run.pull_requests[0].base.ref || 'master' }}
run: |
set -euo pipefail
ruby test/screens/comment.rb \
--before screens/before --after screens/after --base "$BASE" \
--url-prefix "https://github.com/${{ github.repository }}/releases/download/screens/pr-$PR-" \
> comment.md

# Replace this workflow's previous comment instead of stacking a new
# one on every push.
existing=$(gh api "repos/${{ github.repository }}/issues/$PR/comments" \
--jq 'map(select(.body | startswith("<!-- theme-screens -->"))) | .[0].id // empty')
if [ -n "$existing" ]; then
gh api --method PATCH "repos/${{ github.repository }}/issues/comments/$existing" \
-F body=@comment.md >/dev/null
else
gh api --method POST "repos/${{ github.repository }}/issues/$PR/comments" \
-F body=@comment.md >/dev/null
fi
43 changes: 43 additions & 0 deletions .github/workflows/screens.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Screens

# Runs in the PR's own context, so it also works for pull requests from forks —
# which means it gets a read-only token and no secrets. It only produces an
# artifact; publishing the images and posting the comment is screens-comment.yml,
# which runs afterwards with the repository's own permissions.
on:
pull_request:
paths:
- 'app/assets/stylesheets/**'
- 'test/screens/**'
- '.github/workflows/screens.yml'

permissions:
contents: read

jobs:
shoot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true

- name: Shoot both revisions
run: bundle exec rake "screens[origin/${{ github.base_ref }}]"

- name: Record which PR this was
run: echo "${{ github.event.pull_request.number }}" > tmp/screens/pr-number

- uses: actions/upload-artifact@v4
with:
name: screens
path: |
tmp/screens/before
tmp/screens/after
tmp/screens/pr-number
retention-days: 7
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/Gemfile.lock
/pkg/
/tmp/
*.gem
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ gemspec
group :development, :test do
gem 'rake'
gem 'sassc'
# Screenshots. Ferrum is a pure-Ruby CDP client, so CI needs no Node
# toolchain — only the Chrome that the runner already has. Rails itself is
# not here: test/screens/dummy.rb generates an app that bundles its own.
gem 'ferrum'
end
20 changes: 20 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
require "bundler/gem_tasks"
require "tmpdir"

desc "Compile the theme against ActiveAdmin and check the result"
task :css do
ruby "test/css_check.rb"
end

task default: :css

desc "Shoot the screen gallery for this branch and for its base"
task :screens, [:base] do |_task, args|
base = args[:base] || ENV.fetch("BASE_REF", "origin/master")
app = ENV.fetch("DUMMY_APP", File.join(Dir.tmpdir, "active-admin-theme-screens"))
out = ENV.fetch("SCREENS_OUT", File.expand_path("tmp/screens"))
theme = "app/assets/stylesheets/wigu/active_admin_theme.scss"

ruby "test/screens/dummy.rb #{app}"

# The base revision of the theme, compiled from git rather than from a
# checkout, so the dummy app is built once and reused for both runs.
before_root = File.join(out, "before-theme", "wigu")
mkdir_p before_root
File.write(File.join(before_root, "active_admin_theme.scss"), `git show #{base}:#{theme}`)

ruby "test/screens/shoot.rb --app #{app} --out #{out}/after --theme-root app/assets/stylesheets"
ruby "test/screens/shoot.rb --app #{app} --out #{out}/before --theme-root #{File.join(out, "before-theme")}"
end
73 changes: 73 additions & 0 deletions test/screens/comment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Renders the PR comment: one row per scenario, before on the left, after on
# the right. Images are referenced by URL — nothing is committed to the repo.
#
# ruby test/screens/comment.rb --before tmp/screens/before --after tmp/screens/after \
# --url-prefix https://github.com/owner/repo/releases/download/screens/pr-51-
require "json"
require "optparse"

options = { before: "tmp/screens/before", after: "tmp/screens/after", url_prefix: "" }
OptionParser.new do |opts|
opts.on("--before DIR") { |v| options[:before] = v }
opts.on("--after DIR") { |v| options[:after] = v }
opts.on("--url-prefix URL") { |v| options[:url_prefix] = v }
opts.on("--base REF") { |v| options[:base] = v }
end.parse!

def index(dir)
path = File.join(dir, "index.json")
File.exist?(path) ? JSON.parse(File.read(path), symbolize_names: true) : []
end

before = index(options[:before]).to_h { |s| [s[:name], s] }
after = index(options[:after])

puts "<!-- theme-screens -->"
puts "## Screens"
puts
puts "Every scenario shot twice against the same ActiveAdmin and the same dummy " \
"admin: once with the theme as it is on `#{options[:base] || "the base branch"}`, " \
"once with the theme from this PR. Nothing is committed — the images are " \
"release assets."
puts

changed = []
unchanged = []

after.each do |shot|
next unless shot[:file]
pair = before[shot[:name]]
(pair && pair[:digest] == shot[:digest] ? unchanged : changed) << [shot, pair]
end

if changed.empty?
puts "This PR does not change any of the #{after.size} scenarios."
else
changed.each do |shot, pair|
puts "### #{shot[:name]}"
puts
puts shot[:why] if shot[:why]
puts
puts "| before | after |"
puts "|---|---|"
b = pair&.dig(:file) ? "![before](#{options[:url_prefix]}before-#{pair[:file]})" : "_not shot_"
puts "| #{b} | ![after](#{options[:url_prefix]}after-#{shot[:file]}) |"
puts
end
end

unless unchanged.empty?
puts "<details><summary>Unchanged by this PR (#{unchanged.size})</summary>"
puts
unchanged.each { |shot, _| puts "- #{shot[:name]}" }
puts
puts "</details>"
puts
end

failures = after.select { |s| s[:error] }
unless failures.empty?
puts "> [!WARNING]"
puts "> #{failures.size} scenario(s) could not be shot, so they are not compared here:"
failures.each { |s| puts "> - `#{s[:name]}` — #{s[:error]}" }
end
Loading
Loading