오픈소스 기여하기

Bumgu

2025/10/22

Categories: dev Tags: typescript ci opensource

저는 CI tool로 Github Actions를 자주 사용합니다.
다들 아시다시피, Github Actions는 yaml기반으로 workflow파일을 작성하는데 그러다보면 인덴트, 혹은 잘못된 문법으로 CI job failed 알람을 받아본 경험이 많을것 입니다.
그러다가 트위터 에서 image 이런 포스트를 발견했습니다.
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));
}

SetupBunOptionsbun깃허브에서 가져왔습니다.

테스트 작성

정상적으로 돌아가는지 /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"
      "
    `);
  });
});

결과는 image 정상적으로 통과합니다.

테스트

이제 진짜 TypeScript 로 workflow를 만들고, 정상적으로 yml을 생성하는지 확인하겠습니다.

#!/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);

이 파일에 실행권한을 주고 실행합니다. image 정상적으로 작동하는것을 확인했으니 이제 커밋하고 pr을 올립니다!

결과

image merge가 되었습니다!


개인적으로 항상 오픈소스 컨트리뷰터가 되고싶었었는데 Kubernetes, Grafana 등 대형 프로젝트는 엄두가 안나서 손을 못대고 있었습니다.
이번 기회에 무려 스타가 100개가 넘는 프로젝트에 기여할 수 있어서 좋은 경험이었습니다.

>> Home