Adrian Marin

How to Package a Rails Engine Generation to Automation

wroc_love.rb 2022

00:00:17.520 Thank you for having me. I'm thrilled to be here! Let me just get a picture of all of us to post on Twitter.
00:00:23.039 Perfect! I just want to say that you are amazing people, and you're incredibly smart. I mean, Pavel's presentation yesterday and Nyx and Yaroslav, that was amazing!
00:00:29.599 You captured the essence of view components and Hotwire. I appreciate your efforts in clarifying the differences between Hotwire and React. It's important to know that we have many tools in our toolkit, and it's vital to use the right ones.
00:00:35.280 So, today I'm going to talk about how to package a Rails engine from generation to automation. It might seem like a trivial subject compared to your nice talks, but I believe it's important. My name is Adrian, and I'm the author of Avo. Avo is an application-building framework built on top of Ruby on Rails that helps developers create tools ten times faster. I've worked as a freelancer in digital agencies, corporations, and Silicon Valley startups, but now I'm focusing on being an entrepreneur. You can find me, Adrian the Dev, everywhere, including at agentdev.com. If you want to check out Avo, go to avo.com.
00:01:05.920 So what exactly is a Rails engine? A Rails engine is essentially a miniature Rails application within your host application. It holds its own routes, models, controllers, views, and more.
00:01:19.280 When would we use an engine? Let's take, for example, a marketplace app. We will always have buyers and sellers, and likely they will have different admin panels, back offices, and functionalities. How do we build that if we're not using engines?
00:01:32.879 We could create separate directories under controllers for buyers and sellers, and do the same in models. However, we can also generate new engines for buyers and sellers, which would act as separate applications, keeping all your logic organized.
00:02:06.960 This approach is especially effective when you want to build an engine for your own company or application. But perhaps you’re developing something that you want to share with the world, such as an admin panel framework.
00:02:18.800 So let's get started! We’ll generate an engine. I'll just write 'rails plugin new admin' and include the mountable option. This way, you will get all those goodies – the controllers, models, directories, and everything will be autoloaded for you. Additionally, you'll have a dummy app inside your test directory. This dummy app is essentially another Rails application where you can test your code through automated or manual testing.
00:03:14.000 So what are we going to build? We're going to create this admin panel framework that will automatically find all our models and display them in a sidebar. Each model will have a link to an index page where we'll see all our records, such as users, posts, comments, and so on. It’s crucial to test your engine and ensure all your code passes.
00:03:59.519 Now, how do we distribute this engine? The easiest way is through RubyGems. We accomplish this using the gem utility and a gemspec file. Every engine or gem you generate will include this specification.
00:04:11.840 Think of the gemspec as the package.json equivalent for your package. In the first section, you'll find information about the package—such as its name, author, version, description, and so on. Then, there’s some metadata that can be added to the gemspec. These links from Avo are used by RubyGems and other sites that scrape this gem to present your page nicely, showing the homepage, bug tracker, and other relevant links.
00:05:32.960 The 'files' option is also critical because, when you package your gem, you need to be mindful of what gets included. You might have dot files or your node_modules directory included, which you definitely don’t want. This is where you specify the files to be added to your gem.
00:06:01.759 Lastly, let’s discuss dependencies. If you're using Pundit in your runtime, you can manage that dependency in two ways. You could build your gem without specifying Pundit as a dependency, leaving it up to the user to read your documentation and install it—an easy path to broken functionality and poor reviews. Or, you can specify the dependency directly in the gemspec, ensuring that when a user runs 'bundle install' in their parent application, Bundler will automatically include Pundit.
00:06:56.800 Of course, you can also include development dependencies, as you’ll have a gemspec inside your engine, where you can add specs for the dummy app. So let's build our engine. Using the command 'bundle exec rails build', all the files and options will be compiled into one gem file.
00:08:05.600 I like to build in isolation; local environments often differ significantly, creating potential issues. Using Docker helps to mitigate this. I start from a Ruby image, install required dependencies, and cache items like Nokogiri to improve build times in Docker. By installing Bundler here and copying everything over, I can run the build command in a controlled environment.
00:08:53.520 Now, your Docker image will contain the gem file you generated in isolation, ensuring that all assets are compiled correctly. This gem file will reside inside your Docker image. We also use a helper script to pick up the latest version of the gem.
00:09:38.240 Following this, the image gets built, and I recommend deleting the latest package to prevent any mix-ups. When I build and isolate, it often involves bug fixes or new feature trials before I make stable releases and could risk deploying older packages.
00:10:00.720 After that, we upload the file from the Docker image to our local machine. To publish our gem, we employ the gem utility and use the 'gem push' command.
00:10:51.760 So here it is, your work is published, and now everyone can see it! You've done a fantastic job! But what happens if someone reports a bug or requests a new feature after your gem is published?
00:11:05.040 We’ll then have to release again, and we use versioning for this. Each time you generate a new engine or package, a version.rb file is created, starting at 0.1.0. You can increment numbers as necessary before running 'bundle install', which will update your gemfile.lock accordingly.
00:11:51.360 You can also use the 'bump' gem to automate version increments for pre-releases, minor, major, or patch versions, which adjusts the digits and adds the specified version. This can also commit the code and create a tag for you in GitHub.
00:12:07.520 Whenever you publish something new, it’s essential to inform your users about what changed. When you update a package and something breaks without any explanation, it can lead to user frustration. You must publish release notes every time.
00:12:56.640 You can do this with a changelog markdown file in your repository, documenting every update. Alternatively, frameworks like Confluence, or even GitHub releases, can be employed to keep users informed.
00:13:33.440 So what’s the update workflow? After pushing the initial gem version, any new fixes or features should be handled through branching. You add changes, commit, push to GitHub, and open a pull request (PR). Once that PR merges, pull the main branch into your local environment, bump the version number, build the gem, publish it to Rubygems, and ensure your changelog is up to date.
00:14:22.880 It can be tedious, but we can automate some of these processes using GitHub Actions. GitHub Actions, similar to most CI systems, allows you to define tasks that can run at specified events, like when you open a PR or push to a branch.
00:15:01.760 These tasks typically consist of bash commands or pre-made actions. GitHub also has a marketplace filled with actions that can help.
00:15:44.320 To automate testing, we first name our action and define its triggers, such as whenever a pull request is opened or pushed to the main branch. We will be testing our code across several Ruby and Rails versions—using the matrix strategy ensures everything works with Rails 6 and 7.
00:16:38.880 Next, we set our environment variables. For instance, you can define variables that only trigger under specific conditions, adding plenty of flexibility to your tests. Our tests run on Ubuntu, and we set up any necessary services like Postgres.
00:17:40.480 One useful pre-made action allows us to check out the code directly without needing to specify the commit or PR. We can then install Ruby, create the database, migrate, and run our tests.
00:18:03.520 In case of test failures, RSpec will generate images and logs to provide insight into what went wrong. We also implement an ‘upload artifact’ action to archive our screenshots and logs, making them available for download.
00:19:07.040 Next, we use yet another action to generate coverage reports and send them to Codecov. Parallelly, we’ll also automate linting and code analysis using Reviewdog, with jobs set to trigger on each PR.
00:19:55.840 This setup incorporates two jobs for standard Ruby and ESLint checks. GitHub Actions will not only fail on errors but also highlight the exact lines in PRs, facilitating easier debugging.
00:20:21.440 Moreover, many of these actions also provide suggestions for quick fixes, allowing you to commit any recommendations directly, which can save time.
00:21:02.080 Next, labeling PRs can help a lot. We commonly adopt strategies for classes of work, such as features, chores, fixes, and refactors. Labels will automatically attach based on your branch naming conventions, helping speed up project management.
00:21:56.000 Automating release notes is also beneficial and can be set to execute every time a merge occurs. The Release Drafter, for example, will create these notes based on the latest merge and associated pull requests.
00:22:42.960 This means every time you push changes, you'll receive nicely categorized release notes detailing features, bug fixes, and contributor information.
00:23:38.800 As we approach the finale, remember that cutting releases must be automatic. Whenever you tag your release, specific tasks will execute, including version checks, gem building, and release notes fetching.
00:24:33.760 Following this, you’ll create a release attached to the tag along with any assets they might need. Finally, all users need to do to access your gem is run 'bundle add admin', or add it to their gemfile and run 'bundle install'.
00:25:39.040 They will also need to mount the engine to their application to fully utilize its functionality. For instance, they can mount it under the '/admin' path, allowing them to access all the integrated routes, views, and controllers.
00:26:20.640 Now, let's address the asset pipeline briefly. There are various methods, with two prevalent strategies being hooking into the asset pipeline or pre-compiling assets at build time to alleviate potential issues for users.
00:27:16.320 For instance, we can use 'package.json' to define build scripts. When the project is compiled, any assets required by the engine can automatically be processed and delivered to the right public directories.
00:27:59.680 In summary, we discussed what a Rails engine is, how to generate and share it via RubyGems, updating workflows, and automating functions via GitHub Actions. Most importantly, we learned how to manage assets effectively.
00:28:20.800 Before we conclude, I have a community shoutout. If any Romanian Ruby developers are watching, please check out Ruby Romania, where we are trying to establish a new community.
00:29:05.360 In addition, there's a Short Ruby newsletter that compiles essential updates from Twitter related to Ruby. If you want updates without navigating endless debates, it’s a great option.
00:29:47.840 Always remember, be awesome, build useful things, share them with others, and most importantly, be kind! I'm Adrian the Dev; you can find me everywhere. Don’t forget to check out Avo, the Short Ruby newsletter, Ruby Romania and JIN. Thank you all!
00:30:31.360 If you have questions, please feel free to ask! I'm here for as long as I can!
00:30:44.480 Hi, thank you for your talk! I have a question about other usages of engines beyond gems. Did you consider using it as part of a larger application?
00:32:36.640 In our case, it was a monolith and we realized we could build another application within it.
00:32:56.480 We debated whether to create an engine for better scalability or just keep it namespaced within Rails. What do you think are the pros and cons of each approach?
00:33:19.840 My general advice is to build anything with scalability in mind. Constructing your features this way allows for easy extraction when needed.
00:33:47.680 Take, for instance, the marketplace scenario where buyers and sellers are distinct features; using engines makes perfect sense here.
00:34:09.040 On the other hand, if the functionalities are closely related, you might opt for a namespaced approach to simplify management.
00:34:54.240 I've also encountered situations where we began with namespacing and outgrew the structure, necessitating a shift to separate engines.
00:35:30.960 Regarding engines and webpacker, have you had experiences with running both in a main application?
00:35:46.560 Yes, we began using webpacker, but there wasn't too much documentation available. That said, it can be integrated easily.
00:36:02.720 It's definitely possible to have a webpacker asset pipeline inside your engine, either hooking into the asset pipeline or compiling assets at build time.
00:36:51.840 If you have further questions about ES build or Tailwind, I'd be glad to assist!
00:37:15.440 For cases where you have multiple engines, like buyers and sellers, how would you prevent duplication of models?
00:37:56.960 This is where modular architecture becomes beneficial. You could define shared functionalities in modules or concerns that can be included in different models across engines.
00:38:32.080 In my case with Avo, we actually don’t have any models within it; it integrates with your app to recognize the models you already have.
00:38:50.720 Feel free to ask more questions on any topics discussed in our session today. I am available both now and later tonight for further queries or insights!