저는 CI tool로 Github Actions를 자주 사용합니다.
다들 아시다시피, Github Actions는 yaml기반으로 workflow파일을 작성하는데 그러다보면 인덴트, 혹은 잘못된 문법으로 CI job failed 알람을 받아본 경험이 많을것 입니다.
그러다가 트위터 에서
이런 포스트를 발견했습니다.
이 gha-ts 프로젝트는 TypeScript 로 workflow파일을 작성하면 .yml파일을 생성해주는 오픈소스입니다.
인덴트나 문법 걱정없이 type safe한 workflow를 생성할 수 있습니다.
그런데 setup에 node, java, python, go, dotnet은 존재하는데 bun이 없는걸 보고 bun setup을 추가하기로 마음 먹었습니다.
코드
우선 Repository 를 클론받고 브랜치를 생성했습니다.
/src/actions/setup.ts 파일에 모든 setup 함수와 인터페이스 등이 선언되어있어서 이 파일에 코드를 작성했습니다.
기존 코드
import { uses, UsesStep } from "../workflow-types";
import { buildWith, CamelToKebabMap } from "./common";
export interface SetupNodeOptions {
nodeVersion?: string; // node-version
cache?: "npm" | "pnpm" | "yarn";
cacheDependencyPath?: string; // cache-dependency-path
registryUrl?: string; // registry-url
scope?: string; // scope
alwaysAuth?: boolean | string; // always-auth
nodeVersionFile?: string; // node-version-file
architecture?: string; // architecture
checkLatest?: boolean | string; // check-latest
token?: string; // token
mirror?: string; // mirror
mirrorToken?: string; // mirror-token
}
const setupNodeMap: CamelToKebabMap = {
nodeVersion: "node-version",
nodeVersionFile: "node-version-file",
cacheDependencyPath: "cache-dependency-path",
registryUrl: "registry-url",
alwaysAuth: "always-auth",
checkLatest: "check-latest",
mirrorToken: "mirror-token",
};
export function setupNode(options: SetupNodeOptions = {}): UsesStep {
return uses("actions/setup-node@v4", buildWith(options, setupNodeMap));
}
/*... 중략*/
export interface SetupDotnetOptions {
dotnetVersion?: string; // dotnet-version
dotnetQuality?: string; // dotnet-quality
globalJsonFile?: string; // global-json-file
sourceUrl?: string; // source-url
owner?: string; // owner
configFile?: string; // config-file
cache?: boolean | string; // cache
cacheDependencyPath?: string; // cache-dependency-path
}
const setupDotnetMap: CamelToKebabMap = {
dotnetVersion: "dotnet-version",
dotnetQuality: "dotnet-quality",
globalJsonFile: "global-json-file",
sourceUrl: "source-url",
configFile: "config-file",
cacheDependencyPath: "cache-dependency-path",
};
export function setupDotnet(opts: SetupDotnetOptions = {}): UsesStep {
return uses("actions/setup-dotnet@v4", buildWith(opts, setupDotnetMap));
}
코드는 간단합니다. set-up에 들어가는 옵션(version 등)을 정의한 인터페이스와, 해당 인터페이스를 kebab-case로 매핑해놓은 Map, 그리고 그것들을 사용해 return해주는 setup함수만 있으면 완성이었습니다.
(2025년 10월22일 기준 CamelToKebabMap은 삭제됨)
아래와 같이 코드를 작성했습니다.
추가한 코드
export interface SetupBunOptions {
bunVersion?: string; // bun-version
bunVersionFile?: string; // bun-version-file
bunDownloadUrl?: string; // bun-download-url
registryUrl?: string; // registry-url
scope?: string; // scope
}
const setupBunMap: CamelToKebabMap = {
bunVersion: "bun-version",
bunVersionFile: "bun-version-file",
bunDownloadUrl: "bun-download-url",
registryUrl: "registry-url",
scope: "scope",
};
export function setupBun(opts: SetupBunOptions = {}): UsesStep {
return uses("oven-sh/setup-bun@v2", buildWith(opts, setupBunMap));
}
SetupBunOptions는 bun의 깃허브에서 가져왔습니다.
테스트 작성
정상적으로 돌아가는지 /tests/ 경로에 테스트 파일을 작성했습니다.
import { describe, it, expect } from "bun:test";
import { setupBun } from "../src/actions/setup";
import { createSerializer } from "../src/render";
import { Workflow } from "../src/workflow-types";
const renderWorkflow = (workflow: Workflow) => {
return createSerializer(workflow, Bun.YAML.stringify).stringifyWorkflow();
};
describe("setupBun", () => {
it("should render with default options", () => {
const workflow: Workflow = {
name: "Test Workflow",
on: "push",
jobs: {
test: {
"runs-on": "ubuntu-latest",
steps: [setupBun()],
},
},
};
expect(renderWorkflow(workflow)).toMatchInlineSnapshot(`
"# Do not modify!
# This file was generated by https://github.com/JLarky/gha-ts
name: Test Workflow
"on": push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: oven-sh/setup-bun@v2
"
`);
});
it("should render with a specific bun version", () => {
const workflow: Workflow = {
name: "Test Workflow",
on: "push",
jobs: {
test: {
"runs-on": "ubuntu-latest",
steps: [setupBun({ bunVersion: "1.0.0" })],
},
},
};
expect(renderWorkflow(workflow)).toMatchInlineSnapshot(`
"# Do not modify!
# This file was generated by https://github.com/JLarky/gha-ts
name: Test Workflow
"on": push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.0.0
"
`);
});
it("should render with all options", () => {
const workflow: Workflow = {
name: "Test Workflow",
on: "push",
jobs: {
test: {
"runs-on": "ubuntu-latest",
steps: [
setupBun({
bunVersion: "latest",
bunVersionFile: ".bun-version",
bunDownloadUrl: "https://example.com/bun.zip",
registryUrl: "https://registry.npmjs.org",
scope: "@my-scope",
}),
],
},
},
};
expect(renderWorkflow(workflow)).toMatchInlineSnapshot(`
"# Do not modify!
# This file was generated by https://github.com/JLarky/gha-ts
name: Test Workflow
"on": push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version-file: .bun-version
bun-download-url: https://example.com/bun.zip
registry-url: https://registry.npmjs.org
scope: "@my-scope"
"
`);
});
});
결과는
정상적으로 통과합니다.
테스트
이제 진짜 TypeScript 로 workflow를 만들고, 정상적으로 yml을 생성하는지 확인하겠습니다.
.github/workflows/setup-bun.ts
#!/usr/bin/env bun
import { YAML } from "bun";
import { workflow } from "../../src/workflow-types";
import { checkout, setupBun } from "../../src/actions";
import { generateWorkflow } from "../../src/cli";
const wf = workflow({
name: "Example workflow",
on: {
push: { branches: ["main"] },
pull_request: {},
},
jobs: {
exampleJobBun: {
"runs-on": "ubuntu-latest",
steps: [
checkout({ fetchDepth: 0 }),
setupBun({ bunVersion: "1.3.0" }),
{ name: "Test", run: "bun --version" },
],
},
},
});
await generateWorkflow(wf, YAML.stringify, import.meta.url);
이 파일에 실행권한을 주고 실행합니다.
정상적으로 작동하는것을 확인했으니 이제 커밋하고 pr을 올립니다!
결과
merge가 되었습니다!
개인적으로 항상 오픈소스 컨트리뷰터가 되고싶었었는데 Kubernetes, Grafana 등 대형 프로젝트는 엄두가 안나서 손을 못대고 있었습니다.
이번 기회에 무려 스타가 100개가 넘는 프로젝트에 기여할 수 있어서 좋은 경험이었습니다.