SCANOSS GitHub Actions モノレポ対応:変更コンポーネントだけスキャンする構成

サイオステクノロジーは、OSS管理ツール「SCANOSS」の日本国内初の代理店です。

前回の記事ではSCANOSS GitHub Actionsを使った基本的なコードスキャン自動化の手順を紹介しました。基本スキャンやポリシーチェック、サブディレクトリスキャンまで確認できたところで、次に気になるのはモノレポ構成への対応です。

scanPathパラメータは単一の相対パスしか受け付けないため、複数のサブディレクトリを個別にスキャンするにはワークフロー側で工夫が必要です。本記事では、SCANOSS公式のMONOREPO_SETUP.mdで推奨されているreusable workflow(workflow_call)パターンを使い、PRで変更されたコンポーネントのワークフローだけを発火させる構成を実際に構築・検証します。絞れるのは発火までで、発火したジョブはそのコンポーネントをまるごとフルスキャンします。

この記事が扱うのはGitHub Actionsの構成だけです。 検出結果の読み方・ポリシーの見え方は前回の記事、手元でのスキャンはSCANOSS CLIの記事、コミットの時点で止める構成はpre-commitの記事にあります。また、この記事はコンポーネントごとにワークフローを分ける構成を前提にしています。スキャンをコンポーネント単位に分けるかどうかそのものは扱いません。

この記事でわかること:

  • モノレポでscanPathが抱える制約と解決策
  • reusable workflow(workflow_call)による共通スキャンワークフローの作り方
  • scanoss.jsonをどこに置き、中のパスをどの基準で書くか
  • パスフィルターとの連携で、変更コンポーネントのワークフローだけを発火させる構成
  • ⚠️ 絞れるのは発火までで、1ファイルの変更でもそのコンポーネントは全体がスキャンされること
  • ⚠️ scanMode: deltaを足すと、この分割が壊れる理由(モノレポ固有の罠)
  • 構築時のハマりポイント(permissionsの配置)と解決策
  • コンポーネントごとにCheck Runが立つ実物と、コミットの時点で止めたい場合の行き先

前提条件

  • 前回の記事の内容を理解していること(基本スキャン、scanPathの動作)
  • GitHubリポジトリにActions実行権限があること
  • リポジトリのSettings > SecretsにSCANOSS_KEYを登録済みであること

本文の例は、メジャーバージョンの移動タグ@v1を指しています。 執筆時点ではv1.7.0(2026-06-25)と同じコミットです。固定したい場合は@v1.7.0のようにパッチまで書いてください。この構成で使う入力(scanPath / api.key)の名前は変わっていません。

検証に使用したリポジトリ構成

本記事の検証には、application配下にbackendfrontendの2コンポーネントを持つモノレポ構成のリポジトリを使用しました。

application/
├── backend/          # バックエンドのコード
└── frontend/         # フロントエンドのコード

この2ディレクトリをスキャン対象として検証します。

モノレポでのスキャン課題

前回の記事で紹介したscanPathパラメータを使えば、スキャン対象をサブディレクトリに限定できます。しかし、scanPath単一の相対パスしか受け付けません。複数パスのカンマ区切りやワイルドカードには対応していません。

# これはできる
scanPath: application/backend

# これはできない
scanPath: application/backend, application/frontend

モノレポで複数パッケージを個別にスキャンするには、ワークフロー側で対応が必要です。SCANOSS公式のMONOREPO_SETUP.mdでは、GitHub Actionsのreusable workflowworkflow_call)を使ったパターンが推奨されています。

ワークフローの全体構成

コンポーネントごとのトリガーワークフローがworkflow_callで共通のベースワークフローを呼ぶ構成図。permissionsは呼び出し元とベースの両方に必要で、呼び出し元に書かないとstartup_failureになる

作成するファイルは以下の3つです。スキャン対象のコンポーネントが増えるたびにトリガーワークフローを1つ追加します。

.github/workflows/
├── scanoss-monorepo-base.yml           # ベースワークフロー(共通、1つだけ)
├── scanoss-monorepo-backend.yml   # backend 用トリガー
└── scanoss-monorepo-frontend.yml     # frontend 用トリガー
ファイル役割
scanoss-monorepo-base.ymlworkflow_callで呼び出される共通スキャン処理。scan_pathを入力として受け取る
scanoss-monorepo-backend.ymlbackendディレクトリの変更時にベースワークフローを呼び出す
scanoss-monorepo-frontend.ymlfrontendディレクトリの変更時にベースワークフローを呼び出す

ベースワークフローの作成

workflow_callで呼び出される共通のスキャンワークフローです。スキャン対象のパスをinputs.scan_pathとして受け取ります。

# .github/workflows/scanoss-monorepo-base.yml
name: SCANOSS Monorepo Base Scan

on:
  workflow_call:
    inputs:
      scan_path:
        description: 'Directory to scan'
        required: true
        type: string

jobs:
  scanoss-scan:
    name: SCANOSS Scan (${{ inputs.scan_path }})
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      checks: write
      actions: read
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run SCANOSS Code Scan
        id: scanoss-scan
        uses: scanoss/gha-code-scan@v1
        with:
          scanPath: ${{ inputs.scan_path }}
          api.key: ${{ secrets.SCANOSS_KEY }}

permissionsジョブレベルに配置しています。workflow_callで呼ばれる側の権限は呼び出し元のpermissionsが上限になるので、ここで広く書いても呼び出し元を超えられません。スキャンするジョブに必要なぶんだけを宣言しておけば十分です。詳細は後述の「ハマりポイント」で説明します。

コンポーネント別トリガーワークフロー

各コンポーネントにつき1つのワークフローファイルを作成し、ベースワークフローを呼び出します。

# .github/workflows/scanoss-monorepo-backend.yml
name: SCANOSS Scan - backend

on:
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write
  checks: write
  actions: read

jobs:
  scan:
    uses: ./.github/workflows/scanoss-monorepo-base.yml
    with:
      scan_path: application/backend
    secrets: inherit
# .github/workflows/scanoss-monorepo-frontend.yml
name: SCANOSS Scan - frontend

on:
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write
  checks: write
  actions: read

jobs:
  scan:
    uses: ./.github/workflows/scanoss-monorepo-base.yml
    with:
      scan_path: application/frontend
    secrets: inherit

secrets: inheritでリポジトリのSecretsを呼び出し先に渡します。これにより、ベースワークフロー側で${{ secrets.SCANOSS_KEY }}が使えます。

まずはこのworkflow_dispatch(手動実行)の構成で動作確認した後、次のセクションでパスフィルターを追加します。この時点では、Actionsタブでジョブがsuccessになることだけ確認できれば十分です。コンポーネントごとにCheck Runが立つ実物は、後述の「実行結果」で見ます。

scanoss.jsonはコンポーネントごとに置く

scanoss.jsonscanPathの直下から読まれます。コンポーネントごとにジョブを分けるこの構成では、設定ファイルもコンポーネントごとに置くことになります。リポジトリルートに1枚置いても、scanPathを指定したジョブからは読まれません。

application/
├── backend/
│   ├── scanoss.json      # backend のジョブが読む
│   └── src/
└── frontend/
    ├── scanoss.json      # frontend のジョブが読む
    └── src/

中でパスを書くときの基準は、そのコンポーネントから見た相対パスです。 scanPath: application/backendのジョブでは、スキャン結果に出るキーがsrc/app.pyになります(application/backend/から数えません)。bomのルールもこの基準で書きます。

{
  "bom": {
    "remove": [
      { "path": "src/app.py", "purl": "pkg:github/owner/repo" }
    ]
  }
}

リポジトリルートからのパスで書くと当たりません。 application/backend/src/app.pyと書いても、結果のキーはsrc/app.pyなので一致しません。エラーにはならず、除外や宣言が黙って効かないまま進みます

パスフィルターとの連携

この構成の最大の利点は、パスフィルターon.pull_request.paths)との組み合わせです。トリガーワークフローにpathsを追加するだけで、PRで変更されたコンポーネントだけスキャンを発火させることができます。

# .github/workflows/scanoss-monorepo-backend.yml
name: SCANOSS Scan - backend

on:
  pull_request:
    paths:
      - 'application/backend/**'
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write
  checks: write
  actions: read

jobs:
  scan:
    uses: ./.github/workflows/scanoss-monorepo-base.yml
    with:
      scan_path: application/backend
    secrets: inherit

変更点はon:セクションにpull_request.pathsを追加しただけです。workflow_dispatchも残しておけば、手動実行も引き続き可能です。

この構成により、以下の動作になります。

PRの変更内容backend スキャンfrontend スキャン
backend/src/main.pyを修正実行される実行されない
frontend/src/index.tsを修正実行されない実行される
両方を修正実行される実行される
docs/のみ修正実行されない実行されない

この表は「PRの変更内容」で見た場合の動作です。 pull_requestpathsはそのpushの差分ではなく、PRのbase…head全体の差分で評価されます。そのため、最初はbackendだけ変更していたPRに、途中でfrontendの変更を追加すると、以降そのPRではbackendも毎回発火するようになります(backendを1文字も触っていないpushでも同様です)。一度両方に触れたPRは、以降は両方のスキャンが走るものとして扱ってください。

モノレポでコンポーネントが増えるほど、全パッケージをフルスキャンすると不要なAPI呼び出しと待ち時間が発生します。パスフィルターを使えば、PRで触ったコンポーネントのワークフローだけが発火するので、レビューのフィードバックが速くなり、API利用量も抑えられます。

ただし、絞れるのは発火までです。 発火したジョブは自分のscanPathまるごとフルスキャンします。そのコンポーネントの中で1ファイルだけ変更したPRでも、スキャンされるのは全体です。大きなコンポーネントほど、この構成で得られる軽さは頭打ちになります。

scanMode: deltaを足すと、この分割が壊れる

パスフィルターで「変更されたコンポーネントだけ発火」までできたので、次はスキャンの中身も変更ファイルだけに絞りたくなります。ActionにはscanMode: deltaという入力があり、PRで変更されたファイルだけをスキャンしてくれます。

この構成には足さないでください。 deltaはscanPathを尊重しないので、コンポーネントごとに分けたことが無意味になります。

deltaの動きはこうです。GitHub APIから変更ファイルの一覧を取り、それを作業ディレクトリ(delta-xxxxxxxx/)へコピーして、そのディレクトリを丸ごとスキャンします。コピーする対象をscanPathで絞る処理はありません。scan_pathに何を渡していても、そのPRで変更されたファイル全部が対象になります。

それでもCheck RunはscanPathを名乗ります。 Policy Check: Copyleft - application/backendという名前で立ち、summaryにも(📁 application/backend)と出ます。名前が示す範囲と、実際に見た範囲がずれます。

コンポーネントごとにscanoss.jsonを置いている場合は実害になります。scanoss.jsonの読み込み位置はscanPath基準のままなので、backendのジョブがfrontendの変更ファイルをbackend側のscanoss.jsonbom.include)で判定します

  • Copyleftは宣言に依存しないので結論は変わらない(見ているのは検出されたコンポーネントのライセンス)
  • Undeclaredは宣言に依存するので、隣のコンポーネントで宣言済みのものが「未宣言」として出ます
  • さらに結果に出るパスの基準も変わりますfullscanPath相対、deltaはリポジトリ相対)。bomのルールをパスで書いていると、モードによって書き分けが必要になります

観察モードで回している間は「件数が多いな」で済んでしまい、ゲートを固めた瞬間に誤ブロックとして出てきます

変更で絞る役は、すでにパスフィルターが持っています。 この構成でdeltaを足す意味はありません。

deltaそのものの挙動(対応するイベント、削除されたファイルの扱い、ブランチを切った最初のpushで必ず落ちること)は前回の記事で扱っています。

ハマりポイント:permissionsは呼び出し元に書く

呼び出し元にpermissionsを書かないと落ちる

構築時にいちばん手が止まったのがここです。呼び出し元ワークフローにpermissionsがないと、startup_failureで失敗します。startup_failureはログが一切生成されないため、原因の特定に手間取ります。

落ちるかどうかを決めているのは呼び出し元だけです。 ベースワークフロー側のpermissionsは、トップレベルに置いてもジョブレベルに置いてもworkflow_callは成功します。

reusable workflowでは、呼び出し元のpermissionsが上限となります。呼び出し元にpermissionsを書かない場合はリポジトリのデフォルト権限が適用されますが、これがSCANOSS Code Scan Actionの必要とする権限(checks: write等)に届かない場合にstartup_failureが発生します。

この既定の権限は、リポジトリのSettings > Actions > General > Workflow permissionsで確認できます。 既定が「Read repository contents permission」(読み取りのみ)のリポジトリでは不足して落ちますが、「Read and write permissions」に設定済みのリポジトリでは最初から足りているため、このハマりポイントを踏みません。手元のリポジトリで再現するかどうかは、まずここを見て判断してください。

解決策

呼び出し元ワークフローにpermissionsを明示的に記述してください。

  1. 呼び出し元ワークフローにpermissions明示的に記述する
  2. ベースワークフローのpermissionsジョブレベルに配置する(スキャンするジョブだけに権限が付くので、ワークフローに他のジョブを足したときに広がらない)

本記事のワークフロー例はすべてこの対応済みの構成です。

実行結果

各コンポーネントが独立したワークフローランとして実行されます。GitHub Actions UIでは別々のワークフローとして並ぶので、どのコンポーネントのスキャンが失敗したかが一目でわかります。

ポリシーチェックを有効にしている場合は、Check Runもコンポーネントごとに立ちます。 2つのコンポーネント(application/backend / application/frontend)に分けたリポジトリで、backend側にコピーレフトが混ざっている状態だとこう出ます。

Policy Check: Copyleft - application/backend   conclusion: neutral
  ### Copyleft Licenses
   | Component | License | URL | Copyleft |
   | - | :-: | - | - |
   | pkg:pypi/mysqlclient | GPL-2.0-or-later | https://spdx.org/licenses/GPL-2.0-or-later.html | YES |

Policy Check: Copyleft - application/frontend  conclusion: success
  ### :white_check_mark: Policy Pass (📁 `application/frontend`)
  #### No copyleft licenses were found

どのコンポーネントで出たのかが名前で分かります。 混入があったものだけを見ればよく、残りはsuccessのまま並びます(neutralは観察モードでの見え方です。切り替えは前回の記事で扱っています)。

実行時間を目安として持たないでください。 対象の量とAPIの応答で動きます。コンポーネントごとに分けたぶん1本あたりは軽くなり、しかも並走するので、コンポーネントが増えても待ち時間はそのぶん伸びません。

コンポーネントの追加方法

新しいパッケージ(例: admin)をスキャン対象に追加する場合、トリガーワークフローを1つ作成するだけです。ベースワークフローの変更は不要です。

# .github/workflows/scanoss-monorepo-admin.yml
name: SCANOSS Scan - admin

on:
  pull_request:
    paths:
      - 'application/admin/**'
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write
  checks: write
  actions: read

jobs:
  scan:
    uses: ./.github/workflows/scanoss-monorepo-base.yml
    with:
      scan_path: application/admin
    secrets: inherit

コピーしてscan_pathpathsを書き換えるだけなので、追加コストは低いです。

ただし、足し忘れは何も知らせてくれません。 この構成はコンポーネントを1つずつ登録していく形なので、トリガーワークフローを作らなかったものは、エラーも警告も出ないままスキャンの対象から外れ続けます。Check Runが立たないだけなので、PRの画面を見ても欠けに気づけません。コンポーネントを追加する手順そのものに、このワークフローの追加を組み込んでおいてください。

まとめ

SCANOSS GitHub Actionsのreusable workflowパターンを使い、モノレポでコンポーネント別にスキャンを実行する構成を紹介しました。

項目ポイント
scanPathの制約単一の相対パスのみ。複数パスやワイルドカードは非対応
公式推奨パターンreusable workflow(workflow_call)でベースワークフローを共通化
scanoss.jsonscanPathの直下に置く。中のパスはそのコンポーネントから見た相対
パスフィルター連携on.pull_request.pathsで変更コンポーネントだけスキャンを発火。絞れるのは発火までで、スキャンはコンポーネントまるごと
permissions呼び出し元に明示的に記述する(無いとstartup_failure)。ベースワークフローはジョブレベル
scanMode: delta足さない。 scanPathを無視するのでコンポーネントごとの分割が壊れ、Undeclaredが誤検出する
Check Runコンポーネントごとに立ち、名前にscanPathが付く。どこで出たかが名前で分かる
コンポーネント追加トリガーワークフローを1ファイル追加するだけ

コミットの時点で止めたい場合は、同じ「コンポーネントごとの振り分け」をpre-commitでやる構成を「SCANOSS pre-commit:コードスキャンでコピーレフト混入を検出する」で解説しています。CIのパスフィルターに当たる役を、pre-commitのfiles:が担います。

参考資料

関連リンク

シリーズ記事

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。
エンジニア募集中!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です