Move bin workflows into Bake tasks

This commit is contained in:
Sami Samhuri 2026-02-07 16:58:40 -08:00
parent 5ef44a5ccb
commit d4e9b53822
No known key found for this signature in database
8 changed files with 276 additions and 324 deletions

View file

@ -7,8 +7,7 @@ Source code for [samhuri.net](https://samhuri.net), powered by a Ruby static sit
This repository is now a single integrated Ruby project. The legacy Swift generators (`gensite/` and `samhuri.net/`) have been removed. This repository is now a single integrated Ruby project. The legacy Swift generators (`gensite/` and `samhuri.net/`) have been removed.
- Generator core: `lib/` - Generator core: `lib/`
- Build tasks: `bake.rb` - Build tasks and utility workflows: `bake.rb`
- CLI and utilities: `bin/`
- Tests: `spec/` - Tests: `spec/`
- Config: `site.toml` and `projects.toml` - Config: `site.toml` and `projects.toml`
- Content: `posts/` and `public/` - Content: `posts/` and `public/`
@ -22,12 +21,6 @@ This repository is now a single integrated Ruby project. The legacy Swift genera
## Setup ## Setup
```bash
bin/bootstrap
```
Or manually:
```bash ```bash
rbenv install -s "$(cat .ruby-version)" rbenv install -s "$(cat .ruby-version)"
rbenv exec bundle install rbenv exec bundle install
@ -55,6 +48,9 @@ Other targets:
rbenv exec bundle exec bake mudge rbenv exec bundle exec bake mudge
rbenv exec bundle exec bake beta rbenv exec bundle exec bake beta
rbenv exec bundle exec bake release rbenv exec bundle exec bake release
rbenv exec bundle exec bake generate . www https://samhuri.net
rbenv exec bundle exec bake watch target=debug
rbenv exec bundle exec bake deploy --test true --delete true
rbenv exec bundle exec bake publish_beta rbenv exec bundle exec bake publish_beta
rbenv exec bundle exec bake publish rbenv exec bundle exec bake publish
``` ```
@ -62,8 +58,8 @@ rbenv exec bundle exec bake publish
## Draft Workflow ## Draft Workflow
```bash ```bash
bin/new-draft "Post title" rbenv exec bundle exec bake new_draft "Post title"
bin/publish-draft public/drafts/post-title.md rbenv exec bundle exec bake publish_draft public/drafts/post-title.md
``` ```
## Tests And Lint ## Tests And Lint
@ -80,15 +76,15 @@ rbenv exec bundle exec bake test
rbenv exec bundle exec bake lint rbenv exec bundle exec bake lint
``` ```
## Site Generation CLI ## Site Generation
```bash ```bash
bin/pressa SOURCE TARGET [URL] rbenv exec bundle exec bake generate SOURCE TARGET [URL]
# example # example:
bin/pressa . www https://samhuri.net rbenv exec bundle exec bake generate . www https://samhuri.net
``` ```
## Notes ## Notes
- `bin/watch` is Linux-only and requires `inotifywait`. - `bake watch` is Linux-only and requires `inotifywait`.
- Deployment uses `rsync` to the configured `mudge` host paths in `bake.rb` and `bin/publish`. - Deployment uses `rsync` to the configured `mudge` host paths in `bake.rb`.

278
bake.rb
View file

@ -1,5 +1,16 @@
# Build tasks for samhuri.net static site generator # Build tasks for samhuri.net static site generator
require 'etc'
require 'fileutils'
require 'shellwords'
DRAFTS_DIR = 'public/drafts'.freeze
PUBLISH_HOST = 'mudge'.freeze
PRODUCTION_PUBLISH_DIR = '/var/www/samhuri.net/public'.freeze
BETA_PUBLISH_DIR = '/var/www/beta.samhuri.net/public'.freeze
WATCHABLE_DIRECTORIES = %w[public posts lib].freeze
BUILD_TARGETS = %w[debug mudge beta release].freeze
# Generate the site in debug mode (localhost:8000) # Generate the site in debug mode (localhost:8000)
def debug def debug
build('http://localhost:8000') build('http://localhost:8000')
@ -25,29 +36,158 @@ def serve
require 'webrick' require 'webrick'
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www') server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www')
trap('INT') { server.shutdown } trap('INT') { server.shutdown }
puts "Server running at http://localhost:8000" puts 'Server running at http://localhost:8000'
server.start server.start
end end
# Generate a site from an arbitrary source directory into a target directory.
# @parameter source_path [String] Directory containing site sources.
# @parameter target_path [String] Directory to write generated site.
# @parameter url [String] Optional site URL override.
def generate(source_path = '.', target_path = 'www', url = nil)
require_relative 'lib/pressa'
site = Pressa.create_site(source_path:, url_override: url)
generator = Pressa::SiteGenerator.new(site:)
generator.generate(source_path:, target_path:)
puts "Site built successfully in #{target_path}"
end
# Install local prerequisites and gem dependencies.
def setup
ruby_version = File.read('.ruby-version').strip
if RUBY_PLATFORM.include?('linux')
puts '*** installing Linux prerequisites'
unless system('sudo', 'apt', 'install', '-y',
'build-essential',
'git',
'inotify-tools',
'libffi-dev',
'libyaml-dev',
'pkg-config',
'zlib1g-dev')
abort 'Error: failed to install Linux prerequisites.'
end
end
if command_available?('rbenv')
puts "*** using rbenv (ruby #{ruby_version})"
abort 'Error: rbenv install failed.' unless system('rbenv', 'install', '-s', ruby_version)
abort 'Error: bundle install failed.' unless system('rbenv', 'exec', 'bundle', 'install')
else
puts '*** rbenv not found, using system Ruby'
abort 'Error: bundle install failed.' unless system('bundle', 'install')
end
puts '*** done'
end
# Create a new draft in public/drafts/.
# @parameter title_parts [Array] Optional title words; defaults to Untitled.
def new_draft(*title_parts)
title, filename =
if title_parts.empty?
['Untitled', next_available_draft]
else
given_title = title_parts.join(' ')
slug = slugify(given_title)
abort 'Error: title cannot be converted to a filename.' if slug.empty?
filename = "#{slug}.md"
path = draft_path(filename)
abort "Error: draft already exists at #{path}" if File.exist?(path)
[given_title, filename]
end
FileUtils.mkdir_p(DRAFTS_DIR)
path = draft_path(filename)
content = render_draft_template(title)
File.write(path, content)
puts "Created new draft at #{path}"
puts '>>> Contents below <<<'
puts
puts content
end
# Publish a draft by moving it to posts/YYYY/MM and updating dates.
# @parameter input_path [String] Draft path or filename in public/drafts.
def publish_draft(input_path = nil)
if input_path.nil? || input_path.strip.empty?
puts 'Usage: bake publish_draft <draft-path-or-filename>'
puts
puts 'Available drafts:'
drafts = Dir.glob("#{DRAFTS_DIR}/*.md").map { |path| File.basename(path) }
if drafts.empty?
puts ' (no drafts found)'
else
drafts.each { |draft| puts " #{draft}" }
end
abort
end
draft_path_value, draft_file = resolve_draft_input(input_path)
abort "Error: File not found: #{draft_path_value}" unless File.exist?(draft_path_value)
now = Time.now
content = File.read(draft_path_value)
content.sub!(/^Date:.*$/, "Date: #{ordinal_date(now)}")
content.sub!(/^Timestamp:.*$/, "Timestamp: #{now.strftime('%Y-%m-%dT%H:%M:%S%:z')}")
target_dir = "posts/#{now.strftime('%Y/%m')}"
FileUtils.mkdir_p(target_dir)
target_path = "#{target_dir}/#{draft_file}"
File.write(target_path, content)
FileUtils.rm_f(draft_path_value)
puts "Published draft: #{draft_path_value} -> #{target_path}"
end
# Watch content directories and rebuild on every change.
# @parameter target [String] One of debug, mudge, beta, or release.
def watch(target: 'mudge')
unless command_available?('inotifywait')
abort 'inotifywait is required (install inotify-tools).'
end
loop do
abort 'Error: watch failed.' unless system('inotifywait', '-e', 'modify,create,delete,move', *watch_paths)
puts "changed at #{Time.now}"
sleep 2
run_build_target(target)
end
end
# Deploy files via rsync without building first.
# @parameter beta [Boolean] Deploy to beta host path.
# @parameter test [Boolean] Enable rsync --dry-run.
# @parameter delete [Boolean] Enable rsync --delete.
# @parameter paths [Array] Optional local paths; defaults to www/.
def deploy(*paths, beta: false, test: false, delete: false)
publish_dir = truthy?(beta) ? BETA_PUBLISH_DIR : PRODUCTION_PUBLISH_DIR
local_paths = paths.empty? ? ['www/'] : paths
run_rsync(local_paths:, publish_dir:, dry_run: test, delete:)
end
# Publish to beta/staging server # Publish to beta/staging server
def publish_beta def publish_beta
beta beta
puts "Deploying to beta server..." run_rsync(local_paths: ['www/'], publish_dir: BETA_PUBLISH_DIR, dry_run: false, delete: true)
system('rsync -avz --delete www/ mudge:/var/www/beta.samhuri.net/public')
end end
# Publish to production server # Publish to production server
def publish def publish
release release
puts "Deploying to production server..." run_rsync(local_paths: ['www/'], publish_dir: PRODUCTION_PUBLISH_DIR, dry_run: false, delete: true)
system('rsync -avz --delete www/ mudge:/var/www/samhuri.net/public')
end end
# Clean generated files # Clean generated files
def clean def clean
require 'fileutils'
FileUtils.rm_rf('www') FileUtils.rm_rf('www')
puts "Cleaned www/ directory" puts 'Cleaned www/ directory'
end end
# Run RSpec tests # Run RSpec tests
@ -62,7 +202,7 @@ end
# List all available drafts # List all available drafts
def drafts def drafts
Dir.glob('public/drafts/*.md').sort.each do |draft| Dir.glob("#{DRAFTS_DIR}/*.md").sort.each do |draft|
puts File.basename(draft) puts File.basename(draft)
end end
end end
@ -82,11 +222,121 @@ private
# Build the site with specified URL # Build the site with specified URL
# @parameter url [String] The site URL to use # @parameter url [String] The site URL to use
def build(url) def build(url)
require_relative 'lib/pressa'
puts "Building site for #{url}..." puts "Building site for #{url}..."
site = Pressa.create_site(source_path: '.', url_override: url) generate('.', 'www', url)
generator = Pressa::SiteGenerator.new(site:) end
generator.generate(source_path: '.', target_path: 'www')
puts "Site built successfully in www/" def run_rsync(local_paths:, publish_dir:, dry_run:, delete:)
command = ['rsync', '-aKv', '-e', 'ssh -4']
command << '--dry-run' if truthy?(dry_run)
command << '--delete' if truthy?(delete)
command.concat(local_paths)
command << "#{PUBLISH_HOST}:#{publish_dir}"
puts "Running: #{Shellwords.join(command)}"
abort 'Error: rsync failed.' unless system(*command)
end
def run_build_target(target)
target_name = target.to_s
unless BUILD_TARGETS.include?(target_name)
abort "Error: invalid target '#{target_name}'. Use one of: #{BUILD_TARGETS.join(', ')}"
end
public_send(target_name)
end
def watch_paths
WATCHABLE_DIRECTORIES.flat_map { |path| ['-r', path] }
end
def resolve_draft_input(input_path)
if input_path.include?('/')
if input_path.start_with?('posts/')
abort "Error: '#{input_path}' is already published in posts/ directory"
end
[input_path, File.basename(input_path)]
else
[draft_path(input_path), input_path]
end
end
def draft_path(filename)
File.join(DRAFTS_DIR, filename)
end
def slugify(title)
title.downcase
.gsub(/[^a-z0-9\s-]/, '')
.gsub(/\s+/, '-')
.gsub(/-+/, '-')
.gsub(/^-|-$/, '')
end
def next_available_draft(base_filename = 'untitled.md')
return base_filename unless File.exist?(draft_path(base_filename))
name_without_ext = File.basename(base_filename, '.md')
counter = 1
loop do
numbered_filename = "#{name_without_ext}-#{counter}.md"
return numbered_filename unless File.exist?(draft_path(numbered_filename))
counter += 1
end
end
def render_draft_template(title)
now = Time.now
<<~FRONTMATTER
---
Author: #{current_author}
Title: #{title}
Date: unpublished
Timestamp: #{now.strftime('%Y-%m-%dT%H:%M:%S%:z')}
Tags:
---
# #{title}
TKTK
FRONTMATTER
end
def current_author
Etc.getlogin || ENV['USER'] || `whoami`.strip
rescue StandardError
ENV['USER'] || `whoami`.strip
end
def ordinal_date(time)
day = time.day
suffix = case day
when 1, 21, 31
'st'
when 2, 22
'nd'
when 3, 23
'rd'
else
'th'
end
time.strftime("#{day}#{suffix} %B, %Y")
end
def command_available?(command)
system('which', command, out: File::NULL, err: File::NULL)
end
def truthy?(value)
case value
when true
true
when false, nil
false
else
%w[1 true yes on].include?(value.to_s.downcase)
end
end end

View file

@ -1,31 +0,0 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
RUBY_VERSION="$(cat "$ROOT_DIR/.ruby-version")"
if [[ "$(uname)" = "Linux" ]]; then
echo "*** installing Linux prerequisites"
sudo apt install -y \
build-essential \
git \
inotify-tools \
libffi-dev \
libyaml-dev \
pkg-config \
zlib1g-dev
fi
cd "$ROOT_DIR"
if command -v rbenv >/dev/null 2>/dev/null; then
echo "*** using rbenv (ruby $RUBY_VERSION)"
rbenv install -s "$RUBY_VERSION"
rbenv exec bundle install
else
echo "*** rbenv not found, using system Ruby"
bundle install
fi
echo "*** done"

View file

@ -1,94 +0,0 @@
#!/usr/bin/env ruby -w
require 'fileutils'
DRAFTS_DIR = File.expand_path("../public/drafts", __dir__).freeze
def usage
puts "Usage: #{$0} [title]"
puts
puts "Examples:"
puts " #{$0} Top 5 Ways to Write Clickbait # using a title without quotes"
puts " #{$0} 'Something with punctuation?!' # fancy chars need quotes"
puts " #{$0} working-with-databases # using a slug"
puts " #{$0} # Creates untitled.md (or untitled-2.md, etc.)"
puts
puts "Creates a new draft in public/drafts/ directory with proper frontmatter."
end
def draft_path(filename)
File.join(DRAFTS_DIR, filename)
end
def main
if ARGV.include?('-h') || ARGV.include?('--help')
usage
exit 0
end
title, filename =
if ARGV.empty?
['Untitled', next_available_draft]
else
given_title = ARGV.join(' ')
filename = "#{slugify(given_title)}.md"
path = draft_path(filename)
if File.exist?(path)
puts "Error: draft already exists at #{path}"
exit 1
end
[given_title, filename]
end
FileUtils.mkdir_p(DRAFTS_DIR)
path = draft_path(filename)
content = render_template(title)
File.write(path, content)
puts "Created new draft at #{path}"
puts '>>> Contents below <<<'
puts
puts content
end
def slugify(title)
title.downcase
.gsub(/[^a-z0-9\s-]/, '')
.gsub(/\s+/, '-')
.gsub(/-+/, '-')
.gsub(/^-|-$/, '')
end
def next_available_draft(base_filename = 'untitled.md')
return base_filename unless File.exist?(draft_path(base_filename))
name_without_ext = File.basename(base_filename, '.md')
counter = 1
loop do
numbered_filename = "#{name_without_ext}-#{counter}.md"
return numbered_filename unless File.exist?(draft_path(numbered_filename))
counter += 1
end
end
def render_template(title)
now = Time.now
iso_timestamp = now.strftime('%Y-%m-%dT%H:%M:%S%:z')
<<~FRONTMATTER
---
Author: #{`whoami`.strip}
Title: #{title}
Date: unpublished
Timestamp: #{iso_timestamp}
Tags:
---
# #{title}
TKTK
FRONTMATTER
end
main if $0 == __FILE__

View file

@ -1,28 +0,0 @@
#!/usr/bin/env ruby
require_relative '../lib/pressa'
if ARGV.length < 2
puts "Usage: pressa SOURCE TARGET [URL]"
puts ""
puts "Arguments:"
puts " SOURCE Directory containing posts/ and public/"
puts " TARGET Directory to write generated site"
puts " URL Optional site URL override"
exit 1
end
source_path = ARGV[0]
target_path = ARGV[1]
site_url = ARGV[2]
begin
site = Pressa.create_site(source_path:, url_override: site_url)
generator = Pressa::SiteGenerator.new(site:)
generator.generate(source_path:, target_path:)
puts "Site generated successfully!"
rescue => e
puts "Error: #{e.message}"
puts e.backtrace
exit 1
end

View file

@ -1,54 +0,0 @@
#!/bin/bash
# exit on errors
set -e
PUBLISH_HOST="mudge"
PUBLISH_DIR="/var/www/samhuri.net/public"
ECHO=0
RSYNC_OPTS=""
BREAK_WHILE=0
while [[ $# > 0 ]]; do
ARG="$1"
case "$ARG" in
-b|--beta)
PUBLISH_DIR="/var/www/beta.samhuri.net/public"
shift
;;
-t|--test)
ECHO=1
RSYNC_OPTS="$RSYNC_OPTS --dry-run"
shift
;;
-d|--delete)
RSYNC_OPTS="$RSYNC_OPTS --delete"
shift
;;
# we're at the paths, no more options
*)
BREAK_WHILE=1
break
;;
esac
[[ $BREAK_WHILE -eq 1 ]] && break
done
declare -a CMD
if [[ $# -eq 0 ]]; then
CMD=(rsync -aKv -e "ssh -4" $RSYNC_OPTS www/ $PUBLISH_HOST:$PUBLISH_DIR)
else
CMD=(rsync -aKv -e "ssh -4" $RSYNC_OPTS $@ $PUBLISH_HOST:$PUBLISH_DIR)
fi
if [[ $ECHO -eq 1 ]]; then
echo "${CMD[@]}"
fi
"${CMD[@]}"

View file

@ -1,70 +0,0 @@
#!/usr/bin/env ruby -w
require 'fileutils'
def usage
puts "Usage: #{$0} <draft-path-or-filename>"
puts
puts "Examples:"
puts " #{$0} public/drafts/reverse-engineering-photo-urls.md"
puts
puts "Available drafts:"
drafts = Dir.glob('public/drafts/*.md').map { |f| File.basename(f) }
if drafts.empty?
puts " (no drafts found)"
else
drafts.each { |d| puts " #{d}" }
end
end
if ARGV.empty?
usage
abort
end
input_path = ARGV.first
# Handle both full paths and just filenames
if input_path.include?('/')
draft_path = input_path
draft_file = File.basename(input_path)
if input_path.start_with?('posts/')
abort "Error: '#{input_path}' is already published in posts/ directory"
end
else
draft_file = input_path
draft_path = "public/drafts/#{draft_file}"
end
abort "Error: File not found: #{draft_path}" unless File.exist?(draft_path)
# Update display date timestamp to current time
def ordinal_date(time)
day = time.day
suffix = case day
when 1, 21, 31 then 'st'
when 2, 22 then 'nd'
when 3, 23 then 'rd'
else 'th'
end
time.strftime("#{day}#{suffix} %B, %Y")
end
now = Time.now
iso_timestamp = now.strftime('%Y-%m-%dT%H:%M:%S%:z')
human_date = ordinal_date(now)
content = File.read(draft_path)
content.sub!(/^Date:.*$/, "Date: #{human_date}")
content.sub!(/^Timestamp:.*$/, "Timestamp: #{iso_timestamp}")
# Use current year/month for directory, pad with strftime
year_month = now.strftime('%Y-%m')
year, month = year_month.split('-')
target_dir = "posts/#{year}/#{month}"
FileUtils.mkdir_p(target_dir)
target_path = "#{target_dir}/#{draft_file}"
File.write(target_path, content)
FileUtils.rm_f(draft_path)
puts "Published draft: #{draft_path} → #{target_path}"

View file

@ -1,17 +0,0 @@
#!/bin/bash
set -euo pipefail
BLOG_TARGET=${BLOG_TARGET:-mudge}
if ! command -v inotifywait >/dev/null 2>/dev/null; then
echo "inotifywait is required (install inotify-tools)."
exit 1
fi
while true; do
inotifywait -e modify,create,delete,move -r public -r posts -r lib
echo "changed at $(date)"
sleep 2
rbenv exec bundle exec bake "$BLOG_TARGET"
done