ã¯ããã« ãã¯ããããããŸããIWSã§ãã Slack ãããŒã«ã«ç°å¢ãã GitHub Actions ã®ã¯ãŒã¯ãããŒãèµ·åããããããã㯠API çµç±ã§ GitHub ãæäœãããããããªã·ãŒã³ã§é¿ããŠéããªãã®ã Personal Access TokenïŒPATïŒ ã®ååšã§ãã ããããPAT ã«ã¯ä»¥äžã®ãããªæ©ã¿ãã€ããŸãšããŸãã çºè¡ãã人ã«çŽã¥ããããéè·ãç°åã§äœ¿ããªããªã æå¹æéã®æŽæ°ã»åçºè¡ãªã©ãéçšã³ã¹ããå°å³ã«é«ã æš©éã¹ã³ãŒããåºããªããã¡ã§ãã»ãã¥ãªãã£çã«æ±ãã¥ãã ããã§æ¬èšäºã§ã¯ãGitHub App ã䜿ã£ãŠçææå¹ã®äžæããŒã¯ã³ãæãåºããPAT 管çããè§£æŸãããæ¹æ³ ã玹ä»ããŸããããããŠãGitHub ã® GraphQL API ã䜿ã£ãŠãã©ã³ãåããããŸãæ€çŽ¢ããå°ãã¿ãèŒããŠããŸãã ãããã GitHub App ãšã¯ GitHub App ã¯ãGitHub ã®æ©èœãæäœããŠæ¡åŒµããããã«æ§ç¯ã§ããçµ±åã®äžçš®ã§ãããŠãŒã¶ãŒã®ãµã€ã³ã€ã³ããµãŒãã¹ ã¢ã«ãŠã³ãã®äœæãå¿
èŠãšããã«ãæè»æ§ãæäŸããããã»ã¹ã®æ©æŠã軜æžã§ããŸãã åŒçšïŒ GitHub DocsGitHub ã¢ããªã®äœæã«ã€ã㊠– GitHubããã¥ã¡ã³ã â ãã£ããèšããšãå人ã¢ã«ãŠã³ãã§ã¯ãªããã¢ããªããšã㊠GitHub ã«å¯ŸããŠæš©éãæãŠãä»çµã¿ ã§ããã€ã³ã¹ããŒã«ãããªããžããªã«å¯ŸããŠãå¿
èŠæå°éã®æš©éã§ãçåœãªïŒæå€§1æéã®ïŒã¢ã¯ã»ã¹ããŒã¯ã³ãçºè¡ã§ããŸãã ããã«ããã å人ã«çŽã¥ããªã æš©éã¹ã³ãŒãããªããžããªåäœã»æäœåäœã§çµãã ããŒã¯ã³ã¯éœåºŠçºè¡ã»ç寿åœãªã®ã§æŒæŽ©ãªã¹ã¯ãäœã ãšãã£ãå¬ãããåŸãããŸãã ãŽãŒã« æ¬èšäºã®ãŽãŒã«ã¯æ¬¡ã®ãšããã§ãã GitHub App ãäœæãã察象ãªããžããªã«ã€ã³ã¹ããŒã«ãã Lambda ãã JWT ãçæããäžæã¢ã¯ã»ã¹ããŒã¯ã³ãååŸãã ååŸããããŒã¯ã³ã§ repository_dispatch ã€ãã³ããçºç«ããGitHub Actions ãèµ·åãã ïŒããŸãïŒGraphQL API ã§ãã©ã³ãåããããŸãæ€çŽ¢ãã 1. GitHub App ãæºåãã ãŸã㯠GitHub App ãäœæããŸããå人ã¢ã«ãŠã³ãã® Settings â Developer settings â GitHub Apps â New GitHub App ããäœæããŸãã äž»ãªèšå®é
ç®ã¯æ¬¡ã®ãšããã§ãã GitHub App name : 奜ããªååãèšå® Homepage URL : ä»»æã® URLïŒäŸ: https://example.com ïŒ Webhook : ä»åã¯å©çšããªãã®ã§ãã§ãã¯ãå€ã Permissions : çšéã«åãããŠèšå®ãæ¬èšäºã§ã¯ Actions : Read and write Contents : Read and write Where can this GitHub App be installed? : Organization ã®ãªããžããªãžã€ã³ã¹ããŒã«ãããã®ã§ Any account ãéžæ äœæãå®äºãããšãApp ã®èšå®ç»é¢ã«é·ç§»ããŸããããã§ Client ID ãæ§ããŠãããããŒãžäžéšã® Private keys ãã Generate a private key ã§ .pem ãã¡ã€ã«ãçºè¡ããŠããŠã³ããŒãããŠãããŸãããã®ç§å¯éµã¯åŸã»ã© JWT ã®çœ²åã«äœ¿çšããŸãã ç¶ã㊠Install App ã®ããŒãžããã察象㮠Organization ã«ã€ã³ã¹ããŒã«ããŸãã æš©éãçµãããã« Only select repositories ãéžæãã察象ãªããžããªãæå®ããŠã€ã³ã¹ããŒã«ããŸãããã ã€ã³ã¹ããŒã«å
ãªããžããªã® Settings â GitHub Apps ã«äœæãã App ã衚瀺ãããã°æºåå®äºã§ãã 2. JWT ãçæãã ããããå
㯠AWS Lambda äžã§ã®å®è£
äŸã玹ä»ããŸãïŒZapier ãæ€èšããŸããããç°å¢å€æ°ãæ±ããæå¿µããŸããïŒã ãŸãã¯ç§å¯éµã䜿ã£ãŠ JWT ãçæããŸãã iss ã«ã¯ GitHub App ã® Client ID ãã exp ã¯æå€§10å以å
ã«èšå®ããŸãã import jwt import time def generate_github_jwt(private_key: str) -> str: """GitHub App çšã® JWT ãçæãã""" payload = { 'iat': int(time.time()), 'exp': int(time.time()) + 600, # æå€§10å 'iss': '<GitHub App ã® Client ID>', } return jwt.encode(payload, private_key, algorithm='RS256') 3. ã€ã³ã¹ããŒã«ã¢ã¯ã»ã¹ããŒã¯ã³ãååŸãã çæãã JWT ã䜿ã£ãŠãã€ã³ã¹ããŒã«åäœã®ã¢ã¯ã»ã¹ããŒã¯ã³ãååŸããŸããããŒã¯ã³ã®æå¹æéã¯æå€§1æéã§ãã import requests def get_access_token(jwt_token: str) -> str: """GitHub App ã®ã€ã³ã¹ããŒã«ã¢ã¯ã»ã¹ããŒã¯ã³ãååŸãã""" install_id = '<GitHub App ã® Installation ID>' url = f'https://api.github.com/app/installations/{install_id}/access_tokens' headers = { 'Authorization': f'Bearer {jwt_token}', 'Accept': 'application/vnd.github+json', } data = { 'repository': '<察象ãªããžããªå>', } response = requests.post(url, json=data, headers=headers) return response.json()['token'] data ã«ããã«çްããæš©éããªããžããªæå®ãå ããããšã§ãã€ã³ã¹ããŒã«æã®æš©éããããäžæ®µçµã£ãããŒã¯ã³ãçºè¡ã§ããŸãã data = { 'repository': '<察象ãªããžããªå>', 'permissions': { 'actions': 'write', }, } æå°æš©éã®ååã培åºãããå Žåã¯ãåŒã³åºãããšã«å¿
èŠãªæš©éã ããä»äžããããŒã¯ã³ãçºè¡ããã®ãããããã§ãã 4. GitHub Actions ã repository_dispatch ã§çºç«ãã ååŸããããŒã¯ã³ã䜿ã£ãŠã repository_dispatch ãšã³ããã€ã³ããå©ããŸããããã§ GitHub Actions ã®ã¯ãŒã¯ãããŒãå€éšããèµ·åã§ããŸãã def workflow_trigger(access_token: str, env: str, branch: str) -> None: """GitHub Actions ã®ã¯ãŒã¯ãããŒãèµ·åãã""" url = 'https://api.github.com/repos/<owner>/<repo>/dispatches' headers = { 'Authorization': f'Bearer {access_token}', 'Accept': 'application/vnd.github+json', } data = { 'event_type': f'deploy-{env}', 'client_payload': { 'branch': branch, }, } response = requests.post(url, json=data, headers=headers) if response.status_code != 204: raise Exception(f'Dispatch failed: {response.status_code} {response.text}') åŒã³åºãããåŽã®ã¯ãŒã¯ãããŒã§ã¯ã on: repository_dispatch ã® types ã«äžã§éã£ã event_type ãæå®ããŠãããŸãã on: repository_dispatch: types: [deploy-development] ã®ããã«ããããšã§ãevent_type ã§éã£ãå€ãš repository_dispatch ã® types ã§èšå®ããå€ãäžèŽãããšãã«çºç«ãããããšãã§ããŸãã event_type ã deploy-development çºç«ãã event_type ã deploy-check çºç«ããªã ããšã¯ Slack ã® ChatBot ãã Lambda ãèµ·åããããã«ããã°ãPAT ã䜿ãã Slack ãã GitHub Actions ãå©ã ä»çµã¿ã宿ããŸãã GitHub åŽã«ãããã® App ããå®è¡ãããããšããå±¥æŽãæ®ãã®ã§ãç£æ»ã®èгç¹ã§ãå®å¿ã§ãã ããŸã: GraphQL API ã§ãã©ã³ãåããããŸãæ€çŽ¢ãã Slack ã³ãã³ãããçºç«ããå ŽåããŠãŒã¶ãŒãæã¡èŸŒããã©ã³ãåã¯ã¿ã€ãããã¡ã§ããããã§ãçºè¡ããããŒã¯ã³ã䜿ã£ãŠ GraphQL API ãããã©ã³ãäžèЧãååŸãããããŸããããã§æãè¿ããã©ã³ããéžã¶ åŠçãå
¥ããŠãããšäŸ¿å©ã§ãã ãã©ã³ãäžèЧãååŸãã def fetch_branches(access_token: str, query: str) -> list: """GraphQL API ã§ãªããžããªã®ãã©ã³ãäžèЧãååŸãã""" QUERY = """ query($owner: String!, $repo: String!, $query: String!) { repository(owner: $owner, name: $repo) { refs(refPrefix: "refs/heads/", first: 100, query: $query) { edges { node { name } } } } } """ headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json', } variables = { 'owner': '<owner>', 'repo': '<repo>', 'query': query, } response = requests.post( 'https://api.github.com/graphql', json={'query': QUERY, 'variables': variables}, headers=headers, ) edges = response.json()['data']['repository']['refs']['edges'] return [edge['node']['name'] for edge in edges] é¡äŒŒåºŠãæãé«ããã©ã³ããéžã¶ import difflib def search_branch(branches: list, query: str) -> str: """difflib ã§äžçªè¿ããã©ã³ãåãéžã¶""" best_match = None highest_ratio = 0.0 for branch in branches: ratio = difflib.SequenceMatcher(None, branch, query).ratio() if ratio > highest_ratio: best_match = branch highest_ratio = ratio return best_match Python æšæºã©ã€ãã©ãªã® difflib.SequenceMatcher ã§æååã®é¡äŒŒåºŠãèšç®ããæãè¿ããã©ã³ãåãæ¡çšããããšããåçŽãªã¢ãããŒãã§ããããã ãã§ãã develp ããã develop ãã«å¯ãããããã®æå©ãã¯ããŠãããŸãã ãŸãšã PAT 管çã¯ããã©ããGitHub App ã«çœ®ãæãããšéçšè² è·ãã»ãã¥ãªãã£ãªã¹ã¯ãäžããããã JWT â ã€ã³ã¹ããŒã«ã¢ã¯ã»ã¹ããŒã¯ã³ â repository_dispatch ã®æµãã§ãå€éšããå®å
šã« GitHub Actions ãèµ·åã§ããã GraphQL API ãš difflib ãçµã¿åãããã°ããã©ã³ãåã®ã¿ã€ãã«ãèãã Slack ã³ãã³ããäœããã GitHub Actions éçšã§ PAT ã«æ¶èããŠããæ¹ã¯ããã² GitHub App åãæ€èšããŠã¿ãŠãã ããã Lambda å®è£
ã³ãŒãå
šäœ import json, requests, os import time import jwt import difflib def lambda_handler(event, context): env = event.get('env', 'development') target_branch = event.get('branch', 'develop') private_key = aws_parameters('PRIVATE_KEY', decryption=True) jwt_token = generate_github_jwt(private_key) access_token = get_access_token(jwt_token) if target_branch != 'develop': branches = fetch_branches(access_token, target_branch) target_branch = search_branch(branches, target_branch) workflow_trigger(access_token, env, target_branch) return { 'statusCode': 200, 'body': json.dumps('command success!!'), } def aws_parameters(parameter_name: str, decryption: bool = False) -> str: aws_session_token = os.environ['AWS_SESSION_TOKEN'] headers = {'X-Aws-Parameters-Secrets-Token': aws_session_token} response = requests.get( f'http://localhost:2773/systemsmanager/parameters/get?name={parameter_name}&withDecryption={decryption}', headers=headers, ) return json.loads(response.text)['Parameter']['Value'] def generate_github_jwt(private_key: str) -> str: payload = { 'iat': int(time.time()), 'exp': int(time.time()) + 600, 'iss': aws_parameters('GITHUB_CLIENT_ID'), } return jwt.encode(payload, private_key, algorithm='RS256') def get_access_token(jwt_token: str) -> str: install_id = aws_parameters('GITHUB_INSTALL_ID') url = f'https://api.github.com/app/installations/{install_id}/access_tokens' headers = { 'Authorization': f'Bearer {jwt_token}', 'Accept': 'application/vnd.github+json', } data = {'repository': '<察象ãªããžããªå>'} response = requests.post(url, json=data, headers=headers) return response.json()['token'] def fetch_branches(access_token: str, query: str) -> list: QUERY = """ query($owner: String!, $repo: String!, $query: String!) { repository(owner: $owner, name: $repo) { refs(refPrefix: "refs/heads/", first: 100, query: $query) { edges { node { name } } } } } """ headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json', } variables = { 'owner': '<owner>', 'repo': '<repo>', 'query': query, } response = requests.post( 'https://api.github.com/graphql', json={'query': QUERY, 'variables': variables}, headers=headers, ) edges = response.json()['data']['repository']['refs']['edges'] return [edge['node']['name'] for edge in edges] def search_branch(branches: list, query: str) -> str: best_match = None highest_ratio = 0.0 for branch in branches: ratio = difflib.SequenceMatcher(None, branch, query).ratio() if ratio > highest_ratio: best_match = branch highest_ratio = ratio return best_match def workflow_trigger(access_token: str, env: str, branch: str) -> None: url = 'https://api.github.com/repos/<owner>/<repo>/dispatches' headers = { 'Authorization': f'Bearer {access_token}', 'Accept': 'application/vnd.github+json', } data = { 'event_type': f'deploy-{env}', 'client_payload': {'branch': branch}, } response = requests.post(url, json=data, headers=headers) if response.status_code != 204: raise Exception(f'Dispatch failed: {response.status_code} {response.text}')