From cd79062d4dfca920d658fb7cd8eebe4626076078 Mon Sep 17 00:00:00 2001 From: Ikraam Ghoor Date: Wed, 20 May 2026 11:50:50 +0100 Subject: [PATCH] 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. --- CHANGELOG.md | 7 ++++ Dockerfile | 1 + Gemfile.lock | 2 +- README.md | 15 ++++++-- lib/trmnlp/cli.rb | 2 ++ lib/trmnlp/commands/clone.rb | 4 +-- lib/trmnlp/commands/init.rb | 15 ++++++-- lib/trmnlp/version.rb | 2 +- spec/gemspec_spec.rb | 15 ++++++++ spec/lib/trmnlp/commands/clone_spec.rb | 17 +++++++-- spec/lib/trmnlp/commands/init_spec.rb | 42 ++++++++++++++++++++-- templates/init/.github/workflows/trmnl.yml | 30 ++++++++++++++++ templates/init/.gitignore | 2 ++ templates/init/src/full.liquid | 8 ++++- templates/init/src/half_horizontal.liquid | 8 ++++- templates/init/src/half_vertical.liquid | 8 ++++- templates/init/src/quadrant.liquid | 8 ++++- trmnl_preview.gemspec | 7 ++-- 18 files changed, 173 insertions(+), 20 deletions(-) create mode 100644 spec/gemspec_spec.rb create mode 100644 templates/init/.github/workflows/trmnl.yml create mode 100644 templates/init/.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 8de4e1a..9e55017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Dockerfile b/Dockerfile index a8dd569..2fdbf34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/Gemfile.lock b/Gemfile.lock index 67ee886..82226a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/README.md b/README.md index 5d4bf39..adb65aa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/trmnlp/cli.rb b/lib/trmnlp/cli.rb index 5e6b31c..30680dd 100644 --- a/lib/trmnlp/cli.rb +++ b/lib/trmnlp/cli.rb @@ -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 diff --git a/lib/trmnlp/commands/clone.rb b/lib/trmnlp/commands/clone.rb index 85e23a9..580966b 100644 --- a/lib/trmnlp/commands/clone.rb +++ b/lib/trmnlp/commands/clone.rb @@ -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 }) diff --git a/lib/trmnlp/commands/init.rb b/lib/trmnlp/commands/init.rb index c64c267..897ec06 100644 --- a/lib/trmnlp/commands/init.rb +++ b/lib/trmnlp/commands/init.rb @@ -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 diff --git a/lib/trmnlp/version.rb b/lib/trmnlp/version.rb index 111c850..98fe5e7 100644 --- a/lib/trmnlp/version.rb +++ b/lib/trmnlp/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module TRMNLP - VERSION = '0.8.2' + VERSION = '0.8.3' end diff --git a/spec/gemspec_spec.rb b/spec/gemspec_spec.rb new file mode 100644 index 0000000..1ca3326 --- /dev/null +++ b/spec/gemspec_spec.rb @@ -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 diff --git a/spec/lib/trmnlp/commands/clone_spec.rb b/spec/lib/trmnlp/commands/clone_spec.rb index 24fc5cf..7cd0424 100644 --- a/spec/lib/trmnlp/commands/clone_spec.rb +++ b/spec/lib/trmnlp/commands/clone_spec.rb @@ -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 diff --git a/spec/lib/trmnlp/commands/init_spec.rb b/spec/lib/trmnlp/commands/init_spec.rb index 0bf1a11..ad09828 100644 --- a/spec/lib/trmnlp/commands/init_spec.rb +++ b/spec/lib/trmnlp/commands/init_spec.rb @@ -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') } diff --git a/templates/init/.github/workflows/trmnl.yml b/templates/init/.github/workflows/trmnl.yml new file mode 100644 index 0000000..0c3ff93 --- /dev/null +++ b/templates/init/.github/workflows/trmnl.yml @@ -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 }} diff --git a/templates/init/.gitignore b/templates/init/.gitignore new file mode 100644 index 0000000..da1b0b7 --- /dev/null +++ b/templates/init/.gitignore @@ -0,0 +1,2 @@ +# trmnlp build output +_build/ diff --git a/templates/init/src/full.liquid b/templates/init/src/full.liquid index 52ad651..5fc3dc0 100644 --- a/templates/init/src/full.liquid +++ b/templates/init/src/full.liquid @@ -1 +1,7 @@ -full! +
+ Hello, TRMNL! +
+ +
+ My Plugin +
diff --git a/templates/init/src/half_horizontal.liquid b/templates/init/src/half_horizontal.liquid index 8a82545..7ab249e 100644 --- a/templates/init/src/half_horizontal.liquid +++ b/templates/init/src/half_horizontal.liquid @@ -1 +1,7 @@ -half horizontal! +
+ Half horizontal +
+ +
+ My Plugin +
diff --git a/templates/init/src/half_vertical.liquid b/templates/init/src/half_vertical.liquid index 4647baf..f645d69 100644 --- a/templates/init/src/half_vertical.liquid +++ b/templates/init/src/half_vertical.liquid @@ -1 +1,7 @@ -half vertical! +
+ Half vertical +
+ +
+ My Plugin +
diff --git a/templates/init/src/quadrant.liquid b/templates/init/src/quadrant.liquid index 3c88177..1ff816c 100644 --- a/templates/init/src/quadrant.liquid +++ b/templates/init/src/quadrant.liquid @@ -1 +1,7 @@ -quadrant! +
+ Quadrant +
+ +
+ My Plugin +
diff --git a/trmnl_preview.gemspec b/trmnl_preview.gemspec index 23bfe15..1fced4a 100644 --- a/trmnl_preview.gemspec +++ b/trmnl_preview.gemspec @@ -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']