Skip to content

워크플로우 작성

이 가이드는 gaji를 사용하여 타입 안전한 GitHub Actions 워크플로우를 작성하는 방법을 설명합니다.

독립 실행형 TypeScript 파일

gaji가 생성한 워크플로우 파일은 독립적으로 실행 가능합니다. TypeScript 런타임(tsx, ts-node, Deno)으로 직접 실행하여 워크플로우 JSON을 출력할 수 있습니다. 디버깅과 검사에 편리합니다.

기본 구조

gaji 워크플로우는 세 가지 주요 구성 요소로 이루어집니다:

  1. 액션: getAction()을 사용하여 가져오기
  2. 작업: Job 클래스를 사용하여 생성
  3. 워크플로우: Workflow 클래스를 사용하여 생성
ts
import { 
getAction
,
Job
,
Workflow
} from "../generated/index.js";
// 1. 액션 가져오기 const
checkout
=
getAction
("actions/checkout@v5");
// 2. 워크플로우와 job, 스텝 생성 new
Workflow
({
name
: "CI",
on
: {
push
: {
branches
: ["main"] } },
}) .
jobs
(
j
=>
j
.
add
("build",
new
Job
("ubuntu-latest")
.
steps
(
s
=>
s
.
add
(
checkout
({}))
) ) ) .
build
("ci");

액션 사용하기

gaji dev 실행

bash
gaji dev --watch

액션 가져오기

getAction()을 사용하여 액션 가져오기:

typescript
const checkout = getAction("actions/checkout@v5");
const setupNode = getAction("actions/setup-node@v4");
const cache = getAction("actions/cache@v4");

타입 안전성으로 액션 사용

액션은 설정을 받는 함수를 반환합니다:

typescript
const step = checkout({
  name: "Checkout code",
  with: {
    // ✅ 모든 입력에 대한 완전한 자동완성!
    repository: "owner/repo",
    ref: "main",
    token: "${{ secrets.GITHUB_TOKEN }}",
    "fetch-depth": 0,
  },
});

에디터가 제공하는 것:

  • ✅ 모든 액션 입력에 대한 자동완성
  • ✅ 타입 체크
  • ✅ action.yml의 문서
  • ✅ 기본값 표시

Job 생성

Job은 Job 클래스로 생성합니다:

typescript
const job = new Job("ubuntu-latest");

지원되는 러너

typescript
// Ubuntu
new Job("ubuntu-latest")
new Job("ubuntu-22.04")
new Job("ubuntu-20.04")

// macOS
new Job("macos-latest")
new Job("macos-13")
new Job("macos-12")

// Windows
new Job("windows-latest")
new Job("windows-2022")
new Job("windows-2019")

// Self-hosted
new Job("self-hosted")
new Job(["self-hosted", "linux", "x64"])

스텝 추가

.steps() 콜백과 .add()로 스텝을 추가합니다:

typescript
const job = new Job("ubuntu-latest")
  .steps(s => s
    // 액션 스텝
    .add(checkout({
      name: "Checkout",
    }))

    // run 명령
    .add({
      name: "Build",
      run: "npm run build",
    })

    // 여러 줄 명령
    .add({
      name: "Install dependencies",
      run: `
        npm ci
        npm run build
        npm test
      `.trim(),
    })

    // 환경 변수 포함
    .add({
      name: "Deploy",
      run: "npm run deploy",
      env: {
        NODE_ENV: "production",
        API_KEY: "${{ secrets.API_KEY }}",
      },
    })

    // 조건부 스텝
    .add({
      name: "Upload artifacts",
      if: "success()",
      run: "npm run upload",
    })
  );

워크플로우 생성

기본 워크플로우

typescript
new Workflow({
  name: "CI",
  on: {
    push: {
      branches: ["main"],
    },
  },
})
  .jobs(j => j
    .add("build", buildJob)
  )
  .build("ci");

트리거 이벤트

Push

typescript
on: {
  push: {
    branches: ["main", "develop"],
    tags: ["v*"],
    paths: ["src/**", "tests/**"],
  },
}

Pull Request

typescript
on: {
  pull_request: {
    branches: ["main"],
    types: ["opened", "synchronize", "reopened"],
  },
}

Schedule (Cron)

typescript
on: {
  schedule: [
    { cron: "0 0 * * *" },  // 매일 자정
  ],
}

복수 트리거

typescript
on: {
  push: { branches: ["main"] },
  pull_request: { branches: ["main"] },
  workflow_dispatch: {},  // 수동 트리거
}

복수 Job

typescript
const test = new Job("ubuntu-latest")
  .steps(s => s
    .add(checkout({}))
    .add({ run: "npm test" })
  );

const build = new Job("ubuntu-latest")
  .steps(s => s
    .add(checkout({}))
    .add({ run: "npm run build" })
  );

new Workflow({
  name: "CI",
  on: { push: { branches: ["main"] } },
})
  .jobs(j => j
    .add("test", test)
    .add("build", build)
  )
  .build("ci");

Job 의존성

JobConfig 생성자 파라미터에서 needs를 사용합니다:

typescript
const test = new Job("ubuntu-latest")
  .steps(s => s
    .add({ run: "npm test" })
  );

const deploy = new Job("ubuntu-latest", {
  needs: ["test"],  // test job 완료 대기
})
  .steps(s => s
    .add({ run: "npm run deploy" })
  );

new Workflow({
  name: "Deploy",
  on: { push: { branches: ["main"] } },
})
  .jobs(j => j
    .add("test", test)
    .add("deploy", deploy)
  )
  .build("deploy");

매트릭스 빌드

JobConfig 생성자에서 strategy를 사용합니다:

typescript
const test = new Job("${{ matrix.os }}", {
  strategy: {
    matrix: {
      os: ["ubuntu-latest", "macos-latest", "windows-latest"],
      node: ["18", "20", "22"],
    },
  },
})

생성된 YAML이 포함된 전체 매트릭스 빌드 예제는 매트릭스 빌드 예제를 참조하세요.

컴포지트 액션

Action을 사용하여 재사용 가능한 컴포지트 액션을 만듭니다. 입력을 정의하고, .steps()로 스텝을 추가한 뒤, .build()로 리포지토리에 action.yml을 생성합니다.

전체 예제는 컴포지트 액션 예제를 참조하세요. 전체 API는 Action을 참조하세요.

Job 상속

Job을 상속하여 재사용 가능한 파라미터화된 Job 템플릿을 만들 수 있습니다:

ts
import { 
Job
,
getAction
,
Workflow
} from "../generated/index.js";
const
checkout
=
getAction
("actions/checkout@v5");
const
setupNode
=
getAction
("actions/setup-node@v4");
class
NodeTestJob
extends
Job
{
constructor(
nodeVersion
: string) {
super("ubuntu-latest"); this.
steps
(
s
=>
s
.
add
(
checkout
({}))
.
add
(
setupNode
({
with
: { "node-version":
nodeVersion
} }))
.
add
({
run
: "npm ci" })
.
add
({
run
: "npm test" })
); } }

전체 API 레퍼런스와 고급 패턴(예: DeployJob)은 Job 상속을 참조하세요.

전체 예제: WorkflowCall과 조합하여 환경별 배포 워크플로우 구성

재사용 가능한 워크플로우(workflow_call)를 만든 뒤, WorkflowCall로 환경마다 호출하는 패턴입니다.

먼저, 배포 작업을 담은 재사용 가능한 워크플로우를 작성합니다. workflow_callinputs로 환경 이름을 받습니다.

ts
import { 
getAction
,
Job
,
Workflow
} from "../generated/index.js";
const
checkout
=
getAction
("actions/checkout@v5");
const
setupNode
=
getAction
("actions/setup-node@v4");
new
Workflow
({
name
: "Publish",
on
: {
workflow_call
: {
inputs
: {
environment
: {
description
: "배포 대상 환경 (alpha, staging, live)",
required
: true,
type
: "choice",
options
: ["alpha", "staging", "live"],
}, },
secrets
: {
DEPLOY_TOKEN
: {
required
: true },
}, }, }, }) .
jobs
(
j
=>
j
.
add
("publish",
new
Job
("ubuntu-latest")
.
steps
(
s
=>
s
.
add
(
checkout
({
name
: "Checkout" }))
.
add
(
setupNode
({
name
: "Setup Node.js",
with
: { "node-version": "20",
cache
: "npm" },
})) .
add
({
name
: "Install dependencies",
run
: "npm ci" })
.
add
({
name
: "Build",
run
: "npm run build" })
.
add
({
name
: "Publish",
run
: "npm run publish:${{ inputs.environment }}",
env
: {
DEPLOY_TOKEN
: "${{ secrets.DEPLOY_TOKEN }}",
}, }) ) ) ) .
build
("publish");

다음으로, WorkflowCall을 사용하여 이 워크플로우를 환경별로 호출합니다. needs로 alpha → staging → live 순서를 지정합니다:

ts
import { 
WorkflowCall
,
Workflow
} from "../generated/index.js";
const
alpha
= new
WorkflowCall
("./.github/workflows/publish.yml", {
with
: {
environment
: "alpha" },
secrets
: "inherit",
}); const
staging
= new
WorkflowCall
("./.github/workflows/publish.yml", {
with
: {
environment
: "staging" },
secrets
: "inherit",
needs
: ["publish-alpha"],
}); const
live
= new
WorkflowCall
("./.github/workflows/publish.yml", {
with
: {
environment
: "live" },
secrets
: "inherit",
needs
: ["publish-staging"],
}); new
Workflow
({
name
: "Release",
on
: {
push
: {
tags
: ["v*"] } },
}) .
jobs
(
j
=>
j
.
add
("publish-alpha",
alpha
)
.
add
("publish-staging",
staging
)
.
add
("publish-live",
live
)
) .
build
("release");

이 구조의 장점은 배포 로직이 publish.yml 한 곳에만 존재한다는 점입니다. 배포 스텝을 수정해야 할 때 publish.ts만 고치면 세 환경 모두에 반영됩니다.

DockerAction

DockerAction으로 Docker 컨테이너 액션을 정의합니다. Dockerfile 또는 docker:// 접두사를 붙인 Docker Hub 이미지를 지정할 수 있습니다.

전체 API와 예제는 DockerAction API를 참조하세요.

환경 변수

워크플로우 수준

typescript
new Workflow({
  name: "CI",
  on: { push: { branches: ["main"] } },
  env: {
    NODE_ENV: "production",
  },
});

Job 수준

typescript
new Job("ubuntu-latest", {
  env: {
    DATABASE_URL: "${{ secrets.DATABASE_URL }}",
  },
});

스텝 수준

typescript
.add({
  run: "npm run deploy",
  env: {
    API_KEY: "${{ secrets.API_KEY }}",
  },
})

출력

타입이 지정된 스텝 출력

액션 스텝에 id를 제공하면, gaji는 타입이 지정된 출력 속성이 있는 ActionStep을 반환합니다:

typescript
const checkout = getAction("actions/checkout@v5");

// id를 제공하면 타입이 지정된 출력을 사용 가능
const step = checkout({ id: "my-checkout" });
step.outputs.ref     // "${{ steps.my-checkout.outputs.ref }}"
step.outputs.commit  // "${{ steps.my-checkout.outputs.commit }}"

Job 간 출력 전달

기본 패턴은 .jobs() 콜백을 사용하는 것으로, job 출력 컨텍스트가 자동으로 전달됩니다:

typescript
const checkout = getAction("actions/checkout@v5");

new Workflow({ name: "CI", on: { push: {} } })
  .jobs(j => j
    .add("build",
      new Job("ubuntu-latest")
        .steps(s => s
          .add(checkout({ id: "co" }))
        )
        .outputs(output => ({ ref: output.co.ref }))
    )
    .add("deploy", output =>
      new Job("ubuntu-latest", { needs: ["build"] })
        .steps(s => s
          .add({ run: "echo " + output.build.ref })
        )
    )
  )
  .build("ci");

deploy 콜백의 output 파라미터는 build job이 선언한 출력에 대한 타입이 지정된 접근을 제공하며, ${{ needs.build.outputs.ref }} 표현식을 생성합니다.

Job을 별도의 변수로 정의할 때는 jobOutputs()를 호환성 헬퍼로 사용할 수 있습니다:

typescript
const build = new Job("ubuntu-latest")
  .steps(s => s.add(checkout({ id: "co" })))
  .outputs(output => ({ ref: output.co.ref }));

const buildOutputs = jobOutputs("build", build);
// buildOutputs.ref → "${{ needs.build.outputs.ref }}"

수동으로 정의된 출력($GITHUB_OUTPUT에 쓰는 run 스텝 등)도 사용할 수 있습니다:

typescript
new Workflow({ name: "CI", on: { push: { tags: ["v*"] } } })
  .jobs(j => j
    .add("setup",
      new Job("ubuntu-latest")
        .steps(s => s
          .add({ id: "version", run: 'echo "value=1.0.0" >> $GITHUB_OUTPUT' })
        )
        .outputs({
          version: "${{ steps.version.outputs.value }}",
        })
    )
    .add("deploy", output =>
      new Job("ubuntu-latest", { needs: ["setup"] })
        .steps(s => s
          .add({ run: "deploy --version " + output.setup.version })
        )
    )
  )
  .build("ci");

1. 감시 모드 사용

개발 중에는 항상 gaji dev --watch를 사용하여 새 액션에 대한 타입을 자동으로 생성하세요.

2. 생성된 YAML 검토

커밋하기 전에 항상 생성된 YAML을 검토하여 정확성을 확인하세요.

3. 타입 안전성

gaji는 액션 입력 키의 오타와 잘못된 값 타입을 컴파일 시점에 잡아냅니다. 예제는 타입 안전성을 참조하세요.

알려진 제한사항

getAction()은 문자열 리터럴만 지원

gaji는 TypeScript 파일을 실행하지 않고 정적으로 분석하여 액션 참조를 추출합니다. 따라서 getAction()은 문자열 리터럴만 지원합니다:

typescript
// ✅ 동작 - 문자열 리터럴
const checkout = getAction("actions/checkout@v5");

// ❌ 동작하지 않음 - 변수 참조
const ref = "actions/checkout@v5";
const checkout = getAction(ref);

// ❌ 동작하지 않음 - 템플릿 리터럴
const checkout = getAction(`actions/checkout@v${version}`);

// ❌ 동작하지 않음 - 객체 속성
const checkout = getAction(config.checkoutRef);

gaji가 액션 참조를 감지하지 못하면 action.yml을 가져오거나 해당 액션의 타입을 생성하지 않습니다. 항상 owner/repo@version 문자열을 직접 전달하세요.

YAML 출력에서의 문자열 이스케이프

gaji는 JavaScript 문자열을 YAML로 변환하므로, JavaScript에서 이미 이스케이프된 문자가 출력에서 이중 이스케이프될 수 있습니다:

typescript
// TypeScript에서 \n은 줄바꿈 문자
.add({ run: "echo \"hello\nworld\"" })

JS 문자열에는 실제 줄바꿈이 포함되어 있어 YAML에서 올바르게 처리됩니다. 하지만 YAML 출력에 리터럴 \n 문자를 그대로 유지하려면 이중 이스케이프가 필요합니다:

typescript
// YAML에서 리터럴 \n을 유지하려면 이중 이스케이프
.add({ run: "echo hello\\nworld" })

: 여러 줄 명령어에는 이스케이프 시퀀스 대신 템플릿 리터럴을 사용하세요:

typescript
.add({
  run: `
    echo hello
    echo world
  `.trim(),
})

다음 단계

Released under the MIT License.