Fixing my deployment mistakes
Tags: webdev, devops, nextjs, astro
Published: 2024-05-04 11:42:55

> Read the original [here](https://www.jxd.dev/blog/fixing-my-deployment-mistakes)

I recently published a [blog post](https://www.jxd.dev/blog/deploying-many-apps-from-a-monorepo) 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.

```tsx
// 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:

```bash
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](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs).

```yaml
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:

```yaml
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](https://turbo.build/repo/docs/core-concepts/monorepos/skipping-tasks), it seems fairly simple:

```bash
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:

```yaml
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 👋