CI/CDとは、コードの変更を自動でテストし(CI:継続的インテグレーション)、いつでもリリースできる状態に保ち、自動で本番まで届ける(CD:継続的デリバリー/継続的デプロイ)ための仕組みです。人の手で行っていたビルド・テスト・デプロイを自動化し、「小さな変更を、早く、安全に」届けられるようにします。

この記事では、CI/CDの3つの言葉の違いとパイプラインの流れを整理したうえで、GitHub Actionsで最初のCIとCDのワークフローを作るまでを解説します。matrix、キャッシュ、secrets、environmentsの承認、permissionsの最小化やアクションのSHA固定といった安全な書き方まで扱い、ワークフロー内で行う lint・テストを手元で実行した結果と、actionlint(ワークフローの静的チェックツール)でわざと間違えたYAMLを検証した結果も載せています。

CI/CDとGitHub Actionsの早見表

用語 意味 自動化される範囲
CI(継続的インテグレーション) 変更をこまめに統合し、そのたびに自動でビルド・テストする コミット → ビルド → テスト
継続的デリバリー(CD) いつでも本番に出せる状態を保つ。本番反映は人が承認 テスト → 検証環境 → (承認)→ 本番
継続的デプロイ(CD) テストを通った変更を本番まで自動で反映 テスト → 検証環境 → 本番(すべて自動)
GitHub Actions GitHubに組み込まれたCI/CDの実行環境 .github/workflows/*.yml に書いた処理を自動実行
GitHub Actionsのキーワード 意味
on いつ動かすか(push、pull_request、スケジュール、手動など)
jobs 実行する仕事の単位。既定では並列に動く
runs-on 実行するマシン(ランナー)。例:ubuntu-latest
steps ジョブの中で順番に実行する手順
uses 公開されているアクション(部品)を使う
run シェルのコマンドを実行する
needs ジョブの順番(依存関係)を決める

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CI/CDとは|GitHub Actionsで最初のパイプラインを作るまで」

CI/CDとは

CI:継続的インテグレーション

CIは、変更をこまめにメインのブランチへ統合し、そのたびに自動でビルドとテストを行う取り組みです。

CIがないとき CIがあるとき
統合をまとめて行うため、競合や不具合が一度に見つかる 小さな変更ごとに問題が見つかり、原因を絞りやすい
「自分の環境では動く」が起きやすい 毎回同じ環境でテストされる
レビューで書式や単純なミスを指摘し合う lint・フォーマットは機械が確認し、レビューは設計に集中できる

CD:継続的デリバリーと継続的デプロイ

CDには2つの意味があり、違いは「本番への反映を人が承認するかどうか」です。

項目 継続的デリバリー 継続的デプロイ
本番への反映 人が承認して実行(ボタン1つ) 自動
向いている場面 リリースのタイミングを業務と合わせたい、承認の記録が必要 テストが充実していて、1日に何度もリリースしたい
前提 CIと検証環境へのデプロイが自動化されている 加えて、自動テスト・監視・ロールバックが整っている

まず継続的デリバリーから始め、テストと監視が育ってから継続的デプロイへ進むのが、無理のない順番です。

パイプラインの流れ

コードをpush / プルリクエスト
   ↓
[CI] チェックアウト → 依存関係のインストール → lint → テスト → ビルド
   ↓(mainに取り込まれたら)
[CD] 成果物を保存 → 検証環境(staging)にデプロイ → 確認
   ↓(承認)
[CD] 本番(production)にデプロイ

前の段階が失敗したら、次の段階には進みません。この「通らないと先に進めない関門」を品質ゲートと呼びます。

GitHub Actionsで最初のCIを作る

例として、Pythonの小さなプロジェクトを使います(ツールは uv、ruff、pytest)。

.
├── .github/workflows/ci.yml
├── pyproject.toml
├── src/calc/__init__.py
└── tests/test_calc.py
# src/calc/__init__.py
def add_tax(price: int, rate: float = 0.10) -> int:
    """税込み価格(1円未満切り捨て)を返す。"""
    if price < 0:
        raise ValueError(f"price は0以上: {price}")
    return int(price * (1 + rate))
# pyproject.toml(抜粋)
[dependency-groups]
dev = ["pytest", "ruff"]

[tool.ruff.lint]
select = ["E", "F", "I"]

ワークフローのYAML

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.12", "3.13", "3.14"]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true
      - name: Install
        run: uv sync --locked
      - name: Lint
        run: uv run ruff check .
      - name: Format check
        run: uv run ruff format --check .
      - name: Test
        run: uv run pytest -q
部分 意味
on: pull_request / push: branches: [main] プルリクエストのたびと、mainへの取り込み時に実行
permissions: contents: read 自動で発行されるトークンの権限を読み取りだけにする(後述)
concurrency 同じブランチで新しいpushがあったら、古い実行を取り消す
timeout-minutes: 10 処理が止まったときに、長時間動き続けないようにする
matrix 3つのPythonバージョンで並列にテスト
fail-fast: false 1つのバージョンが失敗しても、他のバージョンの結果も最後まで見る
uses: ...@<SHA> # v7.0.1 アクションをコミットSHAで固定(後述)
enable-cache: true 依存関係のダウンロードをキャッシュして速くする
uv sync --locked ロックファイルどおりに入れる。ロックファイルが古いと失敗する

ワークフローの中の処理を、手元で実行した結果

CIで動かす lint とテストは、手元でも同じコマンドで確認できます。まず手元で通してから push すると、CIの待ち時間を減らせます。

最初の実行では、lint が import の並び順を指摘しました(ruff 0.16.9)。

$ uv run ruff check .
I001 [*] Import block is un-sorted or un-formatted
 --> tests/test_calc.py:1:1
  |
1 | / import pytest
2 | | from calc import add_tax
  | |________________________^
...
Found 1 error.
[*] 1 fixable with the `--fix` option.
$ uv run ruff format --check .
unformatted: File would be reformatted
 --> tests/test_calc.py:4:1
...
1 file would be reformatted, 1 file already formatted

コマンドは終了コード1を返しました。CIでは、終了コードが0以外のステップは失敗となり、ジョブがそこで止まります。これが品質ゲートの仕組みです。自動修正してから再実行します。

$ uv run ruff check --fix . && uv run ruff format .
Found 1 error (1 fixed, 0 remaining).
1 file reformatted, 1 file left unchanged

$ uv run ruff check . && uv run ruff format --check . && uv run pytest -q
All checks passed!
2 files already formatted
..                                                                       [100%]
2 passed in 0.01s

わざと計算を間違えた場合のテスト結果です(pytest 9.1.1)。

$ uv run pytest -q
F.                                                                       [100%]
=================================== FAILURES ===================================
_________________________________ test_add_tax _________________________________

    def test_add_tax():
>       assert add_tax(1000) == 1100
E       assert 100 == 1100
E        +  where 100 = add_tax(1000)

tests/test_calc.py:7: AssertionError
=========================== short test summary info ============================
FAILED tests/test_calc.py::test_add_tax - assert 100 == 1100
1 failed, 1 passed in 0.02s

CIの画面にも同じ出力が表示されるので、どのテストがどんな値で失敗したかがすぐにわかります。

CIの品質ゲートの例

ゲート 確認すること 例(Python) 例(Node.js)
lint 書き方の誤り、未使用の変数 ruff check eslint
フォーマット 書式がそろっているか ruff format --check prettier --check
型チェック 型の不一致 mypy / pyright tsc --noEmit
テスト 動作が正しいか pytest vitest / jest
ビルド 成果物が作れるか uv build npm run build
依存関係の確認 既知の脆弱性がないか Dependabot など npm audit など

速いチェック(lint)を先に、時間のかかるチェック(テスト・ビルド)を後に並べると、失敗したときに早く結果がわかります。GitHubのリポジトリ設定(ブランチ保護・ルールセット)でこのジョブの成功をマージの条件にすると、ゲートとして機能します。

GitHub Actionsで検証→本番のCDを作る

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
      - run: uv build
      - uses: actions/upload-artifact@v7
        with:
          name: dist
          path: dist/

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/download-artifact@v7
        with:
          name: dist
      - name: Deploy to staging
        run: ./scripts/deploy.sh staging
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/download-artifact@v7
        with:
          name: dist
      - name: Deploy to production
        run: ./scripts/deploy.sh production
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

./scripts/deploy.sh は、実際のデプロイ先に合わせて用意するスクリプトの例です。

ポイント 内容
一度だけビルドする build ジョブで作った成果物を、検証と本番の両方で使う。検証したものと同じものが本番に出る
needs build → staging → production の順に実行。前が失敗したら先に進まない
environment デプロイ先ごとに、承認ルールやsecretsを分けられる
cancel-in-progress: false デプロイは途中で取り消さず、順番待ちにする

environments で本番の前に承認を入れる

リポジトリの Settings → Environments で production を作り、Required reviewers(承認者)を設定すると、deploy-production ジョブは承認されるまで待機します。これで「継続的デリバリー」の形になります。

保護ルール 内容
Required reviewers 最大6人(またはチーム)の誰かが承認するまで待つ。自分で自分の実行を承認できないようにする設定もある
Wait timer 指定した時間待ってから実行
Deployment branches and tags main など、決めたブランチからだけデプロイできるようにする
Environment secrets その環境を指定したジョブだけが使えるsecrets。承認が必要な環境では、承認されるまで読めない

プランによる違い:GitHub Free・Pro・Teamでは、Required reviewers と Wait timer は公開リポジトリでのみ使えます。非公開リポジトリで使うにはGitHub Enterpriseが必要です(2026年9月時点の公式ドキュメント)。

secrets の扱い

やること 理由
APIキーなどは Settings → Secrets に登録し、${{ secrets.NAME }} で参照 YAMLやコードに書かない
必要なステップの env にだけ渡す 使わないステップに見せない
本番用は Environment secrets に置く 承認前のジョブや別の環境から使えない
クラウドへのデプロイは OIDC(permissions: id-token: write)を検討 長期間有効なキーを保存しなくて済む

ログに出力されたsecretsの値は自動で *** に置き換えられますが、加工した値(一部を切り出したもの等)は隠せない場合があります。secretsをechoしないことが基本です。

安全なワークフローの書き方

permissions を最小にする

ワークフローには、GitHubを操作するためのトークン(GITHUB_TOKEN)が自動で渡されます。既定の権限はリポジトリや組織の設定によって異なるため、ワークフローの先頭で明示しておくと安心です。

permissions:
  contents: read        # ワークフロー全体は読み取りだけ

jobs:
  comment:
    permissions:
      contents: read
      pull-requests: write   # このジョブだけ、PRへのコメントを許可

全体を最小にし、必要なジョブだけに必要な権限を足すのが基本です。

アクションのバージョンを固定する(SHA固定)

書き方 例 特徴
ブランチ actions/checkout@main 常に最新。内容が予告なく変わる
メジャータグ actions/checkout@v7 読みやすい。タグは付け替えられるため、中身が変わりうる
コミットSHA actions/checkout@3d3c42e...# v7.0.1 中身が変わらない。GitHub公式のセキュリティガイドで推奨

第三者のアクションは、フルの長さのコミットSHAで固定し、コメントにバージョンを書いておくのがおすすめです。SHAはタグから次のように確認できます。

git ls-remote https://github.com/actions/checkout.git refs/tags/v7.0.1
3d3c42e5aac5ba805825da76410c181273ba90b1    refs/tags/v7.0.1

SHAで固定すると更新が手間になるため、Dependabot(.github/dependabot.yml で package-ecosystem: "github-actions")で更新のプルリクエストを自動で作ると両立できます。組織の設定で、SHA固定を必須にすることもできます。

外部から入る値を、run に直接埋め込まない

プルリクエストのタイトルなど、誰でも入力できる値を ${{ }} で run に直接書くと、その値がシェルのコマンドとして解釈されるおそれがあります(スクリプトインジェクション)。環境変数を経由して渡すと安全です。

      - name: Greet
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
        run: |
          echo "PR title: $PR_TITLE"

actionlintでワークフローを検証する

actionlint は、GitHub Actionsのワークフローを実行前にチェックする静的解析ツールです。YAMLの構文だけでなく、キーの打ち間違い、式の誤り、needs の参照先、古いアクション、危険な書き方まで見つけてくれます。今回は v1.7.12 をソースからビルドして使いました(通常は公式のリリースやパッケージマネージャーで入手できます)。

上の ci.yml と deploy.yml を検証した結果です。

$ actionlint
$ echo $?
0

問題がなければ何も表示されず、終了コード0になります。

わざと間違えたYAML

name: CI
on:
  pull-request:
  schedule:
    - cron: "0 25 * * *"
permissions:
  contents: reed
jobs:
  test:
    run-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.12", "3.13"]
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v7
        with:
          python-version: ${{ matrix.python }}
      - name: Greet
        run: echo "PR title: ${{ github.event.pull_request.title }}"
  deploy:
    needs: tset
    runs-on: ubuntu-latest
    steps:
      - run: echo deploy

1回目:YAMLとして読めない

.github/workflows/ci.yml:20:27: could not parse as YAML: mapping values are not allowed in this context [syntax-check]
   |
20 |         run: echo "PR title: ${{ github.event.pull_request.title }}"
   |                           ^~

run: の値の途中に「:(コロンと空白)」があると、YAMLはそこを新しい「キー: 値」と読んでしまいます。run: | の複数行の形にするか、値全体を引用符で囲みます。直して再実行した結果です。

2回目:内容の誤りがまとめて見つかった

ci.yml:3:3: unknown Webhook event "pull-request". see https://docs.github.com/... [events]
ci.yml:5:13: invalid CRON format "0 25 * * *" in schedule event: end of range (25) above maximum (23): 25 [events]
ci.yml:7:13: "reed" is invalid as permission of scope "contents". available values are "read", "write", "none" [permissions]
ci.yml:9:3: "runs-on" section is missing in job "test" [syntax-check]
ci.yml:10:5: unexpected key "run-on" for "job" section. expected one of "concurrency", "container", ... "runs-on", ... [syntax-check]
ci.yml:15:15: the runner of "actions/checkout@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue [action]
ci.yml:18:31: property "python" is not defined in object type {python-version: number} [expression]
ci.yml:20:34: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. ... [expression]
ci.yml:22:3: job "deploy" needs job "tset" which does not exist in this workflow [job-needs]
行 間違い 正しい書き方
3 pull-request pull_request(アンダースコア)
5 cronの時が25 0〜23。cronはUTCなので日本時間の9時は 0 0 * * *
7 reed read
10 run-on runs-on
15 actions/checkout@v2 現在のメジャーバージョン(SHA固定を推奨)
18 matrix.python matrix.python-version(matrixで定義した名前)
20 PRタイトルを run に直接埋め込み env 経由で渡す
22 needs: tset needs: test

これらの多くは、GitHubにpushしてもワークフローが動かない・失敗する・期待と違う動きをする原因になります。actionlintを手元やCIの最初のステップで実行すると、push前に気づけます。

YAMLのよくある落とし穴

書き方 起きること 直し方
python-version: [3.10] 数値として読まれ 3.1 になる ["3.10"] と引用符で囲む
run: echo "a: b" YAMLの構文エラー run: | の複数行形式
on: push だけ フォークからのPRでCIが動かない pull_request も指定
インデントのずれ 別のキーの子として読まれる actionlintで確認

よくある質問

Q. GitHub Actionsは無料で使えますか? A. 公開リポジトリでは、GitHubが用意する標準のランナーを無料で使えます。非公開リポジトリでは、プランごとに毎月の無料枠(実行時間とストレージ)があり、超えた分は課金されます。最新の条件はGitHubの料金ページで確認してください。

Q. CIとCDは、どちらから始めればよいですか? A. CIから始めます。lint とテストが自動で動くだけでも、レビューとリリースの負担が大きく減ります。テストが安定してきたら、検証環境への自動デプロイ、承認つきの本番デプロイへと広げていきます。

Q. matrix で実行時間が増えるのが心配です。 A. matrixのジョブは並列に実行されるので、全体の待ち時間はあまり増えません。ただし、実行時間の合計(無料枠の消費)は増えるため、プルリクエストでは主要なバージョンだけ、mainでは全バージョン、のように分けることもできます。

Q. ワークフローを手元で試す方法はありますか? A. lint・テストなどのコマンドは、そのまま手元で実行できます。ワークフローの書き方の誤りは actionlint で確認できます。ワークフロー全体を手元のコンテナで動かす「act」というツールもありますが、GitHub上のランナーと完全に同じ環境ではない点に注意が必要です。

まとめ

  • CIは「変更のたびに自動でビルド・テスト」、CDは「いつでも・自動で届ける」
  • 継続的デリバリーは本番に人の承認、継続的デプロイは本番まで自動
  • GitHub Actionsは on・jobs・steps・uses・run を .github/workflows/*.yml に書く
  • lint → テスト → ビルドの品質ゲートを通らないと先に進めないようにする
  • 本番は environments の承認、secrets は必要なジョブ・ステップだけに渡す
  • permissions は最小、アクションはコミットSHAで固定してDependabotで更新
  • actionlint で、キーの打ち間違い・式の誤り・危険な書き方をpush前に見つける

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CI/CDとは|GitHub Actionsで最初のパイプラインを作るまで」

株式会社RJCでは、テストやデプロイを自動化して、安心して変更を届けられる開発の進め方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?