0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub Actionsの成功表示と外部保存を切り分ける

0
Last updated at Posted at 2026-10-12

GitHubで管理しているMarkdownを、レビュー後にWebサービスへ下書き保存する仕組みを作っています。PRをmainへマージするとGitHub Actionsが起動し、Playwrightでエディターを操作する構成です。

この仕組みで確認したのが、Workflowが成功していても、外部サービスへの保存処理を実行していない場合があるということでした。

最初の実行は、記事の送信フラグが無効だったため対象0件で終了していました。フラグを有効にすると保存ステップへ進みましたが、今度は認証状態の読み込みで停止しました。

整理すると、次の2つは別の問題です。

確認した状態 その時点で分かったこと
対象0件、保存ステップがskipped、Workflowはsuccess 保存対象が選ばれておらず、ブラウザー操作を実行していない
対象1件、保存ステップがfailure、AUTH_CONFIG 保存対象は選ばれたが、認証設定を読み込めずブラウザー起動前に停止した

この記事では、Actionsの一覧だけで判断せず、対象選択、ステップの実行、認証、外部保存を順番に確認した流れをまとめます。GitHub ActionsからPlaywrightや外部APIを実行し、「緑なのに反映されていない」を調べる方を想定しています。

記事を置いただけでは保存対象にならなかった

今回の記事管理では、frontmatterに下書き保存用のフラグがあります。

---
title: "動作確認用の記事"
slug: example-draft
status: draft
upload: false
note_id: null
---

status: draftは記事の状態、upload: trueは下書きを外部サービスへ保存してよいことを表します。書きかけの原稿が意図せず送られないよう、執筆時はupload: falseにしています。

対象選択スクリプトは、マージされたPRの変更ファイルから記事を読み、そのうちuploadが有効なものだけを選びます。結果の件数をGITHUB_OUTPUTへ書き、後続ステップの実行条件に使っています。

const articles = (await loadArticles(files)).filter(a => a.upload);

await writeFile(
  'targets.json',
  JSON.stringify(articles.map(a => a.file)) + '\n',
);
await appendFile(process.env.GITHUB_OUTPUT!, `count=${articles.length}\n`);
console.log(`Selected ${articles.length} draft(s).`);

Workflow側は、この件数が0以外のときだけChromiumのインストールと保存処理を行います。

- name: Select requested articles
  id: targets
  run: npm run note:targets

- name: Install Chromium
  if: steps.targets.outputs.count != '0'
  run: npx --no-install playwright install --with-deps chromium

- name: Save drafts
  if: steps.targets.outputs.count != '0'
  env:
    NOTE_STORAGE_STATE: ${{ secrets.NOTE_STORAGE_STATE }}
    NOTE_USERNAME: ${{ vars.NOTE_USERNAME }}
    GH_TOKEN: ${{ github.token }}
  run: npm run note:upload -- --files targets.json

これは実装から抜き出して名前を一部一般化した例です。イベント条件やcheckoutなどは省略しています。

最初の実行では、形式チェックと対象選択は成功していました。一方、Chromiumのインストールと下書き保存は両方skippedでした。記事のupload: falseと対象選択の条件を照合すると、保存されなかった理由を説明できます。

対象0件を正常終了にする設計自体には理由があります。執筆途中の記事や保存しない変更をマージするたびにエラーを出す必要はありません。ただし、保存を期待している実行なら、その正常終了を保存成功と読み替えないことが必要でした。

最初に変えたのは、対象フラグの1行

下書き連携の対象にする修正は、frontmatterの1行だけでした。

-upload: false
+upload: true

ここで本文やブラウザー操作コードまで変更する必要はありません。保存ステップがまだ実行されていないため、エディターのセレクターや保存ボタンを調べても、今回のスキップ理由には到達しません。

対象フラグを変更したPRでは、形式チェックとモックテストが通りました。マージ後の実行ではChromiumのインストールも成功し、保存ステップへ進むようになりました。

ただし、保存まで完了したわけではありませんでした。

対象1件になった後、認証設定で停止した

次に確認したログは、ブラウザーを起動する前のstartupで止まっていました。個人情報を含む箇所を除くと、重要なのは次の値です。

{"stage":"startup","code":"VALIDATED","selected":1,"skipped":0}
{"stage":"startup","code":"AUTH_CONFIG","message":"Set NOTE_STORAGE_STATE secret or run npm run note:auth locally."}

前の実行との違いは、保存対象が1件選ばれていることです。対象選択の問題は解消しましたが、Playwrightへ渡す認証状態を読み込めていませんでした。

読み込み部分は、概ね次の順序です。

let storageState;

try {
  storageState = JSON.parse(
    process.env.NOTE_STORAGE_STATE
      || await readFile('.auth/note.json', 'utf8'),
  );

  if (!Array.isArray(storageState.cookies)
      || !Array.isArray(storageState.origins)) {
    throw new Error();
  }
} catch {
  throw new Error('AUTH_CONFIG');
}

const browser = await chromium.launch();
const context = await browser.newContext({ storageState });

GitHub ActionsではSecretを環境変数へ渡し、ローカルでは専用の認証ファイルを読む実装です。認証ファイルはGit管理から除外しています。

この例では、環境変数が空、ローカルファイルが存在しない、JSONが不正、想定する配列がない、といった失敗を同じAUTH_CONFIGで扱っています。そのため、ログにAUTH_CONFIGがあるだけで、Secret未登録と一意に断定することはできません。

次に調べるのは、GitHubのSecret設定、値の受け渡し、JSON形式です。セッションの期限切れやエディターのUI変更とは分けます。今回のコードではAUTH_CONFIGがブラウザー起動より前に出るので、実ページへアクセスして拒否されたエラーではありません。

Secretの登録とログイン成立も別に確認する

GitHubの公式資料では、未設定のSecretを参照する式は空文字列になります。Workflowに${{ secrets.NOTE_STORAGE_STATE }}と書いてあることは、実行時に値を使えることの証明にはなりません。

確認するときは、値を出力するのではなく、名前、保存先、Workflowからの参照、期待するJSON形式を照合します。EnvironmentのSecretを使う構成なら、そのジョブが該当Environmentを利用する設定かも確認します。

この実装で必要なのは、手動ログイン済みのブラウザーから取得したPlaywrightのstorage stateです。パスワードそのものをActionsへ渡す方式にはしていません。

登録は、手元で作成したファイルを標準入力から渡せます。

gh secret set NOTE_STORAGE_STATE \
  --repo OWNER/REPOSITORY < .auth/note.json

OWNER/REPOSITORYは説明用の置き換えです。JSONはbase64化せず登録する前提で、認証ファイルの中身を記事やログへ貼りません。

また、cookiesとoriginsが配列になっているだけでは、ログインが有効だとは分かりません。空配列でも構造チェックは通りますし、保存したCookieの期限が切れている場合もあります。

現在の実装には、記事を新規作成せずログイン先だけを調べる--check-sessionがあります。ローカルで認証設定を用意した後は、この確認でアカウントとログイン状態を照合し、その後に対象1件の下書き保存を確認する順序になります。

形式検査の成功を、実保存の成功にしない

この仕組みでは、PRの検証用Workflowと、マージ後の保存用Workflowを分けています。

PRのチェックはMarkdownの形式、対象パス、模擬画面での処理などを確認します。実アカウントのSecretsを渡して外部サービスへ保存するものではありません。

したがって、次の段階はそれぞれ違う証拠で確認します。

段階 確認する証拠
原稿とコードを検証できた 形式チェック、モックテストの結果
保存する原稿が選ばれた 対象件数と対象パス
保存処理が実行された 保存ステップがskippedではないこと
実アカウントでログインできた セッション・アカウント確認の結果
下書きを保存できた 保存後に再読込したタイトル・本文・下書き状態

今回、実行履歴では最初の保存ステップがスキップされたこと、次の実行はselected: 1の後にAUTH_CONFIGで終了コード1になったことを確認しました。対象フラグを有効にしたPRの差分も1行だけでした。

記事用の最小例では、対象0件と1件、空の認証設定、不正JSON、必要な配列を欠くJSONをローカルで確認しています。空の配列を持つJSONが形式検査に通ることも確認し、形式検査とログイン成立の違いを残しました。実際のCookie、アカウント、外部サービスへの送信は使っていません。

再発を減らすなら、実行結果に件数を残す

今後の改善案は、ActionsのSummaryへ「選択件数」「保存完了件数」「スキップした理由」をまとめることです。これは今回確認した実装への提案で、追加済みの機能としては扱っていません。

対象0件を失敗にするかは、Workflowの用途によります。通常のマージで未送信の記事が含まれることを許すなら、0件は正常終了で構いません。一方、「この1件を保存する」と明示した手動実行なら、対象0件をエラーにする設計も考えられます。

どちらの場合も、成功・失敗の色だけで外部への反映を判断せず、何件を選び、どこまで実行し、何を保存後に確認したかを残すと、調査の入口が分かりやすくなります。

まとめ

今回の調査では、最初に対象選択を確認したことで、保存ステップを実行していない理由が分かりました。対象フラグを有効にした後は、停止地点が認証設定へ進んだこともログから確認できました。

Workflowの成功、形式検査の成功、ブラウザー起動、ログイン、外部保存は、それぞれ別の到達点です。反映されていないときは、期待した保存ステップが実行されたかを最初に確認し、対象件数と停止段階から調べると、まだ動いていない処理を修正せずに済みます。

参考

公式資料は2026年10月12日に確認しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?