diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 000000000..e8c911ec9
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,60 @@
+# This is a basic workflow to help you get started with Actions
+
+name: Build font
+
+# Controls when the workflow will run
+on:
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ # This workflow contains a single job called "build"
+ font-builder:
+ # The type of runner that the job will run on
+ runs-on: ubuntu-latest
+
+ # Steps represent a sequence of tasks that will be executed as part of the job
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - uses: actions/checkout@v3
+
+ - name: Set env ZIP file name
+ id: zip-name
+ shell: bash
+ # Set the archive name to repo name + "-assets" e.g "MavenPro-assets"
+ run: echo "ZIP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')-fonts" >> $GITHUB_ENV
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+
+ - name: Install dependencies
+ run: |
+ python3 -m pip install --upgrade pip
+ python3 -m pip install gftools
+
+ - name: Build font
+ run: |
+ cd sources
+ bash build.sh
+
+ - name: Archive artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ env.ZIP_NAME }}
+ path: fonts
+
+ - name: Upload font
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: Upload compiled fonts
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..bcce22655
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+# python virtual environment for installing gftools
+fontenv
+
+# auto generated file by gftools
+source/build.ninja
+source/.ninja_log
diff --git a/source/build.sh b/source/build.sh
new file mode 100644
index 000000000..845e4084c
--- /dev/null
+++ b/source/build.sh
@@ -0,0 +1,3 @@
+rm fonts/*.otf
+node clean-ufo.mjs
+gftools builder source/config.yaml
diff --git a/source/clean-ufo.mjs b/source/clean-ufo.mjs
new file mode 100644
index 000000000..0af096bcf
--- /dev/null
+++ b/source/clean-ufo.mjs
@@ -0,0 +1,55 @@
+import { readdirSync, readFileSync, writeFileSync } from "node:fs";
+
+const a = readdirSync("./source/CactusClassicalSerif-Regular.ufo", {
+ withFileTypes: true,
+}).filter((o) => o.isFile());
+const b = readdirSync("./source/CactusClassicalSerif-Regular.ufo/glyphs", {
+ withFileTypes: true,
+}).filter((o) => o.isFile());
+
+for (const file of a) {
+ const path = `${file.path}/${file.name}`;
+ let data = readFileSync(path, "utf-8");
+ if (data.charCodeAt(0) === 0xfeff) {
+ data = data.slice(1);
+ writeFileSync(path, data);
+ }
+}
+
+for (const file of b) {
+ const path = `${file.path}/${file.name}`;
+ let data = readFileSync(path, "utf-8");
+ if (data.charCodeAt(0) === 0xfeff) {
+ data = data.slice(1);
+ writeFileSync(path, data);
+ }
+}
+
+let fontinfo = readFileSync(
+ "./source/CactusClassicalSerif-Regular.ufo/fontinfo.plist",
+ "utf-8"
+);
+
+if (!fontinfo.includes("encodingID")) {
+ fontinfo = fontinfo.replace(
+ new RegExp(`platformID\\n\\s+3`),
+ "platformID3encodingID10"
+ );
+ writeFileSync(
+ "./source/CactusClassicalSerif-Regular.ufo/fontinfo.plist",
+ fontinfo
+ );
+}
+
+let features = readFileSync(
+ "./source/CactusClassicalSerif-Regular.ufo/features.fea",
+ "utf-8"
+);
+
+if (features.includes("@\\BASE")) {
+ features = features.replace(/@\\BASE.*/s, "");
+ writeFileSync(
+ "./source/CactusClassicalSerif-Regular.ufo/features.fea",
+ features
+ );
+}
diff --git a/source/config.yaml b/source/config.yaml
new file mode 100644
index 000000000..7bdb80377
--- /dev/null
+++ b/source/config.yaml
@@ -0,0 +1,4 @@
+sources:
+ - CactusClassicalSerif-Regular.ufo
+familyName: "Cactus Classical Serif"
+removeOutlineOverlaps: false