惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
O
OpenAI News
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
云风的 BLOG
云风的 BLOG
罗磊的独立博客
S
SegmentFault 最新的问题
The Register - Security
The Register - Security
Hugging Face - Blog
Hugging Face - Blog
D
DataBreaches.Net
U
Unit 42
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
雷峰网
雷峰网
V
Visual Studio Blog
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
博客园 - 【当耐特】
G
Google Developers Blog
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
I
InfoQ
Martin Fowler
Martin Fowler
F
Fortinet All Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
AI
AI
Google Online Security Blog
Google Online Security Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - Franky
Blog — PlanetScale
Blog — PlanetScale
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org

Maxime Heckel's Blog

On Rendering the Sky, Sunsets, and Planets - The Blog of Maxime Heckel Shades of Halftone - The Blog of Maxime Heckel Field Guide to TSL and WebGPU - The Blog of Maxime Heckel On Shaping Light: Real-Time Volumetric Lighting with Post-Processing and Raymarching for the Web - The Blog of Maxime Heckel Speaking at Figma Config 2025 - The Blog of Maxime Heckel Post-Processing Shaders as a Creative Medium - The Blog of Maxime Heckel On Crafting Painterly Shaders - The Blog of Maxime Heckel The Art of Dithering and Retro Shading for the Web - The Blog of Maxime Heckel Moebius-style post-processing and other stylized shaders - The Blog of Maxime Heckel Shining a light on Caustics with Shaders and React Three Fiber - The Blog of Maxime Heckel Real-time dreamy Cloudscapes with Volumetric Raymarching - The Blog of Maxime Heckel Painting with Math: A Gentle Study of Raymarching - The Blog of Maxime Heckel Building a magical AI-powered semantic search from scratch - The Blog of Maxime Heckel Beautiful and mind-bending effects with WebGL Render Targets - The Blog of Maxime Heckel Refraction, dispersion, and other shader light effects - The Blog of Maxime Heckel The magical world of Particles with React Three Fiber and Shaders - The Blog of Maxime Heckel The Study of Shaders with React Three Fiber - The Blog of Maxime Heckel Building a Design System from scratch - The Blog of Maxime Heckel Everything about Framer Motion layout animations - The Blog of Maxime Heckel Building a Vaporwave scene with Three.js - The Blog of Maxime Heckel Cubic Bézier: from math to motion - The Blog of Maxime Heckel First steps with GPT-3 for frontend developers - The Blog of Maxime Heckel Migrating to Next.js - The Blog of Maxime Heckel Static Tweets with MDX and Next.js - The Blog of Maxime Heckel Advanced animation patterns with Framer Motion - The Blog of Maxime Heckel Scrollspy demystified - The Blog of Maxime Heckel The Power of Composition with CSS Variables - The Blog of Maxime Heckel My first failed SwiftUI project - The Blog of Maxime Heckel Guide to creating animations that spark joy with Framer Motion - The Blog of Maxime Heckel Using Shortcuts and serverless to build a personal Apple Health API - The Blog of Maxime Heckel SEO mistakes I've made and how I fixed them - The Blog of Maxime Heckel Going native: SwiftUI from the perspective of a React developer - The Blog of Maxime Heckel Build your own preview deployment service - The Blog of Maxime Heckel The little guide to CI/CD for frontend developers - The Blog of Maxime Heckel Immigrating to the US - The Blog of Maxime Heckel The physics behind spring animations - The Blog of Maxime Heckel Generate screenshots of your code with a serverless function - The Blog of Maxime Heckel How to use Framer Motion with Emotion styled-components - The Blog of Maxime Heckel Data Fetching with NextJS: What I learned - The Blog of Maxime Heckel Learning in public - The Blog of Maxime Heckel Fixing the dark mode flash issue on server rendered websites - The Blog of Maxime Heckel How to fix NPM link duplicate dependencies issues - The Blog of Maxime Heckel Running scheduled cross-browser end-to-end tests on Github CI - The Blog of Maxime Heckel How I built my first custom ESLint rule - The Blog of Maxime Heckel React Lazy: a take on preloading views - The Blog of Maxime Heckel Automated UI accessibility testing with Cypress - The Blog of Maxime Heckel Building a GraphQL wrapper for the Docker API - The Blog of Maxime Heckel Switching off the lights - Adding dark mode to your React app - The Blog of Maxime Heckel Getting started with Typescript on Gatsby - The Blog of Maxime Heckel Rebuilding Redux with Hooks and Context - The Blog of Maxime Heckel Asynchronous rendering with React - The Blog of Maxime Heckel Using Flow generics to type generic React components - The Blog of Maxime Heckel How to efficiently type your styled-components with Flow - The Blog of Maxime Heckel How I got started with Kubernetes on GKE - The Blog of Maxime Heckel React sub-components Part 3: Whitelisting sub-components with flow - The Blog of Maxime Heckel React sub-components Part 2: Using the new Context API - The Blog of Maxime Heckel React sub-components - The Blog of Maxime Heckel Running Golang tests with Jest - The Blog of Maxime Heckel No title No title
Building the perfect GitHub CI workflow for your frontend team - The Blog of Maxime Heckel
Maxime Heckel · 2021-08-03 · via Maxime Heckel's Blog

You've probably noticed if you've been following me for a while that I'm a ✨ big fan ✨ of automation. I wrote about automated CI/CD a year ago and also talked a lot about the concepts surrounding this subject, but never really touched upon the tools I use for my CI jobs and how I use them. One such tool that has really worked for me, especially as a frontend engineer, is GitHub CI.

For over a year now, it's been my service of choice for automated CI/CD pipelines. The workflow syntax is easy to get started with, and has an extended set of features to help you craft your CI experience the way you and your team may want it.

However, even after a year, there still is a lot that I'm learning about this tool every day. When I got started with it, there was no set rule on how to properly architect your workflows, and there's a lot of tips, tricks I discovered along the way to build what I would qualify as "the perfect GitHub CI workflow" (at least to my eyes 😄). This article aims to gather those tips and good practices I've been using for personal projects and at work and show you how you can use all of those in a single workflow to power the CI/CD pipeline of your frontend team.

What would constitute a "good" GitHub CI workflow?

I'm going to throw my best "engineer response" at this question: it depends! Your team might have specific needs or objectives that would make some of my tips not as useful to you as they could be. However, for this article, we need some guidelines that I think would be universal when it comes to building efficient GitHub workflows, such as:

  • cost-saving: bring the "build minutes" down to the lowest possible value to not have a massive bill at the end of the month.

  • efficient: your team's time is precious, the workflow should be as fast as possible, but also fast to fail if something were to go wrong

  • well-architected: each step has a purpose, and might depend on other steps. This also means not running "useless steps".

Now that we've established those guidelines, let's take a look at one of the most important tips of this article.

One workflow to rule them all

Let's consider a typical set of tasks a frontend team would run on every PR:

  1. Lint

  2. Formatting

  3. Type checking

  4. Unit test

  5. Build

  6. End-to-end tests, maybe on different browsers

Running those in separate workflows might look like the most straightforward way to architect those tasks. However, if something as simple as the lint task fails, there's no way you can stop your expensive tasks like build or your end-to-end tests from running. And that, my friends, is not very efficient.

Workflows run in parallel, and there's no way for them to interact with one another. Thus, you can't cancel a workflow due to another workflow's failed state. You're stuck running all the workflows in every PR.

To address this, I chose to combine all my workflows into one. All the tasks that were independent workflows before became part of the same unique workflow, but this time, as jobs.

Excerpt of a Github CI workflow job.

11

- name: Checkout Commit

12

uses: actions/checkout@v2

13

- name: Use Node.js ${{ matrix.node }}

14

uses: actions/setup-node@v1

16

node-version: ${{ matrix.node }}

The cool thing about jobs is that you can run them sequentially or parallel as you please! GitHub provides a handy keyword called needs that lets you set one or several jobs as dependencies, thus preventing a given job to start unless the dependent jobs have successfully run. This allows us to:

  • Fail the workflow fast. If a key job fails, the workflow is marked as failed on your PR as soon as possible

  • Avoid running useless expensive tasks on a "doomed to fail" workflow run

Example of jobs running in parallel and sequentially

13

- name: Checkout Commit

14

uses: actions/checkout@v2

15

- name: Use Node.js ${{ matrix.node }}

16

uses: actions/setup-node@v1

18

node-version: ${{ matrix.node }}

23

runs-on: ubuntu-latest

28

- name: Checkout Commit

29

uses: actions/checkout@v2

30

- name: Use Node.js ${{ matrix.node }}

31

uses: actions/setup-node@v1

33

node-version: ${{ matrix.node }}

38

runs-on: ubuntu-latest

42

needs: [type-check, unit-test]

44

- name: Checkout Commit

45

uses: actions/checkout@v2

46

- name: Use Node.js ${{ matrix.node }}

47

uses: actions/setup-node@v1

49

node-version: ${{ matrix.node }}

You may be wondering: what job should be run in parallel and what job needs to be run sequentially? That will depend on the needs of your team.

On my end, I tend to parallelize unit testing, linting, and type-checking for example. These steps are generally fast and inexpensive to run thus I do not feel they need to depend on each other in most cases. However, I'd require a job such as build to only run if those three jobs above are successful, i.e. run it sequentially.

The screenshot below features the GitHub Workflow powering the CI for this blog. Yours will probably end up sharing some similarities:

Screenshot of a successful Github workflow run showcasing each jobs and their dependencies with one another.

Screenshot of a successful Github workflow run showcasing each jobs and their dependencies with one another.

Screenshot of a failed Github workflow run. Notice how failing the 'build' job did not triggered the two parallel e2e jobs as they were dependent on this job to be successful.

Screenshot of a failed Github workflow run. Notice how failing the 'build' job did not triggered the two parallel e2e jobs as they were dependent on this job to be successful.

As you can see, by combining all our workflows into one, and carefully choosing which job to parallelize or run sequentially, we end up having better visibility on how our CI pipeline functions and the dependencies between each of its steps.

Sharing is caring

Now that all the CI steps are combined into one single workflow, the main challenge is to find out how we can make them as efficient as possible by sharing critical job outputs.

However, it's not very obvious from the get-go how one can share job outputs with other jobs on GitHub CI. There are two ways that I found to be "efficient":

  1. leveraging caching with actions/cache

  2. uploading/downloading artifacts using respectively actions/upload-artifact and actions/download-artifact

The first one is "great" but only for tasks that are repetitive and have outputs that do not change much over time like installing NPM dependencies.

Example of sharing npm dependencies through multiple GitHub CI jobs

10

- name: Checkout Commit

11

uses: actions/checkout@v2

12

- name: Use Node.js ${{ matrix.node }}

13

uses: actions/setup-node@v1

15

node-version: ${{ matrix.node }}

16

- name: Cache yarn dependencies

17

uses: actions/cache@v2

18

id: cache-dependencies

21

key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}

23

${{ runner.os }}-yarn-

24

- name: Install Dependencies

28

if: steps.cache-dependencies.outputs.cache-hit != 'true'

30

yarn install --force --non-interactive

34

runs-on: ubuntu-latest

40

- name: Checkout Commit

41

uses: actions/checkout@v2

42

- name: Use Node.js ${{ matrix.node }}

43

uses: actions/setup-node@v1

45

node-version: ${{ matrix.node }}

49

- name: Restore yarn dependencies

50

uses: actions/cache@v2

51

id: cache-dependencies

54

key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}

56

${{ runner.os }}-yarn-

Using artifacts, however, is what made a significant difference in the efficiency of my GitHub CI workflows.

For example, if you have 2 jobs that respectively run your e2e tests on firefox and chrome, you do not want to build your frontend twice as this could significantly increase the number of "billable minutes" for your CI run. The optimal solution here would consist of having a build job before your end-to-end tests running only once and then share the build artifacts with your chrome-end-to-end and firefox-end-to-end jobs.

To achieve this, we need to leverage actions/upload-artifact and actions/download-artifact:

  • once the build is successful, use actions/upload-artifact to upload your build artifacts

  • then use action/download-artifact on any jobs you want to pull that build output and use it

It's important to note that this trick only works because we're running every single CI step in the same workflow. You can only download artifacts in a workflow that were uploaded during the same workflow run.

Uploading and downloading artifacts to share the build output

13

- name: Upload build artifacts

14

uses: actions/upload-artifact@v2

27

- name: Download build artifacts

28

uses: actions/download-artifact@v2

35

uses: cypress-io/github-action@v2.10.1

45

- name: Download build artifacts

46

uses: actions/download-artifact@v2

51

uses: cypress-io/github-action@v2.10.1

Setting the retention days option when uploading artifacts

9

- name: Upload build artifacts

10

uses: actions/upload-artifact@v2

"You are terminated"

My last tip, and perhaps my favorite due to its simplicity is terminating duplicate workflow runs.

It happens to me very often: I'm done with a current branch and decide to push my code and open a PR, thus triggering a workflow run. Then a few seconds later noticed I forgot to run that one console.log or made a typo somewhere and need to push an extra change, thus triggering yet another workflow run.

By default, there's nothing that will stop the first workflow to run, it will continue until it's finished, thus wasting precious billing minutes that could have had a better use.

To prevent such a thing from happening, GitHub recently introduced the notion of workflow concurrency.

With the concurrency keyword you can create a concurrency group for your workflow (or a job). This will mark any workflow run from that same concurrency group as "pending" if any run is currently in progress. You can also decide to cancel any in-progress workflow of the same concurrency group whenever a new workflow is added to the queue.

Example of GitHub workflow using concurrency groups

10

group: ${{ github.head_ref }}

13

cancel-in-progress: true

Doing this at the workflow level will ensure that any old or outdated workflows that are in progress will get canceled when we push a new change and triggering a new workflow thus saving your team's precious time and money.

Conclusion

So now that we went through all the tips to build the perfect Github CI workflow to power the CI needs of a frontend team, let's take a look at how they hold up against the guidelines we've established earlier:

Is it cost-saving? Yes! We made sure to share the output of expensive steps such as build and to cache repetitive steps that we would have needed to run throughout the workflow like installing our dependencies.

Is it efficient? More efficient than running every job in a separate workflow for sure! Not only we are parallelizing independent jobs like e2e-tests-firefox and e2e-tests-chrome, we're also making sure to cancel any duplicate workflows thanks to the use of concurrency groups.

Is it well architected? As we saw in the screenshot showcased earlier in this blog post, it's now easy to visualize all the steps and their dependencies. Combining every task into one workflow and architecting those jobs using the needs keyword made the whole CI pipeline way easier to understand.

Need a full example? Don't you worry, I got you covered 🙌! You can find my Github CI workflow featuring all the tips and examples of this article on the GitHub repository of this blog. It's fairly dense and long, thus why I did not directly integrate it here directly as it might have been distracting.

I hope some of the tips I introduced in this blog post will help you and your team perfect your own GitHub workflows and thus achieve a fast and reliable CI pipeline for your favorite frontend projects! Are there any other tips that you wish I had introduced in this article? Other GitHub CI secrets that empowered your team worth mentioning? As always, do not hesitate to reach out! I'd love learn more about what worked for you and test them out to further improve this article!