Fixing my deployment mistakes

RMAG news

Read the original here

I recently published a blog post showing how I use Github Actions to deploy apps to Vercel from my monorepo. That solution was very basic but it worked. However, I expected it to work for me a little longer than it did 🤦‍♂️

So in this post I’m going to work on fixing a few issues that have cropped up. I’m going to structure this blog post a little differently than normal. Instead of fixing the problems, perfecting my solution and writing about it – I’m going to write this post while I work and publish it with very little editing (probably none because I’m quite lazy).

I think this’ll be an interesting exercise. It’ll force me to think out loud and then get to share my process with everyone.

So without further ado, let’s break down the problems.

Problems

Yesterday, all my troubles seemed so far away, and I noticed that my builds were failing. Upon closer inspection the reason was that I’m hitting my Vercel limits.

Deploying vntg/jxd
Error: Resource is limited – try again in 6 hours (more than 100, code: “api-deployments-free-per-day”).

Also, with the addition of a bunch more projects into the monorepo, the build time has now crept up towards the 10min mark. That is kinda my personal patience limit…

So those are the two main things I want to solve today.

Brain dump

In the last post I talked about two potential solutions to this problem:

Parallelise the builds with Github’s matrices strategy
Use ts-ignore to only build and deploy projects that have actually changed

I could probably get away with just using ts-ignore for now but think doing both would be beneficial for a few reasons:

Even greater performance
Simplify my code so that everything deployment related lives within the workflow

Recap

Before I start on those two improvements I’d like to recap the existing deploy.ts file and look for some improvements before transitioning away. I think it’ll end up being quicker if I 1) refresh the solution in my mind and 2) migrate a simpler solution.

// deploy.ts
import util from util;
import { exec } from child_process;

const asyncExec = util.promisify(exec);

const projects = [
{
path: apps/bigjournal/web, // Is this needed?
projectId: “”,
},
// …
];

async function run() {
const token = process.env.VERCEL_TOKEN;

if (!token) { // Unnecessary in GH actions
throw new Error(missing vercel token);
}

const orgId = process.env.VERCEL_ORG_ID;

if (!orgId) {
throw new Error(missing org id);
}

for (const { path, projectId } of projects) { // Matrix
console.log(`deploying ${path}`);
console.log(`pulling vercel settings`);
await asyncExec(
`VERCEL_PROJECT_ID=${projectId} vercel pull –yes –environment=production –token=${token}`
);
console.log(`running vercel build`);
await asyncExec(
`VERCEL_PROJECT_ID=${projectId} vercel build –prod –token=${token} ${path}` // Path is only used here!
);
console.log(`deploying to vercel`);
await asyncExec(
`VERCEL_PROJECT_ID=${projectId} vercel deploy –prod –prebuilt –token=${token}`
);
console.log(`✅ deployed ${path}`);
}
}

run()
.then(() => console.log(✅ complete))
.catch((e) => {
console.error(e);
process.exit(1);
});

My first thought is do I even need the path in the configuration? Since we have to run vercel from the root, and the path is stored in the Vercel settings that are pulled… is it needed?

So I’m going to test this locally with my Big Journal project:

export VERCEL_ORG_ID=“…” VERCEL_PROJECT_ID=“…”
vercel pull –yes –environment=production
vercel deploy –prod

✅ that has worked perfectly! So now I know that path isn’t required! I will quickly update the deploy script and push (just to be extra safe).

Matrices

Something I haven’t worked with before and so I’ll need to refer to the documentation.

jobs:
example_matrix:
strategy:
matrix:
version: [10, 12, 14]
os: [ubuntu-latest, windows-latest]

Looking at this example it seems simple enough to split out deploy into a separate job, that depends on the CI job, and have a matrix of project IDs.

After a few changes this is the workflow I am left with:

name: ci/cd

on:
push:
branches: [main”]

env:
DATABASE_URL: ${{secrets.DATABASE_URL}}
TURBO_TOKEN: ${{ secrets.VERCEL_TOKEN }}
TURBO_TEAM: vntg

jobs:
ci:
name: ci
timeout-minutes: 15
runs-on: ubuntu-latest

steps:
name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 2

uses: pnpm/action-setup@v3
with:
version: 8

name: setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm’

name: deps
run: pnpm install

name: build
run: pnpm build

cd:
name: cd
timeout-minutes: 15
runs-on: ubuntu-latest
needs: [ci]
strategy:
matrix:
project:
# big journal
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{matrix.project}}

steps:
name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 2

uses: pnpm/action-setup@v3
with:
version: 8

name: setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm’

name: install vercel cli
run: pnpm install –global vercel@latest

name: pull
run: vercel pull –yes –environment=production –token=${VERCEL_TOKEN}

name: build & deploy
run: vercel deploy –prod –token=${VERCEL_TOKEN}

I also removed the old deploy.ts script and any dependencies it had before pushing and eagerly awaiting the results.

Already this gives a huge performance improvement of ~5x 🙌 but doesn’t help with the biggest problem of hitting our Vercel build limits. For that let’s look into turbo-ignore.

turbo-ignore

Looking at the docs, it seems fairly simple:

npx turbo-ignore workspace –task=build

This command will exit with 1 if the project needs to be rebuilt (based on the commit history).

I can handle this with continue-on-error and if within the workflow. First, I’ll also need to add the workspace value to the matrix using a map.

The final cd job looks like this:

cd:
name: cd
timeout-minutes: 15
runs-on: ubuntu-latest
needs: [ci]
strategy:
matrix:
project:
id:
workspace: @bigjournal/web”
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{matrix.project.id}}

steps:
name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 2

uses: pnpm/action-setup@v3
with:
version: 8

name: setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm’

name: install vercel cli
run: pnpm install –global vercel@latest

id: check
name: check
run: npx turbo-ignore ${{ matrix.project.workspace }} –task=build
continue-on-error: true

name: pull
if: steps.check.outcome != ‘success’
run: vercel pull –yes –environment=production –token=${VERCEL_TOKEN}

name: build & deploy
if: steps.check.outcome != ‘success’
run: vercel deploy –prod –token=${VERCEL_TOKEN}

Conclusion

So I’ve now implemented both fixes and I’m much happier with the overall result. Time will tell if this fixes all deployment issues but right now I’m confident (although I said that last time).

Here’s the highlights:

Everything deployment related is contained within one file
Only changed apps are deployed
Performance improvement of 5x-10x

Until next time – ciao 👋

Leave a Reply

Your email address will not be published. Required fields are marked *