This repository demonstrates a possible implementation of a simple Deployment Pipeline (as proposed by Dave Farley) in a Rails project implemented using GitHub Actions.
Caution
This example is not fit for production use as-is. Check the Things out of Scope section for details.
The deployment pipeline consists of three main stages:
This stage runs on every pull request and push. It includes jobs for scanning Ruby and JavaScript code for security vulnerabilities, linting the code for consistent style, and running unit tests.
Implementation: .github/workflows/commit-stage.yml
This stage runs after the successful completion of the Commit Stage on the main
branch. It builds a Docker image as artifact, tags it with the current commit
SHA and pushes i
8000
t to Docker Hub.
Implementation: .github/workflows/create-artifact.yml
This stage runs after the successful completion of the Create Artifact stage. It starts a Docker container from the built image and runs acceptance tests against it.
Implementation: .github/workflows/acceptance-stage.yml
The acceptance tests are written using the 4 Layer Acceptance Test Architecture (also proposed by Dave Farley).
This layer contains the actual test cases that define the acceptance criteria. The test cases are written solely using the DSL layer.
Implementation: test/system/ directory
This layer provides a high-level language to describe actions and assertions from a business perspective, typically from the point of view of an end user. It is recommended to avoid mentioning technical details or UI elements, focusing instead on what is happening or being done in business domain terms.
Responsibilities of the DSL layer include:
- Providing an easy-to-use and readable API for authoring test cases.
- Offering sensible default parameter values, allowing test cases to be written without specifying details irrelevant to the scenario.
- Mapping identifiers to unique aliases before passing them to the driver or performing assertions. This enables test cases to execute in parallel against the same environment without interfering with each other.
Implementation: test/support/acceptance_test_dsl.rb included inside test/application_system_test_case.rb
This layer interacts with the system under test using a specific protocol (commonly through the UI or a web API). It translates high-level commands from the DSL into low-level operations, handling how business actions are executed within your system.
If your system offers multiple methods of interaction, you would implement a separate driver for each one, ensuring that the DSL remains agnostic to the underlying protocol.
Implementation: test/support/capybara_acceptance_test_driver.rb
This is the actual application or system being tested. When executing acceptance tests, it is crucial to have an environment that closely mirrors a real production environment to accurately assess whether the application currently fulfills all its business requirements.
At the same time, it is equally important to control external variables, such as system time or responses from external systems that are beyond your control, to ensure deterministic test results. Controlling time, for example, is essential for testing time-dependent functionality, as such tests would be impossible to conduct reliably otherwise.
To achieve this, the system must be designed to allow the injection and replacement of such dependencies through configuration, enabling seamless substitution of such external factors.
Following things are whole sub-areas which would have added a lot of incidental complexity to this sample and have been left out and/or simplified on purpose.
Currently, "deploying the application" means just starting a local Docker
container (see
./bin/deploy-to-acceptance-test-environment
used in the acceptance stage for details).
I also removed the default Kamal settings generated by Rails from this project to not give the impression that this project could be deployed anywhere as it is.
Since the Application is not actually deployable - the deployment pipeline just finishes after the acceptance stage and does not push the changes to the production environment as would be common with a real Continuous Delivery workflow.
I added support for a environment variable to completely disable SSL in production and enabled it in the acceptance stage.
The application itself has a concept of users but for simplicity's sake there
is neither user management (new users are added via a public /users/new page)
nor any kind of authentication (users just login via their user name).
This app's page design is powered by ChatGPT and the Bulma CSS framework.