Supercharging Your EmberJS Workflow with Circle CI and GitHub

As a full-stack developer, I‘m always looking for ways to improve my development workflow and ship code faster. One of the most powerful tools in my arsenal is continuous integration and continuous delivery (CI/CD). By automatically building, testing, and deploying my code whenever changes are pushed to GitHub, I can iterate more quickly and catch bugs before they reach production.

In this in-depth guide, we‘ll walk through the process of setting up a robust CI/CD pipeline for an EmberJS application using Circle CI. Whether you‘re a seasoned pro or just getting started with CI/CD, by the end of this article you‘ll have a working pipeline and the knowledge to customize it to your needs.

The Importance of CI/CD for Web Development

Before diving into the technical details, let‘s briefly explore why CI/CD has become an essential practice for modern web development teams. At its core, CI/CD is about automating the software release process so that you can deliver new features and bug fixes to users as quickly and safely as possible.

With CI, every code change is automatically built and tested in a clean environment. This immediate feedback allows developers to detect and fix integration issues early before they compound. CD takes this a step further by automatically deploying passing builds to staging or production environments.

The impact of CI/CD on key software delivery metrics is significant:

Metric Traditional Development CI/CD
Deployment Frequency 1-6 months On-demand (multiple deploys per day)
Lead Time for Changes 1-6 months Less than one day
Mean Time to Recovery (MTTR) 1-6 months Less than one day
Change Failure Rate 16-30% 0-15%

Source: 2019 Accelerate State of DevOps

In other words, teams that adopt CI/CD release more frequently, get changes to market faster, recover from failures quicker, and have fewer bugs in production. This is a major competitive advantage in a world where users expect constant improvements and new features.

Setting Up CI with Circle CI

Now that we understand the "why", let‘s get into the "how". We‘ll be using Circle CI, a popular cloud-based CI/CD platform that integrates seamlessly with GitHub.

Step 1: Create a Circle CI Account

Navigate to https://circleci.com/signup/ and create a new account. You can sign up with your GitHub account for easy access to your repositories.

Step 2: Add Your EmberJS Project

From the Circle CI dashboard, click "Add Project" and select your EmberJS repository from the list. It will default to the latest settings which are fine for our purposes. Click "Set Up Project" to continue.

Add Project in Circle CI

Step 3: Configure Your Build

Circle CI automatically detects that we‘re using EmberJS and provides a sample .circleci/config.yml file. This is where we define the jobs and steps in our build pipeline. Update the file to look like this:

version: 2.1
orbs:
  node: circleci/[email protected]
jobs:
  test:
    docker:
      - image: cimg/node:lts
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: npm
      - run:
          command: npm test
          name: Run tests
      - store_test_results:
          path: test-results
workflows:
  app-tests:
    jobs:
      - test

This config file does the following:

  • Specifies the CircleCI version and the Node.js orb for managing dependencies
  • Defines a test job that will run our EmberJS tests
  • Uses the latest LTS version of Node.js as the base Docker image
  • Checks out our code and installs dependencies using npm
  • Runs npm test to execute our EmberJS test suite
  • Stores the test results in the test-results directory for reporting

Step 4: Run Your First Build

With our config in place, it‘s time to see our pipeline in action. Commit and push your changes to GitHub. This will trigger Circle CI to run your build:

$ git add .circleci/config.yml
$ git commit -m "Add Circle CI config"
$ git push

Navigate to the Circle CI dashboard and you should see your build running:

Circle CI Running Build

If everything is configured correctly, after a few minutes the build should complete successfully! You‘ve just set up continuous integration for your EmberJS app.

Enhancing Your Test Suite

While having a CI pipeline is a great start, the true value comes from having a comprehensive test suite. Let‘s explore some best practices and techniques for testing EmberJS applications.

Testing Philosophies

When it comes to testing, there are two main schools of thought: traditional QA and agile QA. With traditional QA, testing happens at the end of the development process and is done manually by a separate QA team. In contrast, agile QA shifts testing left by incorporating it into the development process via automated tests.

For web applications, I prefer an agile "testing pyramid" approach that focuses on having a solid foundation of unit tests, a smaller set of integration tests, and a few high-level end-to-end (E2E) tests. This provides a good balance of test coverage, speed, and maintainability.

Testing Pyramid

Source: The Practical Test Pyramid

Writing Effective Tests

Writing tests is easy. Writing good tests is hard. An effective test suite should be:

  • Fast: Tests should run quickly so that developers can get rapid feedback.
  • Reliable: Tests should be deterministic and not fail sporadically.
  • Maintainable: Tests should be easy to understand and update as the codebase changes.
  • Relevant: Tests should cover the most important and error-prone parts of your application.

Some specific techniques for EmberJS include:

  • Use QUnit for unit testing controller actions, routes, models, and services
  • Leverage rendering tests to verify a component‘s output based on different inputs
  • Write application tests to validate user flows and interactions
  • Use test doubles and stubs to isolate concerns and improve test speed
  • Generate and track test coverage to identify areas for improvement

Here‘s an example of testing a simple EmberJS component:

import { module, test } from ‘qunit‘;
import { setupRenderingTest } from ‘ember-qunit‘;
import { render } from ‘@ember/test-helpers‘;
import hbs from ‘htmlbars-inline-precompile‘;

module(‘Integration | Component | pretty-color‘, function(hooks) {
  setupRenderingTest(hooks);

  test(‘it renders a div with the color class‘, async function(assert) {
    this.set(‘colorName‘, ‘red‘);

    await render(hbs`<PrettyColor @name={{this.colorName}} />`);

    assert.equal(this.element.querySelector(‘div‘).className, ‘red‘);
  });
});

Test-Driven Development

One of the most impactful ways to improve your testing habits is to embrace test-driven development (TDD). With TDD, you write tests before implementing the corresponding feature. This forces you to think about edge cases upfront and ensures your code is testable by default.

The TDD cycle consists of three steps:

  1. Write a failing test that defines the desired behavior
  2. Implement the minimum amount of code to make the test pass
  3. Refactor the solution to improve the design while keeping the tests green

Over time, this "red-green-refactor" loop becomes a powerful habit that can lead to a more robust and maintainable codebase.

Optimizing Your CI Pipeline

Once you have a core CI pipeline in place, you can start exploring more advanced optimizations to speed up your builds and reduce costs.

Caching Dependencies

By default, each CI run will download and install all of your project‘s dependencies from scratch. For larger projects, this can take a significant amount of time. To speed things up, you can cache your node_modules directory across builds:

steps:
  - checkout
  - node/install-packages:
      pkg-manager: npm
      cache-path: ~/.npm
  - run:
      command: npm test
      name: Run tests

This tells Circle CI to save the ~/.npm directory after each build and restore it on subsequent runs. Just be sure to clear the cache if you update your dependencies!

Parallelizing Tests

Another way to speed up test execution is to split your tests across multiple machines. Circle CI supports parallelism at the job level:

test:
  parallelism: 4

This will create four identical containers and distribute your tests across them. Circle CI automatically balances the load and merges the results.

Keep in mind that not all tests can be parallelized efficiently. I/O heavy tests or E2E scenarios that require a global browser state are better suited for running serially. You may need to experiment to find the optimal distribution for your suite.

Leveraging Docker

By default, Circle CI provides a variety of pre-built environments for popular languages. However, for maximum control and reproducibility, you can define your own Docker images:

jobs:
  test:
    docker:
      - image: cimg/node:lts
        auth:
          username: mydockerhub-user  
          password: $DOCKERHUB_PASSWORD  # context / project UI env-var reference

Here we‘re using the official Node.js LTS image from Docker Hub. This ensures that our CI environment exactly matches our local development setup. You can also use custom images to include additional tools or dependencies.

Securing Sensitive Data

One of the challenges of CI/CD is managing sensitive data like API keys, database credentials, and other secrets. You don‘t want to commit this information to your repository where it could be exposed.

Instead, use Circle CI‘s built-in support for encrypted environment variables. From the Circle CI dashboard, navigate to your project‘s settings and select "Environment Variables". Here you can add key-value pairs that will be securely injected into your build containers.

Circle CI Environment Variables

You can then reference these variables in your .circleci/config.yml file:

steps:
  - run:
      command: |
        npm run build
        AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} npm run deploy

Circle CI also supports creating reusable variable sets called "Contexts". This is useful for sharing common secrets across projects.

Continuous Deployment with Circle CI

While outside the scope of this guide, it‘s worth mentioning that Circle CI also has excellent support for continuous deployment. By adding a deploy job to your .circleci/config.yml, you can automatically push passing builds to staging or production.

For example, here‘s how you might deploy an EmberJS app to AWS S3:

version: 2.1
orbs:
  aws-cli: circleci/[email protected]
  node: circleci/[email protected]
jobs:
  test:
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: npm
      - run:
          command: npm test
          name: Run tests
      - store_test_results:
          path: test-results
  deploy:
    docker:
      - image: cimg/python:3.9
        auth:
          username: mydockerhub-user  
          password: $DOCKERHUB_PASSWORD  # context / project UI env-var reference
    steps:
      - aws-cli/setup
      - checkout
      - node/install-packages:
          pkg-manager: npm
      - run:
          command: npm run build
          name: Build app
      - aws-s3/sync:
          from: dist
          to: ‘s3://my-bucket‘
          arguments: | 
            --acl public-read \
            --cache-control "max-age=86400"
workflows:
  app-tests:
    jobs:
      - test
      - deploy:
          requires:
            - test 
          filters:
            branches:
              only: main

This configuration adds a new deploy job that uses the AWS CLI to sync the dist directory to an S3 bucket. The filters block ensures that deployment only happens on the main branch after the test job has passed.

With a few tweaks, you can adapt this to deploy to Heroku, Netlify, or any other hosting provider.

Conclusion

Congratulations! You now have a solid foundation in CI/CD concepts and a working Circle CI pipeline for your EmberJS application.

But don‘t stop here. Continually look for ways to enhance your testing strategy, optimize your builds, and automate repetitive tasks. The beauty of CI/CD is that it‘s an iterative process – as your application grows and evolves, so should your pipeline.

"Continuous improvement is better than delayed perfection." – Mark Twain

I‘d encourage you to explore other features of Circle CI like artifacts, workspaces, and orbs. The official Circle CI documentation is a great resource.

If you‘re hungry for more CI/CD knowledge, I highly recommend the book Continuous Delivery by Jez Humble and David Farley. It provides a comprehensive overview of the principles and practices behind successful CD implementations.

You might also consider exploring other CI/CD platforms like GitHub Actions, Travis CI, or Jenkins to compare their features and tradeoffs. While the specifics may vary, the core concepts and benefits remain the same.

At the end of the day, investing in CI/CD is one of the highest-leverage ways to improve your productivity and code quality as a full-stack developer. By automating the tedious parts of your workflow, you can focus on what really matters: building great applications.

So go forth and deploy with confidence! Happy coding.

Similar Posts