Added git-ready CI scaffold to init and clone

Necessary so a new plugin is immediately ready to push to GitHub and
deploy on every commit to main. Init and clone now drop a CI workflow
and .gitignore, and run `git init -b main` so the scaffold lands on
the branch the workflow expects. The Docker image now includes git
for the `docker run trmnl/trmnlp clone` flow. View templates ship
canonical layout + title_bar markup. Use --skip-git to opt out.
This commit is contained in:
Ikraam Ghoor
2026-05-20 11:50:50 +01:00
parent 2a60aaf9c3
commit cd79062d4d
18 changed files with 173 additions and 20 deletions
+7
View File
@@ -1,6 +1,13 @@
# Changelog
## 0.8.3
- `trmnlp init` and `trmnlp clone` now scaffold a `.github/workflows/trmnl.yml` CI workflow and a `.gitignore`, and run `git init -b main`, so a cloned plugin is ready to push to GitHub and deploy on every commit to `main`
- Added `--skip-git` to `trmnlp init` and `trmnlp clone` for projects that manage Git themselves
- The Docker image now ships `git` so the `docker run trmnl/trmnlp clone` flow leaves a ready-to-push project on the host
- View templates now ship canonical `layout` + `title_bar` markup that passes `trmnlp lint`
## 0.8.2
- Fixed `framework_version: latest` rendering against the auto-upgrading `/latest/` asset path instead of the current concrete release, matching the hosted service (#99)
+1
View File
@@ -37,6 +37,7 @@ FROM ruby:${RUBY_VERSION}-slim-trixie AS runner
# trixie ships IM7 (the `magick` binary trmnlp's PNG quantizer needs).
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
imagemagick \
firefox-esr \
python3 \
+1 -1
View File
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
trmnl_preview (0.8.2)
trmnl_preview (0.8.3)
activesupport (~> 8.0)
cgi (~> 0.5)
faraday (~> 2.1)
+12 -3
View File
@@ -28,6 +28,10 @@ This is the structure of a plugin project:
```
.
├── .github
│ └── workflows
│ └── trmnl.yml
├── .gitignore
├── .trmnlp.yml
├── bin
│ └── trmnlp
@@ -42,6 +46,8 @@ This is the structure of a plugin project:
| File | Purpose |
|---|---|
| `.github/workflows/trmnl.yml` | GitHub Actions workflow — lints every PR, deploys to TRMNL on `main` |
| `.gitignore` | Keeps `trmnlp build` output out of version control |
| `.trmnlp.yml` | Local dev-server config — not uploaded to TRMNL |
| `src/full.liquid` | Markup for the full screen |
| `src/half_horizontal.liquid` | Top or bottom half of a stacked mashup |
@@ -123,9 +129,12 @@ If an environment variable is more convenient (for example in a CI/CD pipeline),
## Continuous Integration
`trmnlp` runs in GitHub Actions without `trmnlp login` — set the `TRMNL_API_KEY`
environment variable and it's used in place of the saved config. Add it as a
repository secret, then drop this into `.github/workflows/trmnl.yml`:
`trmnlp init` and `trmnlp clone` scaffold a `.github/workflows/trmnl.yml`
workflow and initialize a Git repository, so a fresh project is ready to push
to GitHub. The workflow runs in GitHub Actions without `trmnlp login` — set the
`TRMNL_API_KEY` environment variable and it's used in place of the saved
config. Add it as a repository secret to activate the workflow; it looks like
this:
```yaml
name: TRMNL
+2
View File
@@ -34,11 +34,13 @@ module TRMNLP
desc 'init NAME', 'Start a new plugin project'
method_option :skip_liquid, type: :boolean, default: false, desc: 'Skip generating liquid templates'
method_option :skip_git, type: :boolean, default: false, desc: 'Skip initializing a git repository'
def init(name)
Commands::Init.run(options, name)
end
desc 'clone NAME ID', 'Copy a plugin project from TRMNL server'
method_option :skip_git, type: :boolean, default: false, desc: 'Skip initializing a git repository'
def clone(name, id)
Commands::Clone.run(options, name, id)
end
+2 -2
View File
@@ -7,7 +7,7 @@ require_relative 'pull'
module TRMNLP
module Commands
class Clone < Base
Options = Data.define(:dir, :quiet)
Options = Data.define(:dir, :quiet, :skip_git)
def call(directory_name, id)
authenticate!
@@ -15,7 +15,7 @@ module TRMNLP
destination_path = Pathname.new(options.dir).join(directory_name)
raise DirectoryExists, "directory #{destination_path} already exists, aborting" if destination_path.exist?
Init.run({ dir: options.dir, skip_liquid: true, quiet: true }, directory_name)
Init.run({ dir: options.dir, skip_liquid: true, quiet: true, skip_git: options.skip_git }, directory_name)
Pull.run({ dir: destination_path.to_s, force: true, id: id })
+13 -2
View File
@@ -7,7 +7,7 @@ require_relative 'base'
module TRMNLP
module Commands
class Init < Base
Options = Data.define(:dir, :quiet, :skip_liquid)
Options = Data.define(:dir, :quiet, :skip_liquid, :skip_git)
def call(name)
destination_dir = Pathname.new(options.dir).join(name)
@@ -17,7 +17,9 @@ module TRMNLP
destination_dir.mkpath
end
template_dir.glob('**/{*,.*}').each do |source_pathname|
# NOTE: FNM_DOTMATCH so the glob descends into hidden template
# directories (e.g. .github/); without it those files are skipped.
template_dir.glob('**/{*,.*}', File::FNM_DOTMATCH).each do |source_pathname|
next if source_pathname.directory?
next if options.skip_liquid && source_pathname.extname == '.liquid'
@@ -41,6 +43,8 @@ module TRMNLP
destination_pathname.chmod(destination_pathname.stat.mode | 0o200)
end
init_git_repo(destination_dir) unless options.skip_git
reporter.info <<~HEREDOC
To start the local server:
@@ -58,6 +62,13 @@ module TRMNLP
private
def template_dir = paths.templates_dir.join('init')
# Make the scaffold a Git repository on `main` so it's ready to push
# to GitHub (the workflow's `branches: [main]` trigger requires it,
# regardless of the host's init.defaultBranch).
# Does nothing when git is unavailable — `system` returns nil rather
# than raising, so the scaffold itself still succeeds.
def init_git_repo(dir) = system('git', 'init', '-q', '-b', 'main', dir.to_s)
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module TRMNLP
VERSION = '0.8.2'
VERSION = '0.8.3'
end
+15
View File
@@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'trmnl_preview.gemspec' do
subject(:gemspec) { Gem::Specification.load(gemspec_path) }
let(:gemspec_path) { File.expand_path('../trmnl_preview.gemspec', __dir__) }
# The .github/ template directory is hidden; a plain Dir[] glob skips it and
# would silently drop the scaffolded workflow from the published gem.
it 'packages the GitHub Actions workflow template' do
expect(gemspec.files).to include('templates/init/.github/workflows/trmnl.yml')
end
end
+14 -3
View File
@@ -5,7 +5,9 @@ require 'tmpdir'
require 'trmnlp/commands/clone'
RSpec.describe TRMNLP::Commands::Clone do
subject(:command) { described_class.new(context:, options: described_class::Options.new(dir: tmp_root, quiet: true)) }
subject(:command) do
described_class.new(context:, options: described_class::Options.new(dir: tmp_root, quiet: true, skip_git: false))
end
let(:tmp_root) { Dir.mktmpdir('trmnlp-clone-') }
let(:context) { TRMNLP::Context.new(tmp_root) }
@@ -23,8 +25,8 @@ RSpec.describe TRMNLP::Commands::Clone do
it 'scaffolds the project via Init' do
command.call('my-plugin', '42')
expect(TRMNLP::Commands::Init).to have_received(:run).with({ dir: tmp_root, skip_liquid: true, quiet: true },
'my-plugin')
expect(TRMNLP::Commands::Init).to have_received(:run)
.with({ dir: tmp_root, skip_liquid: true, quiet: true, skip_git: false }, 'my-plugin')
end
it 'pulls the plugin settings into the new directory' do
@@ -46,5 +48,14 @@ RSpec.describe TRMNLP::Commands::Clone do
expect { command.call('my-plugin', '42') }.to raise_error(TRMNLP::NotLoggedIn)
end
it 'forwards skip_git to Init' do
cmd = described_class.new(context:,
options: described_class::Options.new(dir: tmp_root, quiet: true, skip_git: true))
cmd.call('skipped', '99')
expect(TRMNLP::Commands::Init).to have_received(:run)
.with({ dir: tmp_root, skip_liquid: true, quiet: true, skip_git: true }, 'skipped')
end
end
end
+40 -2
View File
@@ -6,7 +6,9 @@ require 'trmnlp/commands/init'
RSpec.describe TRMNLP::Commands::Init do
subject(:command) do
described_class.new(context:, options: described_class::Options.new(dir: tmp_root, quiet: true, skip_liquid: false))
described_class.new(context:,
options: described_class::Options.new(dir: tmp_root, quiet: true,
skip_liquid: false, skip_git: false))
end
let(:tmp_root) { Dir.mktmpdir('trmnlp-init-') }
@@ -28,7 +30,8 @@ RSpec.describe TRMNLP::Commands::Init do
it 'omits liquid files when skip_liquid is true' do
cmd = described_class.new(context:,
options: described_class::Options.new(dir: tmp_root,
quiet: true, skip_liquid: true))
quiet: true, skip_liquid: true,
skip_git: false))
cmd.call('no-liquid')
project = File.join(tmp_root, 'no-liquid')
@@ -36,6 +39,41 @@ RSpec.describe TRMNLP::Commands::Init do
expect(File).not_to exist(File.join(project, 'src', 'full.liquid'))
end
it 'scaffolds the GitHub Actions workflow' do
command.call('demo')
expect(File).to exist(File.join(tmp_root, 'demo', '.github', 'workflows', 'trmnl.yml'))
end
it 'scaffolds a gitignore' do
command.call('demo')
expect(File).to exist(File.join(tmp_root, 'demo', '.gitignore'))
end
it 'initializes a git repository' do
command.call('demo')
expect(File).to be_directory(File.join(tmp_root, 'demo', '.git'))
end
it 'initializes the git repository on the main branch' do
command.call('demo')
head = File.read(File.join(tmp_root, 'demo', '.git', 'HEAD')).strip
expect(head).to eq('ref: refs/heads/main')
end
it 'skips git init when skip_git is true' do
cmd = described_class.new(context:,
options: described_class::Options.new(dir: tmp_root,
quiet: true, skip_liquid: false,
skip_git: true))
cmd.call('no-git')
expect(File).not_to be_directory(File.join(tmp_root, 'no-git', '.git'))
end
context 'when the template source is read-only (#83)' do
let(:templates_dir) { Pathname.new(Dir.mktmpdir('trmnlp-templates-')) }
let(:read_only_file) { templates_dir.join('init', 'src', 'settings.yml') }
+30
View File
@@ -0,0 +1,30 @@
name: TRMNL
on:
pull_request:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
- run: gem install trmnl_preview
- run: trmnlp lint
push:
needs: lint
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
- run: gem install trmnl_preview
- run: trmnlp push --force
env:
TRMNL_API_KEY: ${{ secrets.TRMNL_API_KEY }}
+2
View File
@@ -0,0 +1,2 @@
# trmnlp build output
_build/
+7 -1
View File
@@ -1 +1,7 @@
full!
<div class="layout layout--col layout--center">
<span class="title title--large">Hello, TRMNL!</span>
</div>
<div class="title_bar">
<span class="title">My Plugin</span>
</div>
+7 -1
View File
@@ -1 +1,7 @@
half horizontal!
<div class="layout layout--col layout--center">
<span class="title title--large">Half horizontal</span>
</div>
<div class="title_bar">
<span class="title">My Plugin</span>
</div>
+7 -1
View File
@@ -1 +1,7 @@
half vertical!
<div class="layout layout--col layout--center">
<span class="title title--large">Half vertical</span>
</div>
<div class="title_bar">
<span class="title">My Plugin</span>
</div>
+7 -1
View File
@@ -1 +1,7 @@
quadrant!
<div class="layout layout--col layout--center">
<span class="title title--large">Quadrant</span>
</div>
<div class="title_bar">
<span class="title">My Plugin</span>
</div>
+5 -2
View File
@@ -21,17 +21,20 @@ Gem::Specification.new do |spec|
spec.metadata['rubygems_mfa_required'] = 'true'
spec.files = Dir.chdir(__dir__) do
[
files = [
'bin/**/*',
'db/**/*',
'lib/**/*',
'templates/**/{*,.*}',
'web/**/*',
'CHANGELOG.md',
'LICENSE.txt',
'README.md',
'trmnl_preview.gemspec'
].flat_map { |glob| Dir[glob] }
# FNM_DOTMATCH so the glob descends into the templates' hidden directories
# (e.g. .github/) — a plain Dir[] skips them and drops the file from the gem.
files + Dir.glob('templates/**/{*,.*}', File::FNM_DOTMATCH)
end
spec.bindir = 'bin'
spec.executables = ['trmnlp']