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.
- Generator core: `lib/`
- Build tasks: `bake.rb`
- CLI and utilities: `bin/`
- Build tasks and utility workflows: `bake.rb`
- Tests: `spec/`
- Config: `site.toml` and `projects.toml`
- Content: `posts/` and `public/`
@ -22,12 +21,6 @@ This repository is now a single integrated Ruby project. The legacy Swift genera
## Setup
```bash
bin/bootstrap
```
Or manually:
```bash
rbenv install -s "$(cat .ruby-version)"
rbenv exec bundle install
@ -55,6 +48,9 @@ Other targets:
rbenv exec bundle exec bake mudge
rbenv exec bundle exec bake beta
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
```
@ -62,8 +58,8 @@ rbenv exec bundle exec bake publish
## Draft Workflow
```bash
bin/new-draft "Post title"
bin/publish-draft public/drafts/post-title.md
rbenv exec bundle exec bake new_draft "Post title"
rbenv exec bundle exec bake publish_draft public/drafts/post-title.md
```
## Tests And Lint
@ -80,15 +76,15 @@ rbenv exec bundle exec bake test
rbenv exec bundle exec bake lint
```
## Site Generation CLI
## Site Generation
```bash
bin/pressa SOURCE TARGET [URL]
# example
bin/pressa . www https://samhuri.net
rbenv exec bundle exec bake generate SOURCE TARGET [URL]
# example:
rbenv exec bundle exec bake generate . www https://samhuri.net
```
## Notes
- `bin/watch` is Linux-only and requires `inotifywait`.
- Deployment uses `rsync` to the configured `mudge` host paths in `bake.rb` and `bin/publish`.
- `bake watch` is Linux-only and requires `inotifywait`.
- 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
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)
def debug
build('http://localhost:8000')
@ -25,29 +36,158 @@ def serve
require 'webrick'
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www')
trap('INT') { server.shutdown }
puts "Server running at http://localhost:8000"
puts 'Server running at http://localhost:8000'
server.start
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
def publish_beta
beta
puts "Deploying to beta server..."
system('rsync -avz --delete www/ mudge:/var/www/beta.samhuri.net/public')
run_rsync(local_paths: ['www/'], publish_dir: BETA_PUBLISH_DIR, dry_run: false, delete: true)
end
# Publish to production server
def publish
release
puts "Deploying to production server..."
system('rsync -avz --delete www/ mudge:/var/www/samhuri.net/public')
run_rsync(local_paths: ['www/'], publish_dir: PRODUCTION_PUBLISH_DIR, dry_run: false, delete: true)
end
# Clean generated files
def clean
require 'fileutils'
FileUtils.rm_rf('www')
puts "Cleaned www/ directory"
puts 'Cleaned www/ directory'
end
# Run RSpec tests
@ -62,7 +202,7 @@ end
# List all available 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)
end
end
@ -82,11 +222,121 @@ private
# Build the site with specified URL
# @parameter url [String] The site URL to use
def build(url)
require_relative 'lib/pressa'
puts "Building site for #{url}..."
site = Pressa.create_site(source_path: '.', url_override: url)
generator = Pressa::SiteGenerator.new(site:)
generator.generate(source_path: '.', target_path: 'www')
puts "Site built successfully in www/"
generate('.', 'www', url)
end
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

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