はじめに
こんにちは、enechainでeSquare Liveを開発しているエンジニアの古瀬(@tsuperis3112)です!
今回は、マニュアル依存になりがちなデプロイフローの問題を actions/github-script
で解消した方法についてお話します。
eSquare Liveの開発では、効率的かつ信頼性の高い開発フローを維持するために様々なツールを試行錯誤しています。その中で、GitHub Actionsをさらに強化するためにGitHub scriptを導入し、大きな効果を得ることができました。
GitHub Script概要
複雑なCIを設定する時、どのようなスクリプトを書くでしょうか? 例えば、「特定の条件を満たす場合のみ、特定の環境へのデプロイを行う」「複数のブランチの状態を比較し、コンフリクトがないかチェックする」といった処理が必要になるかもしれません。
Github Scriptを利用しない場合、ghコマンドで情報を取得し環境変数に格納、その値をシェルスクリプトでパースして、など複雑な手順を踏む必要があります。
シェルスクリプトは静的解析が難しいため、IDEの補完も限定的にしか利用できず、ハードワークになる可能性が高いです。さらに、bashやzsh、BSD系/GNU系など、ローカル環境とCI環境でコマンドの挙動に差分が発生する可能性もあり、予期せぬエラーに悩まされることも少なくありません。
Github Scriptを利用する場合、JavaScriptで実装できるため、以下のようなメリットがあります。
- 複雑な処理を比較的簡単に記述できる: JavaScriptの強力な制御構文(条件分岐やループ)により、複雑なロジックも簡単に実装できます。
- 型システムの恩恵を受けられる: JSDocやTypeScriptの型定義を利用することで、IDEの強力なコード補完や、ESLintなどのリンターによる静的解析のサポートを受けられます。これにより、タイプミスや型エラーを減らし、開発効率とコード品質を向上させることができます。
- テストが容易: JestやVitestなどのテスティングフレームワークを用いて、GitHub Scriptのロジックに対してユニットテストを記述できます。これにより、バグの早期発見やリファクタリングの容易化、CI/CDパイプラインへの信頼性向上が期待できます。
- JavaScriptエコシステムの活用: npmで公開されている豊富なライブラリを活用することで、開発を効率化できます。
以下のactionはPull Requestのベースブランチがmainかそれ以外かを判定するためのスクリプトです。
actions/github-script
を利用することで、GitHub APIの操作やイベント(Pull Request、Workflow Dispatchなど)のコンテキストを簡単に取得・活用できます。
steps: - uses: actions/github-script@v7 id: myscript with: result-encoding: string script: | const branch = context.payload.pull_request?.base === 'main' ? 'main' : 'other' return branch - uses: sample-action with: branch: {{ steps.myscript.outputs.result }}
uses
にactions/github-script
を指定し、with
にresult-encoding
としてstring
またはjson
を指定し、script
に実行するJavaScriptを記述します。
result-encoding
はデフォルトではjson
ですが、使い勝手を考慮してここではstring
を使用しています。公式の例でもstring
が多く使用されています。
context
には現在のイベントに含まれるコンテキスト情報が含まれており、イベント別に様々な情報を取得できます。Pull Requestの場合、ベースブランチ、ターゲットブランチ、作成者、PRメッセージなど多くの情報を取得可能です。
最終的に戻り値として出力した内容が.outputs.result
に格納され、以降のステップやジョブで利用できるようになります。
セットアップ
GitHub Scriptで記述する言語はJavaScriptのためGitHub特有のオブジェクトや型の知識を持ちません。しかしJavaScriptはJSDocを使用することで、型情報をIDE(e.g. VSCode, IntelliJ IDEA)に認識させることが可能です。
そこでまず、GitHub Scriptの型情報をインストールし、それを設定したアクションファイルを作成します。
npm install -D @types/github-script
.github/actions/action.js
1
型情報をJSDocに記述することでIDEが引数の型を認識します。
/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ module.exports = async ({ github, core, context }) => { return "action"; };
.github/workflows/ci.js
Node.jsではデフォルトのモジュールシステムがcjsなので、requireで実行ファイルのインポートを行います。
name: "Sample CI" on: push: workflow_dispatch: jobs: set-env: runs-on: ubuntu-latest outputs: target: ${{ steps.script.outputs.result }} steps: - uses: actions/checkout@v4 - uses: actions/github-script@v7 id: script with: script: | # action.js呼び出し # リポジトリルートをカレントディレクトリとしている const script = require('./.github/actions/action.js') return await script({github, core, context}) result-encoding: string ...snip...
これで最小限のアクションが完成しました。この空のアクションに処理を追加していき、CI/CDに関するデプロイ作業を自動化していきましょう!
script関数の引数はそれぞれ以下のように定義されています。
引数 | 説明 |
---|---|
github | GitHub の API を操作します |
core | Action の成否を決めたり、ログの出力などを行います |
context | 現在のイベントやワークフローの情報が含まれています。情報の参照をする場合はここをみることが多いです。 |
ほとんどのケースではサンプルの通りgithub
, core
, context
を扱うことで十分な操作ができます。
context の中身
実際にどんな値が入っているのかをconsole.log(JSON.stringify(context))
して確認しました。基本的な情報が豊富に含まれているため、詳細はそれぞれ開いてご確認ください。
Push
{ "payload": { "after": "b042efbe914c0a84146cb39d1a44d1c9f1511321", "base_ref": null, "before": "d6e674261c20f08c2ac841f12034f7034aff6a8b", "commits": [ { "author": { "email": "[email protected]", "name": "Takeru Furuse", "username": "tsuperis3112" }, "committer": { "email": "[email protected]", "name": "Takeru Furuse", "username": "tsuperis3112" }, "distinct": true, "id": "b042efbe914c0a84146cb39d1a44d1c9f1511321", "message": "fix", "timestamp": "2024-12-24T17:23:15+09:00", "tree_id": "50fc2599903d7d6d82b1cb0cbed8fa9686b5eb02", "url": "https://github.com/tsuperis3112/samplerepo/commit/b042efbe914c0a84146cb39d1a44d1c9f1511321" } ], "compare": "https://github.com/tsuperis3112/samplerepo/compare/d6e674261c20...b042efbe914c", "created": false, "deleted": false, "forced": false, "head_commit": { "author": { "email": "[email protected]", "name": "Takeru Furuse", "username": "tsuperis3112" }, "committer": { "email": "[email protected]", "name": "Takeru Furuse", "username": "tsuperis3112" }, "distinct": true, "id": "b042efbe914c0a84146cb39d1a44d1c9f1511321", "message": "fix", "timestamp": "2024-12-24T17:23:15+09:00", "tree_id": "50fc2599903d7d6d82b1cb0cbed8fa9686b5eb02", "url": "https://github.com/tsuperis3112/samplerepo/commit/b042efbe914c0a84146cb39d1a44d1c9f1511321" }, "pusher": { "email": "[email protected]", "name": "tsuperis3112" }, "ref": "refs/heads/main", "repository": { "allow_forking": true, "archive_url": "https://api.github.com/repos/tsuperis3112/samplerepo/{archive_format}{/ref}", "archived": false, "assignees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/assignees{/user}", "blobs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/blobs{/sha}", "branches_url": "https://api.github.com/repos/tsuperis3112/samplerepo/branches{/branch}", "clone_url": "https://github.com/tsuperis3112/samplerepo.git", "collaborators_url": "https://api.github.com/repos/tsuperis3112/samplerepo/collaborators{/collaborator}", "comments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/comments{/number}", "commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/commits{/sha}", "compare_url": "https://api.github.com/repos/tsuperis3112/samplerepo/compare/{base}...{head}", "contents_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contents/{+path}", "contributors_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contributors", "created_at": 1728260001, "default_branch": "main", "deployments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/deployments", "description": null, "disabled": false, "downloads_url": "https://api.github.com/repos/tsuperis3112/samplerepo/downloads", "events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/events", "fork": false, "forks": 0, "forks_count": 0, "forks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/forks", "full_name": "tsuperis3112/samplerepo", "git_commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/commits{/sha}", "git_refs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/refs{/sha}", "git_tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/tags{/sha}", "git_url": "git://github.com/tsuperis3112/samplerepo.git", "has_discussions": false, "has_downloads": true, "has_issues": true, "has_pages": false, "has_projects": true, "has_wiki": false, "homepage": null, "hooks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/hooks", "html_url": "https://github.com/tsuperis3112/samplerepo", "id": 868662533, "is_template": false, "issue_comment_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/comments{/number}", "issue_events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/events{/number}", "issues_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues{/number}", "keys_url": "https://api.github.com/repos/tsuperis3112/samplerepo/keys{/key_id}", "labels_url": "https://api.github.com/repos/tsuperis3112/samplerepo/labels{/name}", "language": "C++", "languages_url": "https://api.github.com/repos/tsuperis3112/samplerepo/languages", "license": null, "master_branch": "main", "merges_url": "https://api.github.com/repos/tsuperis3112/samplerepo/merges", "milestones_url": "https://api.github.com/repos/tsuperis3112/samplerepo/milestones{/number}", "mirror_url": null, "name": "samplerepo", "node_id": "R_kgDOM8a9BQ", "notifications_url": "https://api.github.com/repos/tsuperis3112/samplerepo/notifications{?since,all,participating}", "open_issues": 0, "open_issues_count": 0, "owner": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "email": "[email protected]", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "name": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" }, "private": true, "pulls_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls{/number}", "pushed_at": 1735028599, "releases_url": "https://api.github.com/repos/tsuperis3112/samplerepo/releases{/id}", "size": 226, "ssh_url": "[email protected]:tsuperis3112/samplerepo.git", "stargazers": 0, "stargazers_count": 0, "stargazers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/stargazers", "statuses_url": "https://api.github.com/repos/tsuperis3112/samplerepo/statuses/{sha}", "subscribers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscribers", "subscription_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscription", "svn_url": "https://github.com/tsuperis3112/samplerepo", "tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/tags", "teams_url": "https://api.github.com/repos/tsuperis3112/samplerepo/teams", "topics": [], "trees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/trees{/sha}", "updated_at": "2024-12-24T08:22:31Z", "url": "https://github.com/tsuperis3112/samplerepo", "visibility": "private", "watchers": 0, "watchers_count": 0, "web_commit_signoff_required": false }, "sender": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" } }, "eventName": "push", "sha": "b042efbe914c0a84146cb39d1a44d1c9f1511321", "ref": "refs/heads/main", "workflow": "Deploy: Excel Add-in", "action": "script", "actor": "tsuperis3112", "job": "set-env", "runNumber": 4, "runId": 12479116463, "apiUrl": "https://api.github.com", "serverUrl": "https://github.com", "graphqlUrl": "https://api.github.com/graphql" }
{ "payload": { "action": "synchronize", "after": "bafe574499f02df1f38234e29ea6b411f21da12d", "before": "5993b1c07e0e2182b260df23ca3cc28ef47b555b", "number": 1, "pull_request": { "_links": { "comments": { "href": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/1/comments" }, "commits": { "href": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/123/commits" }, "html": { "href": "https://github.com/tsuperis3112/samplerepo/pull/1" }, "issue": { "href": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/1" }, "review_comment": { "href": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/comments{/number}" }, "review_comments": { "href": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/123/comments" }, "self": { "href": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/123" }, "statuses": { "href": "https://api.github.com/repos/tsuperis3112/samplerepo/statuses/bafe574499f02df1f38234e29ea6b411f21da12d" } }, "active_lock_reason": null, "additions": 2, "assignee": null, "assignees": [], "author_association": "OWNER", "auto_merge": null, "base": { "label": "tsuperis3112:main", "ref": "main", "repo": { "allow_auto_merge": false, "allow_forking": true, "allow_merge_commit": true, "allow_rebase_merge": true, "allow_squash_merge": true, "allow_update_branch": false, "archive_url": "https://api.github.com/repos/tsuperis3112/samplerepo/{archive_format}{/ref}", "archived": false, "assignees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/assignees{/user}", "blobs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/blobs{/sha}", "branches_url": "https://api.github.com/repos/tsuperis3112/samplerepo/branches{/branch}", "clone_url": "https://github.com/tsuperis3112/samplerepo.git", "collaborators_url": "https://api.github.com/repos/tsuperis3112/samplerepo/collaborators{/collaborator}", "comments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/comments{/number}", "commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/commits{/sha}", "compare_url": "https://api.github.com/repos/tsuperis3112/samplerepo/compare/{base}...{head}", "contents_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contents/{+path}", "contributors_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contributors", "created_at": "2024-10-07T00:13:21Z", "default_branch": "main", "delete_branch_on_merge": false, "deployments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/deployments", "description": null, "disabled": false, "downloads_url": "https://api.github.com/repos/tsuperis3112/samplerepo/downloads", "events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/events", "fork": false, "forks": 0, "forks_count": 0, "forks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/forks", "full_name": "tsuperis3112/samplerepo", "git_commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/commits{/sha}", "git_refs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/refs{/sha}", "git_tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/tags{/sha}", "git_url": "git://github.com/tsuperis3112/samplerepo.git", "has_discussions": false, "has_downloads": true, "has_issues": true, "has_pages": false, "has_projects": true, "has_wiki": false, "homepage": null, "hooks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/hooks", "html_url": "https://github.com/tsuperis3112/samplerepo", "id": 868662533, "is_template": false, "issue_comment_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/comments{/number}", "issue_events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/events{/number}", "issues_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues{/number}", "keys_url": "https://api.github.com/repos/tsuperis3112/samplerepo/keys{/key_id}", "labels_url": "https://api.github.com/repos/tsuperis3112/samplerepo/labels{/name}", "language": "C++", "languages_url": "https://api.github.com/repos/tsuperis3112/samplerepo/languages", "license": null, "merge_commit_message": "PR_TITLE", "merge_commit_title": "MERGE_MESSAGE", "merges_url": "https://api.github.com/repos/tsuperis3112/samplerepo/merges", "milestones_url": "https://api.github.com/repos/tsuperis3112/samplerepo/milestones{/number}", "mirror_url": null, "name": "samplerepo", "node_id": "R_kgDOM8a9BQ", "notifications_url": "https://api.github.com/repos/tsuperis3112/samplerepo/notifications{?since,all,participating}", "open_issues": 1, "open_issues_count": 1, "owner": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" }, "private": true, "pulls_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls{/number}", "pushed_at": "2024-12-24T08:49:19Z", "releases_url": "https://api.github.com/repos/tsuperis3112/samplerepo/releases{/id}", "size": 226, "squash_merge_commit_message": "COMMIT_MESSAGES", "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", "ssh_url": "[email protected]:tsuperis3112/samplerepo.git", "stargazers_count": 0, "stargazers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/stargazers", "statuses_url": "https://api.github.com/repos/tsuperis3112/samplerepo/statuses/{sha}", "subscribers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscribers", "subscription_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscription", "svn_url": "https://github.com/tsuperis3112/samplerepo", "tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/tags", "teams_url": "https://api.github.com/repos/tsuperis3112/samplerepo/teams", "topics": [], "trees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/trees{/sha}", "updated_at": "2024-12-24T08:23:56Z", "url": "https://api.github.com/repos/tsuperis3112/samplerepo", "use_squash_pr_title_as_default": false, "visibility": "private", "watchers": 0, "watchers_count": 0, "web_commit_signoff_required": false }, "sha": "8422d518823c8ff55beb7d57d74bc81dee3aa99e", "user": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" } }, "body": null, "changed_files": 1, "closed_at": null, "comments": 0, "comments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/1/comments", "commits": 2, "commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/123/commits", "created_at": "2024-12-24T08:48:34Z", "deletions": 4, "diff_url": "https://github.com/tsuperis3112/samplerepo/pull/1.diff", "draft": false, "head": { "label": "tsuperis3112:tmp", "ref": "tmp", "repo": { "allow_auto_merge": false, "allow_forking": true, "allow_merge_commit": true, "allow_rebase_merge": true, "allow_squash_merge": true, "allow_update_branch": false, "archive_url": "https://api.github.com/repos/tsuperis3112/samplerepo/{archive_format}{/ref}", "archived": false, "assignees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/assignees{/user}", "blobs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/blobs{/sha}", "branches_url": "https://api.github.com/repos/tsuperis3112/samplerepo/branches{/branch}", "clone_url": "https://github.com/tsuperis3112/samplerepo.git", "collaborators_url": "https://api.github.com/repos/tsuperis3112/samplerepo/collaborators{/collaborator}", "comments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/comments{/number}", "commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/commits{/sha}", "compare_url": "https://api.github.com/repos/tsuperis3112/samplerepo/compare/{base}...{head}", "contents_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contents/{+path}", "contributors_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contributors", "created_at": "2024-10-07T00:13:21Z", "default_branch": "main", "delete_branch_on_merge": false, "deployments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/deployments", "description": null, "disabled": false, "downloads_url": "https://api.github.com/repos/tsuperis3112/samplerepo/downloads", "events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/events", "fork": false, "forks": 0, "forks_count": 0, "forks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/forks", "full_name": "tsuperis3112/samplerepo", "git_commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/commits{/sha}", "git_refs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/refs{/sha}", "git_tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/tags{/sha}", "git_url": "git://github.com/tsuperis3112/samplerepo.git", "has_discussions": false, "has_downloads": true, "has_issues": true, "has_pages": false, "has_projects": true, "has_wiki": false, "homepage": null, "hooks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/hooks", "html_url": "https://github.com/tsuperis3112/samplerepo", "id": 868662533, "is_template": false, "issue_comment_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/comments{/number}", "issue_events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/events{/number}", "issues_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues{/number}", "keys_url": "https://api.github.com/repos/tsuperis3112/samplerepo/keys{/key_id}", "labels_url": "https://api.github.com/repos/tsuperis3112/samplerepo/labels{/name}", "language": "C++", "languages_url": "https://api.github.com/repos/tsuperis3112/samplerepo/languages", "license": null, "merge_commit_message": "PR_TITLE", "merge_commit_title": "MERGE_MESSAGE", "merges_url": "https://api.github.com/repos/tsuperis3112/samplerepo/merges", "milestones_url": "https://api.github.com/repos/tsuperis3112/samplerepo/milestones{/number}", "mirror_url": null, "name": "samplerepo", "node_id": "R_kgDOM8a9BQ", "notifications_url": "https://api.github.com/repos/tsuperis3112/samplerepo/notifications{?since,all,participating}", "open_issues": 1, "open_issues_count": 1, "owner": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" }, "private": true, "pulls_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls{/number}", "pushed_at": "2024-12-24T08:49:19Z", "releases_url": "https://api.github.com/repos/tsuperis3112/samplerepo/releases{/id}", "size": 226, "squash_merge_commit_message": "COMMIT_MESSAGES", "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", "ssh_url": "[email protected]:tsuperis3112/samplerepo.git", "stargazers_count": 0, "stargazers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/stargazers", "statuses_url": "https://api.github.com/repos/tsuperis3112/samplerepo/statuses/{sha}", "subscribers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscribers", "subscription_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscription", "svn_url": "https://github.com/tsuperis3112/samplerepo", "tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/tags", "teams_url": "https://api.github.com/repos/tsuperis3112/samplerepo/teams", "topics": [], "trees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/trees{/sha}", "updated_at": "2024-12-24T08:23:56Z", "url": "https://api.github.com/repos/tsuperis3112/samplerepo", "use_squash_pr_title_as_default": false, "visibility": "private", "watchers": 0, "watchers_count": 0, "web_commit_signoff_required": false }, "sha": "bafe574499f02df1f38234e29ea6b411f21da12d", "user": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" } }, "html_url": "https://github.com/tsuperis3112/samplerepo/pull/1", "id": 2250680933, "issue_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/1", "labels": [], "locked": false, "maintainer_can_modify": false, "merge_commit_sha": "04ec1b445e480053f5105afbe1fc8a17bb129a85", "mergeable": null, "mergeable_state": "unknown", "merged": false, "merged_at": null, "merged_by": null, "milestone": null, "node_id": "PR_kwDOM8a9Bc6GJqpl", "number": 1, "patch_url": "https://github.com/tsuperis3112/samplerepo/pull/1.patch", "rebaseable": null, "requested_reviewers": [], "requested_teams": [], "review_comment_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/comments{/number}", "review_comments": 0, "review_comments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/123/comments", "state": "open", "statuses_url": "https://api.github.com/repos/tsuperis3112/samplerepo/statuses/bafe574499f02df1f38234e29ea6b411f21da12d", "title": "tmp", "updated_at": "2024-12-24T08:49:21Z", "url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls/123", "user": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" } }, "repository": { "allow_forking": true, "archive_url": "https://api.github.com/repos/tsuperis3112/samplerepo/{archive_format}{/ref}", "archived": false, "assignees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/assignees{/user}", "blobs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/blobs{/sha}", "branches_url": "https://api.github.com/repos/tsuperis3112/samplerepo/branches{/branch}", "clone_url": "https://github.com/tsuperis3112/samplerepo.git", "collaborators_url": "https://api.github.com/repos/tsuperis3112/samplerepo/collaborators{/collaborator}", "comments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/comments{/number}", "commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/commits{/sha}", "compare_url": "https://api.github.com/repos/tsuperis3112/samplerepo/compare/{base}...{head}", "contents_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contents/{+path}", "contributors_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contributors", "created_at": "2024-10-07T00:13:21Z", "default_branch": "main", "deployments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/deployments", "description": null, "disabled": false, "downloads_url": "https://api.github.com/repos/tsuperis3112/samplerepo/downloads", "events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/events", "fork": false, "forks": 0, "forks_count": 0, "forks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/forks", "full_name": "tsuperis3112/samplerepo", "git_commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/commits{/sha}", "git_refs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/refs{/sha}", "git_tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/tags{/sha}", "git_url": "git://github.com/tsuperis3112/samplerepo.git", "has_discussions": false, "has_downloads": true, "has_issues": true, "has_pages": false, "has_projects": true, "has_wiki": false, "homepage": null, "hooks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/hooks", "html_url": "https://github.com/tsuperis3112/samplerepo", "id": 868662533, "is_template": false, "issue_comment_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/comments{/number}", "issue_events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/events{/number}", "issues_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues{/number}", "keys_url": "https://api.github.com/repos/tsuperis3112/samplerepo/keys{/key_id}", "labels_url": "https://api.github.com/repos/tsuperis3112/samplerepo/labels{/name}", "language": "C++", "languages_url": "https://api.github.com/repos/tsuperis3112/samplerepo/languages", "license": null, "merges_url": "https://api.github.com/repos/tsuperis3112/samplerepo/merges", "milestones_url": "https://api.github.com/repos/tsuperis3112/samplerepo/milestones{/number}", "mirror_url": null, "name": "samplerepo", "node_id": "R_kgDOM8a9BQ", "notifications_url": "https://api.github.com/repos/tsuperis3112/samplerepo/notifications{?since,all,participating}", "open_issues": 1, "open_issues_count": 1, "owner": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" }, "private": true, "pulls_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls{/number}", "pushed_at": "2024-12-24T08:49:19Z", "releases_url": "https://api.github.com/repos/tsuperis3112/samplerepo/releases{/id}", "size": 226, "ssh_url": "[email protected]:tsuperis3112/samplerepo.git", "stargazers_count": 0, "stargazers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/stargazers", "statuses_url": "https://api.github.com/repos/tsuperis3112/samplerepo/statuses/{sha}", "subscribers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscribers", "subscription_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscription", "svn_url": "https://github.com/tsuperis3112/samplerepo", "tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/tags", "teams_url": "https://api.github.com/repos/tsuperis3112/samplerepo/teams", "topics": [], "trees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/trees{/sha}", "updated_at": "2024-12-24T08:23:56Z", "url": "https://api.github.com/repos/tsuperis3112/samplerepo", "visibility": "private", "watchers": 0, "watchers_count": 0, "web_commit_signoff_required": false }, "sender": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" } }, "eventName": "pull_request", "sha": "211eeb5476250a2cbb08a086000a5f091d60883a", "ref": "refs/pull/1/merge", "workflow": "Deploy: Excel Add-in", "action": "script", "actor": "tsuperis3112", "job": "set-env", "runNumber": 10, "runId": 12479397618, "apiUrl": "https://api.github.com", "serverUrl": "https://github.com", "graphqlUrl": "https://api.github.com/graphql" }
{ "payload": { "inputs": { "foo": "bar" }, "ref": "refs/heads/main", "repository": { "allow_forking": true, "archive_url": "https://api.github.com/repos/tsuperis3112/samplerepo/{archive_format}{/ref}", "archived": false, "assignees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/assignees{/user}", "blobs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/blobs{/sha}", "branches_url": "https://api.github.com/repos/tsuperis3112/samplerepo/branches{/branch}", "clone_url": "https://github.com/tsuperis3112/samplerepo.git", "collaborators_url": "https://api.github.com/repos/tsuperis3112/samplerepo/collaborators{/collaborator}", "comments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/comments{/number}", "commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/commits{/sha}", "compare_url": "https://api.github.com/repos/tsuperis3112/samplerepo/compare/{base}...{head}", "contents_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contents/{+path}", "contributors_url": "https://api.github.com/repos/tsuperis3112/samplerepo/contributors", "created_at": "2024-10-07T00:13:21Z", "default_branch": "main", "deployments_url": "https://api.github.com/repos/tsuperis3112/samplerepo/deployments", "description": null, "disabled": false, "downloads_url": "https://api.github.com/repos/tsuperis3112/samplerepo/downloads", "events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/events", "fork": false, "forks": 0, "forks_count": 0, "forks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/forks", "full_name": "tsuperis3112/samplerepo", "git_commits_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/commits{/sha}", "git_refs_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/refs{/sha}", "git_tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/tags{/sha}", "git_url": "git://github.com/tsuperis3112/samplerepo.git", "has_discussions": false, "has_downloads": true, "has_issues": true, "has_pages": false, "has_projects": true, "has_wiki": false, "homepage": null, "hooks_url": "https://api.github.com/repos/tsuperis3112/samplerepo/hooks", "html_url": "https://github.com/tsuperis3112/samplerepo", "id": 868662533, "is_template": false, "issue_comment_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/comments{/number}", "issue_events_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues/events{/number}", "issues_url": "https://api.github.com/repos/tsuperis3112/samplerepo/issues{/number}", "keys_url": "https://api.github.com/repos/tsuperis3112/samplerepo/keys{/key_id}", "labels_url": "https://api.github.com/repos/tsuperis3112/samplerepo/labels{/name}", "language": "C++", "languages_url": "https://api.github.com/repos/tsuperis3112/samplerepo/languages", "license": null, "merges_url": "https://api.github.com/repos/tsuperis3112/samplerepo/merges", "milestones_url": "https://api.github.com/repos/tsuperis3112/samplerepo/milestones{/number}", "mirror_url": null, "name": "samplerepo", "node_id": "R_kgDOM8a9BQ", "notifications_url": "https://api.github.com/repos/tsuperis3112/samplerepo/notifications{?since,all,participating}", "open_issues": 0, "open_issues_count": 0, "owner": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" }, "private": true, "pulls_url": "https://api.github.com/repos/tsuperis3112/samplerepo/pulls{/number}", "pushed_at": "2024-12-24T08:23:53Z", "releases_url": "https://api.github.com/repos/tsuperis3112/samplerepo/releases{/id}", "size": 226, "ssh_url": "[email protected]:tsuperis3112/samplerepo.git", "stargazers_count": 0, "stargazers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/stargazers", "statuses_url": "https://api.github.com/repos/tsuperis3112/samplerepo/statuses/{sha}", "subscribers_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscribers", "subscription_url": "https://api.github.com/repos/tsuperis3112/samplerepo/subscription", "svn_url": "https://github.com/tsuperis3112/samplerepo", "tags_url": "https://api.github.com/repos/tsuperis3112/samplerepo/tags", "teams_url": "https://api.github.com/repos/tsuperis3112/samplerepo/teams", "topics": [], "trees_url": "https://api.github.com/repos/tsuperis3112/samplerepo/git/trees{/sha}", "updated_at": "2024-12-24T08:23:56Z", "url": "https://api.github.com/repos/tsuperis3112/samplerepo", "visibility": "private", "watchers": 0, "watchers_count": 0, "web_commit_signoff_required": false }, "sender": { "avatar_url": "https://avatars.githubusercontent.com/u/152579884?v=4", "events_url": "https://api.github.com/users/tsuperis3112/events{/privacy}", "followers_url": "https://api.github.com/users/tsuperis3112/followers", "following_url": "https://api.github.com/users/tsuperis3112/following{/other_user}", "gists_url": "https://api.github.com/users/tsuperis3112/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/tsuperis3112", "id": 152579884, "login": "tsuperis3112", "node_id": "U_kgDOCRgvLA", "organizations_url": "https://api.github.com/users/tsuperis3112/orgs", "received_events_url": "https://api.github.com/users/tsuperis3112/received_events", "repos_url": "https://api.github.com/users/tsuperis3112/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tsuperis3112/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tsuperis3112/subscriptions", "type": "User", "url": "https://api.github.com/users/tsuperis3112", "user_view_type": "public" }, "workflow": ".github/workflows/action.yaml" }, "eventName": "workflow_dispatch", "sha": "8422d518823c8ff55beb7d57d74bc81dee3aa99e", "ref": "refs/heads/main", "workflow": "Deploy: Excel Add-in", "action": "script", "actor": "tsuperis3112", "job": "set-env", "runNumber": 6, "runId": 12479140273, "apiUrl": "https://api.github.com", "serverUrl": "https://github.com", "graphqlUrl": "https://api.github.com/graphql" }
詳細やその他のデータはドキュメントをご覧ください。
eSquare Liveでの活用事例
ここではeSquare Live開発において発生した課題と、GitHub Scriptを用いた実際の対処方法について説明します。
eSquare Liveは以下のようなフローで開発しています。
graph LR subgraph " " C[開発環境] B[検証環境] A[商用環境] end M1(main) D0(develop) D1(develop) F0(feature) R0(release/vX.Y.Z) R1(release/vX.Y.Z) subgraph 作業フロー D0 --> F0 D0 --- D1 F0 -->|マージ| D1 D1 --> |デプロイ| C D1 --> R0 R0 -.- |バグ修正| R1 R0 --> |デプロイ&QA| B R1 -->|マージ| M1 M1 --> |最終デプロイ&動作確認| B M2 --> |デプロイ| A M1 ---|vX.Y.Z| M2(main) end style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#ffcc99,stroke:#333,stroke-width:2px style C fill:#ccffcc,stroke:#333,stroke-width:2px style M1 stroke:#333,stroke-width:4px style M2 stroke:#333,stroke-width:4px
- developブランチから派生した
feature
ブランチで開発 feature
ブランチの内容をdevelopブランチにマージして開発環境にデプロイ- マイルストーン単位でreleaseブランチを作成し検証環境にデプロイ
- QA中に検証環境でバグが見つかったらreleaseブランチに向けて修正
- リリース直前、mainブランチへマージして検証環境への最終デプロイ&動作確認
vX.Y.Z
のタグを打って商用環境にリリース
発生した問題
この開発フローを運用する中で、2つの問題が顕在化しました。
タグの打ち間違い
GitHubのリリース機能から直接タグを打つ際に、意図しないブランチやコミットにタグを付与してしまうリスクがありました。特に、デフォルトブランチがdevelopであるため誤ってタグを打ってしまうと、リリースプロセス全体に混乱を招き、未検証のコードが商用環境にデプロイされる可能性があります。このようなタグの打ち間違いは、リリースの信頼性を損なう重大な問題です。
releaseブランチが複数存在する場合のデプロイ先選択の複雑化
プロジェクトの進行に伴い、メインストリームのreleaseブランチ以外にも、特定の機能や修正に対応する複数のreleaseブランチが存在する状況に陥りました。これらのブランチが混在していることでデプロイ先の選択が複雑化し、誤って意図しないブランチからデプロイされるリスクが高まりました。
特に、hotfixや小規模なリリースの際に、どのreleaseブランチがどこまで検証されているかの確認が複雑になっていきました。
解決策としてのGitHub Scriptの活用
これらの問題を解決するために、GitHub Scriptを活用してCI/CDのフローを制御することにしました。
flowchart LR A5[/Workflow Dispatch/] -- develop/staging --> S A4([Merge into main]) --> S A3([Merge into develop]) --> S A2([Create tag]) --> S A1([Merge into release/*]) --> S subgraph github[Github] Repo[(Application<br />Repository)] Manifest[(k8s manifest<br />Repository)] subgraph actions[Actions] direction LR S{Check input} -- workflow dispatch --> Deploy[Deployment] S -- main→stg -->Deploy S -- develop→dev -->Deploy S -- tag→prod -->CheckMain CheckMain{機能1<br>same hash?} S -- release/*→stg --> CheckLatest CheckLatest{機能2<br>latest?} CheckMain --> Deploy CheckLatest --> Deploy Repo -.-> CheckMain Repo -.-> CheckLatest end end Deploy -- update --> Manifest -. Sync .-> Argo[ArgoCD]
機能1 vX.Y.Z
のタグがmainブランチのコミットハッシュと一致することを確認する
デフォルトブランチ(develop)からのタグの打ち間違いを防ぐために、リリースタグとmainブランチが一致していることを確認します。
タグをプッシュした際に発火するアクションを設定し、mainブランチとタグのコミットハッシュを比較します。これにより、タグが最新のmainブランチから切られているかを確認できます。
タグのコミットハッシュはcontext.sha
に含まれているため、そのまま取得できます。
const tagHash = context.sha
mainブランチのハッシュ値はGitHub APIのGet a reference
を使って取得します
const { data: main } = await github.rest.git.getRef({ ...context.repo, ref: 'heads/main', }) core.info(`tag: ${tagHash}, main: ${main.object.sha}`)
これらの値を使用して、mainブランチからタグが切られたかを確認します。一致しない場合はタグの打ち間違いの可能性が高いため、CIを失敗させます。
GitHub Script内でステップを失敗させるには、core.setFailed
を使用します。
最終的には以下のように記述します。
const tagHash = context.sha; const { data: main } = await github.rest.git.getRef({ ...context.repo, ref: 'heads/main', }) core.info(`tag: ${tagHash}, main: ${main.object.sha}`) if (tagHash !== main.object.sha) { core.setFailed(`tag is not the same as main branch`) return }
これにより、タグは必ず最新のmainブランチと一致していることが保証されました。
機能2 releaseブランチは最新バージョンのみ自動で検証環境にデプロイする
複数のreleaseブランチが存在する場合、最新のブランチのみを自動で検証環境にデプロイする必要があります。
これにより、古いreleaseブランチや並行して存在する他のreleaseブランチからの誤ったデプロイを防ぎ、デプロイ先の選択をシンプルに保つことができます。
以下に、最新のreleaseブランチを確認するための具体的なスクリプトとその実装方法を紹介します。
- ブランチ名のバージョン解析
ブランチ名がセマンティックバージョニング(SemVer)に基づいているかを確認します。 - 全ブランチの取得とソート
リポジトリ内の全てのブランチを取得し、releaseブランチのバージョンを降順にソートします。 - 最新リリースブランチの判定
ソートされたブランチリストから最新のreleaseブランチを特定し、現在のブランチがその最新ブランチであるかを確認します。 - デプロイの制御
最新のreleaseブランチでない場合、即時リターンすることでデプロイを中断します。
releaseブランチをメインストリームのブランチとして扱っていますが、メインストリーム以外のブランチを検証環境にデプロイする場合は、以下のいずれかの方法で対応します。
- 手動(Workflow Dispatch)実行
- mainブランチへのマージ
...snip... // check if release branch ... (1) const branchVersion = parseSemVer(ref) if (!branchVersion.valid) { core.setFailed(`${ref} is not SemVer`) return } const data = await github.paginate(github.rest.repos.listBranches, { ...context.repo, per_page: 100 }) // sort by version and get the latest release branch ... (2) const versions = data .flatMap((b) => { const ver = parseSemVer(b.name) return ver.valid ? [ver] : [] }) .sort(compareSemVer) core.info(`versions: ${JSON.stringify(versions)}`) if (versions.length === 0) { core.setFailed('There is no release branch') return } // (3) const latestReleaseBranchVersion = versions[0] if (latestReleaseBranchVersion.sha !== branchVersion.sha) { core.warning( `This branch is not the latest release branch. The latest release branch is ${latestReleaseBranchVersion}`, ) // (4) return } ...snip...
完成版スクリプト
ここまでの内容を踏まえ最終調整を加えて以下のようなスクリプトが完成しました。
- developブランチにpushされたとき、開発環境にデプロイ
- 最新のreleaseブランチにpushされたとき、検証環境にデプロイ
- mainブランチにpushされたとき、検証環境にデプロイ
- vから始まるタグが打たれたとき、商用環境にデプロイ (e.g. v1.2.3)
- 検証済みのDockerイメージにタグを付ける
- Workflow Dispatchで特定の環境に特定のブランチをデプロイ
- develop, staging環境のみ
on: push: branches: - develop # develop - main # staging - release/* # staging tags: - v* # production workflow_dispatch: inputs: environment: required: true default: develop type: choice options: - develop - staging jobs: set-env: runs-on: ubuntu-latest outputs: target: ${{ steps.script.outputs.result }} steps: - uses: actions/checkout@v4 - uses: actions/github-script@v7 id: script with: script: | const script = require('./.github/actions/set-env.js') return await script({github, core, context}) result-encoding: string deployment: needs: [set-env] uses: --enechain社内用workflow-- with: environment: ${{ needs.set-env.outputs.target }}
/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ module.exports = async ({ github, core, context }) => { core.info(`payload: ${JSON.stringify(context.payload)}`) const target = await module.exports.getTarget({ github, core, context }) core.info(`target: ${target}`) return target } /** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ module.exports.getTarget = async ({ github, core, context }) => { const stg = 'staging' const dev = 'develop' const prd = 'production' // check environment const input = context.inputs?.target // workflow dispatch const replaceHeads = (s) => s.replace(/^refs\/heads\//, '') const payload = context.payload.pull_request?.base || { ref: context.ref } let { ref } = replaceHeads(payload) core.info(`ref: ${ref}`) if (input) { // workflow dispatch return input } else if (ref.startsWith('refs/tags/')) { // tag image const tagHash = context.sha const { data: main } = await github.rest.git.getRef({ ...context.repo, ref: 'heads/main', }) core.info(`tag: ${tagHash}, main: ${main.object.sha}`) if (tagHash !== main.object.sha) { core.setFailed(`tag is not the same as main branch`) return } return prd } else if (ref === 'main') { return stg } else if (ref === 'develop') { return dev } // check if release branch const branchVersion = parseSemVer(ref) if (!branchVersion.valid) { core.setFailed(`${ref} is not SemVer`) return } const data = await github.paginate(github.rest.repos.listBranches, { ...context.repo, per_page: 100 }) // sort by version and get the latest release branch const versions = data .flatMap((b) => { const ver = parseSemVer(b.name) return ver.valid ? [ver] : [] }) .sort(compareSemVer) core.info(`versions: ${JSON.stringify(versions)}`) if (versions.length === 0) { core.setFailed('There is no release branch') return } const latestReleaseBranchVersion = versions[0] if (latestReleaseBranchVersion.sha !== branchVersion.sha) { core.warning( `This branch is not the latest release branch. The latest release branch is ${latestReleaseBranchVersion}`, ) return } return stg } /** * @param {string} branch */ const parseSemVer = (branch) => { const semverRegex = /^release\/(?:v)?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?$/ // SemVerに基づいた正規表現 const match = branch.match(semverRegex) if (!match) { return { valid: false } // 不正なバージョン } const [sha, major, minor, patch, pre] = match return { version: branch, major: Number(major), minor: Number(minor), patch: Number(patch), pre: pre || null, valid: true, sha, } } /** * Compare two semver versions * @param {{major: number, minor: number, patch: number, pre: string}} a * @param {{major: number, minor: number, patch: number, pre: string}} b */ const compareSemVer = (a, b) => { // Compare major version if (a.major !== b.major) { return b.major - a.major } // Compare minor version if (a.minor !== b.minor) { return b.minor - a.minor } // Compare patch version if (a.patch !== b.patch) { return b.patch - a.patch } // Both have same major, minor, and patch const aHasPre = !!a.pre const bHasPre = !!b.pre if (aHasPre && bHasPre) { // Both have prerelease, compare in reverse to get newer first return a.pre === b.pre ? 0 : -a.pre.localeCompare(b.pre) } else if (aHasPre) { return 1 // a is prerelease, b is release; release comes first } else if (bHasPre) { return -1 // a is release, b is prerelease } else { return 0 // Both are release versions } }
まとめ
これらのGitHub Scriptを導入することで、以下のようなメリットが得られました。
自動化によるエラーの削減
タグの打ち間違いや誤ったブランチからのデプロイを自動的に検出・防止することで、人的ミスによるエラーを大幅に削減できます。リリースプロセスの一貫性の向上
一定のルールに基づいてリリースプロセスを自動化することで、リリース作業の一貫性が向上し、品質の安定化が図れます。ユニットテストによる実行前動作確認
通常GitHub Actionsは実行環境の準備や実環境での動作確認をする必要があり、簡単にテストすることは難しいです。今回はgetTarget
をスクリプト化することでjestやvitestによる動作検証できるようになりました。
今後もこの手法を基に、さらなるCI/CDの最適化を目指していきたいと考えています。 後々にはTerraformやKubernetesと共働でリリースブランチ毎の環境構築まで自動で行いたいと考えています。
enechainでは、事業拡大のために共に技術力で道を切り拓いていく仲間を募集しています。
- プロジェクト配下ならどこでも配置可能です↩