Skip to content
Draft
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

## [Unreleased]

### Added
- Initial `opennews-rake-tasks` gem with shared Rake tasks for OpenNews Jekyll static sites
- `validate_yaml`, `check`, `build`, `serve`, `clean` core tasks
- `test` namespace: `html_proofer`, `templates`, `page_config`, `placeholders`, `a11y`, `performance`
- `review` namespace: `external_links`, `compare_deployed_sites`
- `format`/`lint` tasks wrapping StandardRB and Prettier
- `outdated` tasks wrapping `bundle outdated`
- `OpenNews::RakeTasks.configure` API for repo-specific overrides (ignore lists, required files)
135 changes: 135 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ For **Dependabot PRs**, check the release notes for the updated action (linked f
| `jekyll-build/action.yml` | Composite Action | Checkout, setup Ruby, validate YAML, run checks, build Jekyll, run tests |
| `.github/workflows/jekyll-deploy.yml` | Reusable Workflow | Build, deploy to S3, invalidate CloudFront, update GitHub Deployment status |
| `.github/workflows/jekyll-health-check.yml` | Reusable Workflow | Build check + open a GitHub Issue on failure |
| `opennews-rake-tasks.gemspec` | Ruby gem | Shared Rake tasks for local development and CI |

---

Expand Down Expand Up @@ -135,3 +136,137 @@ A floating `@latest` tag is also maintained and always points to the most recent
Every merge to `main` automatically publishes a new patch release via `.github/workflows/release.yml`. Release notes are generated from PR titles, categorized by label (breaking change, enhancement, bug, dependencies).

For breaking changes, manually bump the major version by creating and push the tag via git. This is the only way to break out of the auto-versioning count to a v2 or v3 or wherever you are.

---

## `opennews-rake-tasks` gem

Shared Rake tasks for local development and CI across all OpenNews Jekyll static sites. Load them with one line in your `Rakefile` instead of copying task files from repo to repo.

### What's included

| Task | Description |
|---|---|
| `validate_yaml` | Syntax + duplicate-key check on `_config.yml` and `_data/**/*.{yml,yaml}` |
| `check` | Validates required files and warns on missing deployment config |
| `build` | Runs a Jekyll build into `_site/` |
| `serve` | Starts `jekyll serve --livereload` |
| `clean` | Removes `_site/`, `.jekyll-cache`, `.sass-cache`, `.jekyll-metadata` |
| `test` | Runs all sub-tasks below |
| `test:html_proofer` | Internal link + HTTPS check via html-proofer |
| `test:templates` | Lint Liquid `if`/`for` balance and href escaping |
| `test:page_config` | Checks `permalink` front-matter in root Markdown files |
| `test:placeholders` | Flags TODO/FIXME/XXX/PLACEHOLDER in the built site |
| `test:a11y` | Basic accessibility checks (alt text, lang attr, empty headings) |
| `test:performance` | Flags large HTML/CSS files and inline base64 images |
| `review:external_links` | Live external-URL check via html-proofer (slow, needs network) |
| `review:compare_deployed_sites` | Diffs staging vs production over HTTP |
| `lint` | `format:ruby` + `format:prettier` |
| `format` | `format:ruby_fix` + `format:prettier_fix` |
| `format:ruby` / `format:ruby_fix` | StandardRB check / auto-fix |
| `format:prettier` / `format:prettier_fix` | Prettier check / auto-fix |
| `outdated` / `outdated:direct` / `outdated:all` | `bundle outdated` wrappers |

### Setup

#### Option 1 — RubyGems (recommended)

```ruby
# Gemfile
gem "opennews-rake-tasks"
```

```sh
bundle install
```

#### Option 2 — git source (no publish step, useful during development)

```ruby
# Gemfile
gem "opennews-rake-tasks", github: "OpenNews/opennews-actions", glob: "opennews-rake-tasks.gemspec"
```

#### Option 3 — GitHub Packages

```ruby
# Gemfile
source "https://rubygems.pkg.github.com/OpenNews" do
gem "opennews-rake-tasks"
end
```

> Requires a `BUNDLE_RUBYGEMS__PKG__GITHUB__COM` token set in the environment or `~/.bundle/config`.

#### Option 4 — separate `opennews-rake-tasks` repo

Publish the gem from its own dedicated repo and reference it like Option 1.

**Distribution options comparison:**

Option | Pros | Cons
------ | ---- | ----
RubyGems public gem | Most standard; Dependabot works out of the box; easy for public and private repos | Requires publish & version management; possibly public
GitHub Packages gem | Keeps in-org; works for private repos | Requires token config in Gemfile; lock-in to GH infra
GitHub-only (gem '...', github:) | No publish step, easy to update from HEAD or tag | Ties Gemfile to repo structure; Dependabot may not fully support
Separate opennews-rake-tasks repo | Clean separation, can be public or private | One more repo to track/maintain

### Usage

Add one line to your `Rakefile`:

```ruby
require "opennews/rake_tasks"
```

All tasks are now available via `bundle exec rake`.

### Extending / overriding

Tasks that rely on repo-specific ignore lists or file requirements are configurable. Call `OpenNews::RakeTasks.configure` **before** (or after) loading the tasks — it updates the shared `Configuration` object that every task reads at runtime.

```ruby
# Rakefile
require "opennews/rake_tasks"

OpenNews::RakeTasks.configure do |config|
# Add site-specific URLs to skip in internal link checks
config.html_proofer_ignore_urls += [
/mitrakalita\.com/,
%r{opennews\.us5\.list-manage\.com/},
]

# Add site-specific URLs to skip in external link review
config.external_links_ignore_urls += [
/etherpad\.mozilla\.org/,
/lcc-slack\.herokuapp\.com/,
]

# Skip html-proofer checks in specific directories
config.html_proofer_ignore_files << %r{blog/}

# Remove package.json from required-file checks if this repo has no npm setup
config.required_files -= ["package.json"]
end
```

All configuration attributes and their defaults are documented in
[`lib/opennews/rake_tasks/configuration.rb`](lib/opennews/rake_tasks/configuration.rb).

### Upgrading

Update the gem version in `Gemfile` (or let Dependabot open a PR) and run `bundle update opennews-rake-tasks`.

### Development

```sh
git clone https://github.com/OpenNews/opennews-actions.git
cd opennews-actions
bundle install # installs gem dependencies declared in opennews-rake-tasks.gemspec
```

To build the gem locally:

```sh
gem build opennews-rake-tasks.gemspec
```
14 changes: 14 additions & 0 deletions lib/opennews/rake_tasks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require "opennews/rake_tasks/version"
require "opennews/rake_tasks/configuration"

module OpenNews
module RakeTasks
# Load all .rake files bundled with the gem.
def self.load_tasks
Dir.glob(File.join(__dir__, "rake_tasks", "*.rake")).sort.each { |f| load f }
end
end
end

# Auto-load tasks when this file is required (the common Rakefile pattern).
OpenNews::RakeTasks.load_tasks
106 changes: 106 additions & 0 deletions lib/opennews/rake_tasks/build.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
require "jekyll"
require "yaml"
require "psych"
require "fileutils"

# Recursively walk a Psych AST node and collect duplicate mapping keys.
def collect_yaml_duplicate_keys(node, file, errors = [])
return errors unless node.respond_to?(:children) && node.children

if node.is_a?(Psych::Nodes::Mapping)
keys = node.children.each_slice(2).map { |k, _| k.value if k.respond_to?(:value) }.compact
keys.group_by(&:itself).each { |key, hits| errors << "#{file}: duplicate key '#{key}'" if hits.size > 1 }
end

node.children.each { |child| collect_yaml_duplicate_keys(child, file, errors) }
errors
end

desc "Validate YAML files for syntax errors and duplicate keys"
task :validate_yaml do
errors = []

Dir
.glob("{_config.yml,_data/**/*.{yml,yaml}}")
.sort
.each do |file|
node = Psych.parse_file(file)
collect_yaml_duplicate_keys(node, file, errors)
YAML.safe_load_file(file)
rescue Psych::SyntaxError => e
errors << "#{file}: syntax error — #{e.message}"
rescue Psych::DisallowedClass => e
errors << "#{file}: unsafe YAML — #{e.message}"
rescue => e
errors << "#{file}: #{e.message}"
end

if errors.any?
puts "❌ YAML validation errors:"
errors.each { |e| puts " - #{e}" }
abort
else
puts "✅ YAML files are valid"
end
end

desc "Run configuration checks"
task check: :validate_yaml do
required_files = OpenNews::RakeTasks.configuration.required_files
missing_files = required_files.reject { |f| File.exist?(f) }

if missing_files.any?
puts "❌ Missing required files: #{missing_files.join(", ")}"
exit 1
end

config = YAML.load_file("_config.yml")
errors = []
warnings = []

if config["deployment"]
deployment = config["deployment"]
warnings << "deployment.bucket not configured" unless deployment["bucket"]
warnings << "deployment.staging_bucket not configured" unless deployment["staging_bucket"]
warnings << "deployment.cloudfront_distribution_id not configured" unless deployment["cloudfront_distribution_id"]
else
warnings << "No deployment configuration found in _config.yml"
end

if errors.any?
puts "\n❌ Configuration Errors:"
errors.each { |e| puts " - #{e}" }
exit 1
end

if warnings.any?
puts "\n⚠️ Configuration Warnings:"
warnings.each { |w| puts " - #{w}" }
end

puts "✅ Configuration checks passed!"
end

desc "Build the Jekyll site"
task build: :validate_yaml do
options = { "source" => ".", "destination" => "./_site", "config" => "_config.yml", "quiet" => true }
begin
Jekyll::Site.new(Jekyll.configuration(options)).process
puts "✅ Build complete!"
rescue => e
abort "❌ Jekyll build failed: #{e.message}"
end
end

desc "Serve the Jekyll site locally"
task :serve do
puts "🚀 Starting local Jekyll server..."
sh "bundle exec jekyll serve --livereload"
end

desc "Clean build artifacts"
task :clean do
puts "🧹 Cleaning build artifacts..."
FileUtils.rm_rf(%w[_site .jekyll-cache .sass-cache .jekyll-metadata])
puts "✅ Clean complete!"
end
65 changes: 65 additions & 0 deletions lib/opennews/rake_tasks/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module OpenNews
module RakeTasks
class Configuration
# Files to ignore during html-proofer internal link checks.
# Consumer repos may append repo-specific patterns:
# OpenNews::RakeTasks.configure { |c| c.html_proofer_ignore_files << %r{blog/} }
attr_accessor :html_proofer_ignore_files

# URLs to ignore during html-proofer internal link checks.
attr_accessor :html_proofer_ignore_urls

# Files to ignore during review:external_links checks.
attr_accessor :external_links_ignore_files

# URLs to ignore during review:external_links checks.
# The defaults below cover common infrastructure/tracking hosts that
# are frequently blocked by bot-protection or unreachable from CI.
attr_accessor :external_links_ignore_urls

# Required files checked by the `check` task.
# Override in consumer repos if package.json is not present.
attr_accessor :required_files

def initialize
@html_proofer_ignore_files = []
@html_proofer_ignore_urls = [
"http://localhost",
"http://127.0.0.1",
/mitrakalita\.com/,
]

@external_links_ignore_files = []
@external_links_ignore_urls = [
"http://localhost",
"http://127.0.0.1",
"https://use.typekit.net",
/mitrakalita\.com/,
/flickr\.com/,
/medium\.com/,
/nytimes\.com/,
/eventbrite\.com/,
/archive\.org/,
]

@required_files = %w[_config.yml Gemfile package.json]
end
end

class << self
def configuration
@configuration ||= Configuration.new
end

# Yields the configuration object so consumer repos can adjust defaults:
#
# OpenNews::RakeTasks.configure do |config|
# config.html_proofer_ignore_urls += [/example\.com/]
# config.required_files -= ["package.json"]
# end
def configure
yield(configuration)
end
end
end
end
35 changes: 35 additions & 0 deletions lib/opennews/rake_tasks/format.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace :format do
desc "Run StandardRB linter to check Ruby code style"
task :ruby do
puts "Checking Ruby code style with StandardRB..."
sh "bundle exec standardrb"
end

desc "Auto-fix Ruby code formatting issues with StandardRB"
task :ruby_fix do
puts "Auto-fixing Ruby code formatting with StandardRB..."
sh "bundle exec standardrb --fix"
end

desc "Check non-Ruby files with Prettier (HTML, CSS, JS, YAML, Markdown)"
task :prettier do
puts "Checking file formatting with Prettier..."
sh "npm run format:check"
end

desc "Auto-fix non-Ruby files with Prettier"
task :prettier_fix do
puts "Auto-fixing file formatting with Prettier..."
sh "npm run format"
end
end

desc "Check all code formatting (Ruby + other files)"
task lint: %w[format:ruby format:prettier] do
puts "✅ All formatting checks passed!"
end

desc "Auto-fix all code formatting issues (Ruby + other files)"
task format: %w[format:ruby_fix format:prettier_fix] do
puts "✅ All files formatted!"
end
Loading
Loading