mirror of
https://github.com/usetrmnl/trmnlp.git
synced 2026-08-13 14:27:33 -07:00
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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module TRMNLP
|
||||
VERSION = '0.8.2'
|
||||
VERSION = '0.8.3'
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
@@ -0,0 +1,2 @@
|
||||
# trmnlp build output
|
||||
_build/
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user