GitHub Actionsで複数行出力がGITHUB_OUTPUTに書けないときdelimiterと安全な受渡しを確認する

GitHub Actionsのstep間で複数行のJSONや生成文を渡すとき、GITHUB_OUTPUTには単純な1行のNAME=value形式だけでは値の境界を表せません。

AIものさし編集部

GitHub Actionsのstep間で複数行のJSONや生成文を渡すとき、GITHUB_OUTPUTには単純な1行のNAME=value形式だけでは値の境界を表せません。公式仕様は複数行値にdelimiter形式を示していますが、値の中にdelimiterが単独行で現れる可能性があるなら、その形式を使わずファイルへ書くよう注意しています。まずstep outputで足りる短い値か、内容が任意・大きいデータかを分けます。delimiterを使う場合は衝突を検査し、後続stepで値を読み戻して改行・末尾・文字が一致するか確認します。secretや機密本文をoutputやlogへ不用意に載せないでください。

まず受け渡す値の性質を確認する

出力方法は、文字列の改行数だけで決めません。次の点を記録します。

  • 値が固定形式か、任意のモデル・API・ユーザー入力か

  • 最大サイズと想定される最大行数

  • newlineがLFかCRLFか、末尾改行を保持する必要があるか

  • 値に任意の一行文字列が含まれ得るか

  • downstreamが同一job内のstepか、別jobか

  • 値にtoken、個人情報、顧客データ、署名付きURL等が含まれるか

  • 値を一時ファイルやartifactとして渡し、必要後に削除できるか

「複数行」というだけならdelimiterで安全に扱えるケースがあります。一方、完全に任意の文字列には、値と同じdelimiterが含まれる可能性を一般には排除できません。その場合はdelimiter形式を使わず、ファイルを保存してpathだけをoutputへ渡す設計が安全です。サイズが大きいデータも、outputへ埋め込むよりfile/artifact等の受け渡しが適しています。

GITHUB_OUTPUTの形式を理解する

GitHub Actionsのworkflow command資料では、複数行の値を次の形で記録する方法を示しています。

NAME<<DELIMITER

複数行の値

DELIMITER

実際にはこのブロックをGITHUB_OUTPUTのfileへ書き、後続stepではsteps.<step-id>.outputs.NAMEとして参照します。通常のNAME=valueは一行値向けです。複数行を含むJSONをその形式へ直接書くと、後続の行が別のenvironment file commandとして解釈されたり、値が切れたり、runnerが不正形式としてstepを失敗させたりする可能性があります。

ここで重要なのは、delimiterが内容の一行と完全一致してはならないことです。GitHub Docsは、delimiterが値の一行として現れる可能性がある場合、この方式を使わずファイルに書くよう明示しています。固定文字列EOFを使う例は読みやすい一方、任意のJSONや生成内容で必ず安全だとはいえません。

delimiterを使う場合の安全な手順

内容が任意ではなく、値サイズも小さく、delimiterが衝突しないことを検査できるときに限り、次の順で設定します。

  1. 出力名を固定し、workflow expressionや入力データをそのままshell codeへ埋め込まない。

  2. 改行・文字コード・末尾改行の期待値を決める。

  3. 十分な長さのランダムなdelimiterを生成する。

  4. 値の各行にdelimiterと完全一致する行がないことを確認する。

  5. GITHUB_OUTPUTへ開始行、値、終了行の順で追記する。

  6. 後続stepで受け取り、期待するparse/schemaと元値の境界を確認する。

  7. delimiter衝突または書き込み失敗があれば、outputを空文字として成功扱いせずstepをfailさせる。

擬似例です。shellごとの安全なquote・改行の扱いは実workflowに合わせてください。

delimiterをランダムにするだけでは十分ではありません。生成後に対象文字列のどこか一行と完全一致する場合は、別のdelimiterを生成するかfile方式へ切り替えます。入力が大きくて行ごと検査しにくい、binary dataを含む、改行をそのまま保持しにくい、といった状況もfile方式を選ぶ理由です。

file/artifactへ切り替える判断

内容が完全に任意、delimiter衝突を安全に除外できない、または値が大きいなら、ファイルへ書いてそのfileを後続処理へ渡します。必要なのが同一jobの後続stepだけなら、一時workspace fileを使える場合があります。別jobへ渡すなら、workflowのjob境界やretention・アクセス権に合うartifact等を検討し、pathだけをstep outputへ渡します。artifactには保存期間・閲覧権限・内容の機密性を確認し、必要がなくなったデータは保持しない設計にします。

file名にユーザー値やAI出力をそのまま使うと、path traversalやshell injection等の別問題が起きます。ファイル名は固定または安全なIDから生成し、書き込み先をworkspace内の想定directoryに限定します。download後は、期待path・サイズ・checksum・JSON schemaを確認してから使います。ファイルへ切り替えても、機密性や権限の問題が自動的に解決するわけではありません。

判断表

条件|推奨する受け渡し|確認

短い1行値|通常のNAME=value|改行なし、文字列のquote

複数行で内容が制御され、delimiter衝突を検査できる|delimiter形式|全行を検査、後続でreadback

API/AIの任意出力で行内容を限定できない|fileまたはartifact|path権限、checksum、cleanup

大きなJSON、長い生成文|fileまたはartifact|サイズ上限、retention、ダウンロード後validation

機密・secretを含む|受渡し自体を再設計|output/logへ露出しない、最小権限、maskだけに依存しない

よくある失敗と確認順

delimiterが見つからない

runnerは開始行のdelimiterと一致する終端行を探します。開始後に同じ行がない、改行・quote・shell expansionの結果が想定と違う、値の最後に改行がない、または終端行が別の文字として書き込まれた場合に、対応する境界を見つけられないことがあります。生の機密値を出力せず、fileの行数、byte数、開始・終了markerの存在、改行形式をテスト用fixtureで確認します。

値の一部が消える

固定delimiterが本文中の単独行として現れると、その行以降が別のoutputとして処理されるなど、意図と異なる境界になり得ます。目視で「珍しい文字列だから大丈夫」とせず、値全体をdelimiter行と比較します。MarkdownやコードにはEOF等の短いmarkerが普通に現れるため、固定delimiterを任意出力に使わないことが大切です。

次のjobでoutputが空

step outputとjob output、environment variable、artifactは用途とscopeが違います。step outputは後続stepから参照できるようstepにidを付け、別jobで使う場合はjob outputへのmappingとneeds関係が必要です。fileはjobをまたぐときに自動共有されるとは限らないため、artifact等で明示的に渡します。単なる参照syntaxの誤りと、delimiter解析失敗を分けて調査します。

secretがログやoutputへ漏れる

GITHUB_OUTPUTへ書いた値が必ず安全に隠されると仮定しないでください。値をechoしてdebugする、command文字列に展開する、後続stepで全値をprintする、といった行為を避けます。GitHubのsecret maskingには適用上の条件があるため、maskを唯一の保護策にせず、secretそのものをworkflow outputに渡す必要があるか検討してください。必要ならsecret store、scoped credential、短命の参照tokenなど、既存の安全な仕組みに切り分けます。

最小fixtureで受け渡しを検証する

変更前に、実際のworkflowではなく隔離したtest workflowまたはscript fixtureで、少なくとも次の入力を試します。

  • 一行の通常値

  • LFの複数行、末尾改行あり・なし

  • CRLFの複数行

  • JSONで閉じ括弧が別行にある値

  • delimiter候補と同じ文字列だけの行を含む値

  • 任意入力にdelimiter collisionがある場合のfile fallback

  • 空文字、空行、Unicode、日本語、絵文字

  • 大きめの値でのfile転送

  • secret markerを含むfixtureで、ログに本文が出ないこと

  • step output、job output、artifactでそれぞれ参照scopeが合うこと

各テストでは、送信側の値と受信側の値を同じ規則で正規化し、正規化後のbyteまたはhashを比較します。意図的に末尾改行を捨てるなら仕様として明記し、暗黙にtrimして一致したように扱わないでください。test fixtureに本番secretや顧客データを入れません。この記事のチェック項目は検証計画であり、ここで実際のGitHub Actions runを実行した結果ではありません。

実装前チェックリスト

  1. 値のサイズ・任意性・機密性・scopeを分類した。

  2. 1行形式かdelimiter形式かfile/artifactかを選んだ。

  3. delimiterを使う場合、ランダム生成後に完全一致行の衝突を調べる。

  4. quoteやshell expansionを含む書き方をworkflowのshellで検証する。

  5. outputのreadbackで改行、空行、Unicode、末尾を比較する。

  6. step間・job間のscopeを区別し、必要なmappingを確認する。

  7. 失敗時に空値を正常扱いせず、後続の確定処理を止める。

  8. secret・AI出力・任意ユーザー値をlogへ出さない。

  9. file/artifactの場合はpath、権限、保存期間、cleanupを確認する。

  10. testと実runの証拠を分けて記録する。

次にすること

失敗が起きたrunで、まずworkflowのshell、GITHUB_OUTPUTへ書いた形式、値の任意性、次のstepが参照するscopeを確認します。値が管理でき、delimiter衝突を検査できる場合は安全なdelimiter方式をfixtureで確認します。AIや外部APIから来る任意内容なら、固定delimiterによる手渡しをやめ、file/artifactへ切り替えてdownload後に検証します。どちらの方法でも、元値と受け取り値の一致、secret露出の有無、失敗時に後続jobが停止することを確認してからworkflowへ反映してください。

Sources & scope

確認日:2026-10-15

GitHub Docs, Workflow commands for GitHub Actions: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands(新しいタブで開く)

GitHub Actions Toolkit, commands.md: https://github.com/actions/toolkit/blob/main/docs/commands.md(新しいタブで開く)

公式資料ではGITHUB_OUTPUTを使ったstep outputの設定、複数行delimiter形式、delimiterが値の一行として現れ得る場合はfileを使う注意を確認しました。runner・shell・toolkitのversionによって実装詳細は異なる可能性があります。security review、artifact権限・retention、実workflowのtestは導入側で必要です。本文のfixtureは計画であり、実際のworkflow実行結果ではありません。