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