Doing ci/cd the easy way.

To Begin with I Would Love to reIntroduce You to My Favourite Cli Tool Mise

Mise is a version manager for dev tools & dependencies, a task runner, an environment variables manager all in one place. Mise ensures that every collaborator is working on the same environment and each every stage is reproducible.

In an earlier post, I have discussed, managing environment variables with mise . Today I will be discussing mise’s task runner feature.

For example, if you are using mise, then you can write task recipes to execute a particular job. One mise task can have multiple child subtasks.

Like:

[tasks.install]
description = "Install JS dependencies"
run = "nub install"

[tasks.build]
description="build project"
run = "nub run build"

[tasks.start]
description="start project in production env"
run = "nub run start"

[tasks.upload]
run = "surge ./dist test69.surge.sh"
description = "deploy the built dir on surge"

Btw surge is an awesome tool that deploys your static site for free on their CDN.

you can check them out: https://surge.sh

You can see basic tasks for a simple Node.js application. But you might ask, “How do I use this in CI/CD?”

The simple answer is:

With the help of mise as a task runner and with subtasks.

[tasks.ci]
description = "production build"
run = [
    mise run install
    mise run build
]

[tasks.deploy]
description="deploy project"
run = [
    mise run ci
    mise run upload
]

Whenever someone runs mise run ci, the output always stays deterministic, which you can test on your own machine. before pushing.

And Now a Question Arises: What If?

I’ve Written Something Wrong in Tasks Then Isn’t It the Same as Writing the Entire Pipeline?

The simple answer is no. Mise never allows you to ship an invalid configuration, plus you can always debug your tasks:

mise doctor

Safety?

Mise always checks and matches check-sums (SLSA) for each package downloaded. It’s generally suggested to not use “latest”, but to pin versions for example, [email protected] or [email protected].

And if you want to containerize the app before running it:

mise oci run

This will spin up a container for you containing the application. (This is not suggested for chunk blobs) you can move those build artifacts to some alpine linux or distroless container.

Want to Spin up a Devcontainer ?

mise gen devcontainer --write

Mise handles most of the things for you.

It’s up to you and your creativity on, how well you write task recipes. Personally, I prefer writing atomic tasks so each task stays debug able, manageable and reproducible.

Mise can handle shell scripts as tasks too, and you can use virtually any CLI tool thanks to its nice plugin ecosystem.

Let’s Stay on Our Main Topic: CI.

In any CI platform like GitHub Actions, GitLab Pipelines, Bitbucket Pipelines, Forgejo CI, or any custom CI one thing is always common in all, your ability to run containers and running commands inside it.

Want to write a GitHub Action?

mise gen github-action —write
name: ci

on:
  workflow_dispatch:
  pull_request:
  push:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  MISE_EXPERIMENTAL: true

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6
      - uses: jdx/mise-action@v3
      - run: mise run ci

But our goal isn’t to understand yaml for doing CI, but to simplify the process of writing CI pipelines. The goal is to simplify the process:

name: ci

on:
  push:
  workflow_dispatch:
  pull:

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  ci:
    runs-on: ubuntu-latest
    container:
        image: ghcr.io/jdx/mise:latest
    steps:
      - name: checkout code
        uses: actions/checkout@main

      - name: Install dependencies & building
        run: |
            mise deps
            mise run fetch
            mise run build

        env:
          SOME_ENV: ${{ secrets.SOME_ENV }}

      - name: Upload artifact
        uses: actions/upload-pages-artifact@main
        with:
          path: ./public

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@main

For you, the basic structure of the pipeline stays the same always like some kind of like a boilerplate and the quest of one tab vs 4 spaces somehow becomes irrelevant. You can now focus on writing tasks and let mise handle the rest.

The fun part is that you can now run the same on any CI platform,

each will be giving you a predictable result

See a sample workflow for Forgejo Actions:

name: CI

on:
  push:
  pull_request:
  workflow_dispatch:

env:
  MISE_EXPERIMENTAL: true

jobs:
  check:
    name: Check
    runs-on: codeberg-medium-lazy
    container:
      image: ghcr.io/jdx/mise:latest

    steps:
      - name: Check out source code
        uses: actions/[email protected]

      - name: install dependencies
        run: |
            mise deps
            mise run deploy
        env:
          SOME_ENV: ${{ secrets.SOME_ENV }}

And the same for GitLab Actions:

image: ghcr.io/jdx/mise:latest

stages:

- build

build:
  stage: build
  script:
    - mise deps
    - mise run deploy
Subscribe on GitHub