サイオステクノロジーは、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配下にbackendとfrontendの2コンポーネントを持つモノレポ構成のリポジトリを使用しました。
application/
├── backend/ # バックエンドのコード
└── frontend/ # フロントエンドのコード
この2ディレクトリをスキャン対象として検証します。
モノレポでのスキャン課題
前回の記事で紹介したscanPathパラメータを使えば、スキャン対象をサブディレクトリに限定できます。しかし、scanPathは単一の相対パスしか受け付けません。複数パスのカンマ区切りやワイルドカードには対応していません。
# これはできる
scanPath: application/backend
# これはできない
scanPath: application/backend, application/frontend
モノレポで複数パッケージを個別にスキャンするには、ワークフロー側で対応が必要です。SCANOSS公式のMONOREPO_SETUP.mdでは、GitHub Actionsのreusable workflow(workflow_call)を使ったパターンが推奨されています。
ワークフローの全体構成

作成するファイルは以下の3つです。スキャン対象のコンポーネントが増えるたびにトリガーワークフローを1つ追加します。
.github/workflows/
├── scanoss-monorepo-base.yml # ベースワークフロー(共通、1つだけ)
├── scanoss-monorepo-backend.yml # backend 用トリガー
└── scanoss-monorepo-frontend.yml # frontend 用トリガー
| ファイル | 役割 |
|---|---|
scanoss-monorepo-base.yml | workflow_callで呼び出される共通スキャン処理。scan_pathを入力として受け取る |
scanoss-monorepo-backend.yml | backendディレクトリの変更時にベースワークフローを呼び出す |
scanoss-monorepo-frontend.yml | frontendディレクトリの変更時にベースワークフローを呼び出す |
ベースワークフローの作成
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.jsonはscanPathの直下から読まれます。コンポーネントごとにジョブを分けるこの構成では、設定ファイルもコンポーネントごとに置くことになります。リポジトリルートに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_requestのpathsはその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.json(bom.include)で判定します。
- Copyleftは宣言に依存しないので結論は変わらない(見ているのは検出されたコンポーネントのライセンス)
- Undeclaredは宣言に依存するので、隣のコンポーネントで宣言済みのものが「未宣言」として出ます
- さらに結果に出るパスの基準も変わります(
fullはscanPath相対、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を明示的に記述してください。
- 呼び出し元ワークフローに
permissionsを明示的に記述する - ベースワークフローの
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_pathとpathsを書き換えるだけなので、追加コストは低いです。
ただし、足し忘れは何も知らせてくれません。 この構成はコンポーネントを1つずつ登録していく形なので、トリガーワークフローを作らなかったものは、エラーも警告も出ないままスキャンの対象から外れ続けます。Check Runが立たないだけなので、PRの画面を見ても欠けに気づけません。コンポーネントを追加する手順そのものに、このワークフローの追加を組み込んでおいてください。
まとめ
SCANOSS GitHub Actionsのreusable workflowパターンを使い、モノレポでコンポーネント別にスキャンを実行する構成を紹介しました。
| 項目 | ポイント |
|---|---|
| scanPathの制約 | 単一の相対パスのみ。複数パスやワイルドカードは非対応 |
| 公式推奨パターン | reusable workflow(workflow_call)でベースワークフローを共通化 |
| scanoss.json | scanPathの直下に置く。中のパスはそのコンポーネントから見た相対 |
| パスフィルター連携 | on.pull_request.pathsで変更コンポーネントだけスキャンを発火。絞れるのは発火までで、スキャンはコンポーネントまるごと |
| permissions | 呼び出し元に明示的に記述する(無いとstartup_failure)。ベースワークフローはジョブレベル |
scanMode: delta | 足さない。 scanPathを無視するのでコンポーネントごとの分割が壊れ、Undeclaredが誤検出する |
| Check Run | コンポーネントごとに立ち、名前にscanPathが付く。どこで出たかが名前で分かる |
| コンポーネント追加 | トリガーワークフローを1ファイル追加するだけ |
コミットの時点で止めたい場合は、同じ「コンポーネントごとの振り分け」をpre-commitでやる構成を「SCANOSS pre-commit:コードスキャンでコピーレフト混入を検出する」で解説しています。CIのパスフィルターに当たる役を、pre-commitのfiles:が担います。
参考資料
関連リンク
- SCANOSS Code Scan Action(GitHub リポジトリ)
- SCANOSS MONOREPO_SETUP.md(公式モノレポガイド)
- SCANOSS Code Scan Action(GitHub Marketplace)
- GitHub Actions: Reusing workflows
- GitHub Actions: paths フィルター


