GitHub リポジトリの棚卸し依頼 issue を自動作成する

:::note info
コンテキスト
- GitHub Organization を管理している
- リポジトリの棚卸しを効率化したい
:::

# 前提

repo を複数持つ GitHub Organization があるとする

https://github.com/orgs/rindrics-sandbox-org/repositories

# ゴール

定期的に GitHub Actions workflow として実行し、棚卸し作業指示の GitHub issue を作りたい

## ざっくり設計

- 条件に当てはまる repo 一覧を取得
- issue 作成
  - description には repo 一覧をタスクリストとして記載する

# 手段

GitHub Actions から GitHub の GraphQL API を利用する

## ほかに検討した選択肢

REST API:
- curl
- JavaScript
- gh CLI

上記いずれも使い勝手はそんなに変わらない。特に今回のケースではクエリが単純なので REST API と GraphQL の間でも体験はそんなに違わないかも。
ただし別タスクなどでクエリが複雑になった場合には GraphQL のほうがクエリをシンプルに書けて、学習コストに対するトータルの見返りが大きい可能性がある。

# 限りなく実装な設計

ChatGPT にも考えてもらう

## まず雑に投げてみる
> organization repo のアーカイブ効率化する GitHub Actions 実装の相談: 直近1 ヶ月間更新のないorganization repository を取得したい。archive 対象であるこれらをdescription にタスクリストとして記載した issue を sre というリポジトリに作りたい。これを 1 ヶ月おきに実行するGitHub Actions を書いてほしい


<details><summary>ChatGPT による案</summary>

```yaml
name: Archive Old Repositories

on:
  schedule:
    - cron: '0 0 1 * *'  # 毎月1日の0時に実行
  workflow_dispatch: # 手動実行を許可

jobs:
  archive_old_repos:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.x'

      - name: Install dependencies
        run: pip install requests

      - name: Run script to find and archive old repositories
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
        run: |
          python3 - <<'EOF'
          import requests
          import json
          from datetime import datetime, timedelta

          # GitHub Personal Access Token
          TOKEN = '${{ secrets.GH_TOKEN }}'
          ORG_NAME = 'rindrics-sandbox-org'
          SRE_REPO = 'sre'

          # Calculate the date one month ago
          one_month_ago = datetime.now() - timedelta(days=30)
          one_month_ago_iso = one_month_ago.isoformat() + "Z"

          # GraphQL query
          query = """
          {
            organization(login: "%s") {
              repositories(first: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
                nodes {
                  name
                  updatedAt
                }
              }
            }
          }
          """ % ORG_NAME

          url = 'https://api.github.com/graphql'
          headers = {
              'Authorization': f'Bearer {TOKEN}',
              'Content-Type': 'application/json'
          }

          response = requests.post(url, headers=headers, data=json.dumps({'query': query}))

          if response.status_code == 200:
              data = response.json()
              nodes = data['data']['organization']['repositories']['nodes']

              # Filter repositories updated more than 1 month ago
              old_repos = [repo for repo in nodes if datetime.fromisoformat(repo['updatedAt'][:-1]) < one_month_ago]

              if not old_repos:
                  print("No repositories found that were updated more than a month ago.")
                  exit(0)

              # Create issue body
              issue_body = "The following repositories have not been updated in the last month and are candidates for archiving:\n\n"
              for repo in old_repos:
                  issue_body += f"- [ ] {repo['name']} (Last updated: {repo['updatedAt']})\n"

              # Create an issue in the SRE repository
              issue_url = f'https://api.github.com/repos/{ORG_NAME}/{SRE_REPO}/issues'
              issue_data = {
                  'title': 'Monthly Archive Candidates',
                  'body': issue_body
              }
              issue_headers = {
                  'Authorization': f'token {TOKEN}',
                  'Content-Type': 'application/json'
              }

              issue_response = requests.post(issue_url, headers=issue_headers, data=json.dumps(issue_data))

              if issue_response.status_code == 201:
                  print("Issue created successfully.")
              else:
                  print(f"Failed to create issue: {issue_response.status_code} {issue_response.text}")
          else:
              print(f"Query failed to run by returning code of {response.status_code}. {response.json()}")
          EOF

```

</details>

### いくつか改善点があったので指摘

> 改善点があったので下記に留意して書き直してほしい:
> - GraphQL を投げる部分は独立した step として分けてほしい
>   - 例えば https://github.com/octokit/graphql-action
> - new issue するアクションは独立した step として分けてほしい
>   - 例えば https://github.com/actions-ecosystem/action-create-issue を利用する
> - Python コードをできるだけ減らしてほしい
> - 利用 action のバージョンを下記のようにアップデートしてほしい
>   - actions/checkout@v4
>   - actions/setup-python@v5

<details><summary>ChatGPT による案</summary>

```yaml
name: Archive Old Repositories

on:
  schedule:
    - cron: '0 0 1 * *'  # 毎月1日の0時に実行
  workflow_dispatch: # 手動実行を許可

jobs:
  archive_old_repos:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Query repositories
        id: graphql
        uses: octokit/graphql-action@v2.x
        with:
          query: |
            query ($org: String!, $date: DateTime!) {
              organization(login: $org) {
                repositories(first: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
                  nodes {
                    name
                    updatedAt
                  }
                }
              }
            }
          variables: |
            {
              "org": "rindrics-sandbox-org",
              "date": "${{ steps.date.outputs.one_month_ago }}"
            }
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN }}

      - name: Set date variable
        id: date
        run: echo "one_month_ago=$(date -d '-1 month' --utc +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV

      - name: Filter old repositories and create issue body
        id: create_issue_body
        run: |
          echo "Creating issue body..."
          python3 - <<EOF
          import os
          import json
          from datetime import datetime, timedelta

          response = ${{
            steps.graphql.outputs.data
          }}
          data = json.loads(response)
          nodes = data['organization']['repositories']['nodes']

          one_month_ago = datetime.strptime(os.getenv('one_month_ago'), '%Y-%m-%dT%H:%M:%SZ')

          old_repos = [repo for repo in nodes if datetime.strptime(repo['updatedAt'], '%Y-%m-%dT%H:%M:%SZ') < one_month_ago]

          if not old_repos:
              print("No repositories found that were updated more than a month ago.")
              exit(0)

          issue_body = "The following repositories have not been updated in the last month and are candidates for archiving:\n\n"
          for repo in old_repos:
              issue_body += f"- [ ] {repo['name']} (Last updated: {repo['updatedAt']})\n"

          with open('issue_body.txt', 'w') as f:
              f.write(issue_body)
          EOF

      - name: Create issue
        uses: actions-ecosystem/action-create-issue@v1.x
        with:
          token: ${{ secrets.GH_TOKEN }}
          repository: rindrics-sandbox-org/sre
          title: Monthly Archive Candidates
          body: |
            ${{ steps.create_issue_body.outputs.issue_body }}
```

</details>

### 微修正

> すこし改善点があったので直してほしい
> - `create_issue_body` step の出力は `.txt` でなく $GITHUB_OUTPUT という環境変数に出力してほしい
> - 各 issue description の形式は "[yyyy ${quarter_number} 棚卸し] ${repo_name} のアーカイブを検討する" の形式にしてほしい。たとえば"[2024 2Q] rindrics-sandbox-org/first-repo のアーカイブを検討する" など
> - GH_TOKEN には https://github.com/actions/create-github-app-token を利用して生成したものを使ってほしい


<details><summary>ChatGPT による案</summary>

```yaml
name: Archive Old Repositories

on:
  schedule:
    - cron: '0 0 1 * *'  # 毎月1日の0時に実行
  workflow_dispatch: # 手動実行を許可

jobs:
  archive_old_repos:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Generate GitHub App Token
        id: generate_token
        uses: actions/create-github-app-token@v1.3.0
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}
          installation_id: ${{ secrets.INSTALLATION_ID }}

      - name: Set date variable
        id: date
        run: |
          echo "one_month_ago=$(date -d '-1 month' --utc +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
          echo "current_year=$(date +'%Y')" >> $GITHUB_ENV
          echo "current_quarter=Q$((($(date +%m)-1)/3+1))" >> $GITHUB_ENV

      - name: Query repositories
        id: graphql
        uses: octokit/graphql-action@v2.x
        with:
          query: |
            query ($org: String!, $date: DateTime!) {
              organization(login: $org) {
                repositories(first: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
                  nodes {
                    name
                    updatedAt
                  }
                }
              }
            }
          variables: |
            {
              "org": "rindrics-sandbox-org",
              "date": "${{ steps.date.outputs.one_month_ago }}"
            }
        env:
          GH_TOKEN: ${{ steps.generate_token.outputs.token }}

      - name: Filter old repositories and create issue body
        id: create_issue_body
        run: |
          echo "Creating issue body..."
          python3 - <<EOF
          import os
          import json
          from datetime import datetime, timedelta

          response = ${{
            steps.graphql.outputs.data
          }}
          data = json.loads(response)
          nodes = data['organization']['repositories']['nodes']

          one_month_ago = datetime.strptime(os.getenv('one_month_ago'), '%Y-%m-%dT%H:%M:%SZ')

          old_repos = [repo for repo in nodes if datetime.strptime(repo['updatedAt'], '%Y-%m-%dT%H:%M:%SZ') < one_month_ago]

          if not old_repos:
              print("No repositories found that were updated more than a month ago.")
              exit(0)

          current_year = os.getenv('current_year')
          current_quarter = os.getenv('current_quarter')
          issue_body = ""

          for repo in old_repos:
              repo_name = repo['name']
              issue_body += f"- [ ] [{current_year} {current_quarter}] {repo_name} のアーカイブを検討する (Last updated: {repo['updatedAt']})\n"

          print(f"issue_body={issue_body}")
          with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
              print(f"issue_body={issue_body}", file=fh)
          EOF

      - name: Create issue
        uses: actions-ecosystem/action-create-issue@v1.x
        with:
          token: ${{ steps.generate_token.outputs.token }}
          repository: rindrics-sandbox-org/sre
          title: Monthly Archive Candidates
          body: ${{ steps.create_issue_body.outputs.issue_body }}
```

</details>

## 動きを見つつ修正したもの

```yaml
name: Create Issue for Archive Candidate Repositories

on:
  schedule:
    - cron: '0 0 1 * *'  # 毎月1日の0時に実行
  workflow_dispatch: # 手動実行を許可

jobs:
  create_issue_with_archive_candidate_repos:
    runs-on: ubuntu-latest
    env:
      month_diff: 1

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Generate GitHub App Token for Query
        id: generate_query_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ vars.APP_ID_REPO_LISTER }}
          private-key: ${{ secrets.PRIVATE_KEY_REPO_LISTER }}

      - name: Generate GitHub App Token for Issue Creation
        id: generate_issue_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ vars.APP_ID_ISSUE_CREATOR }}
          private-key: ${{ secrets.PRIVATE_KEY_ISSUE_CREATOR }}

      - name: Set date variable
        id: date
        run: |
          echo "month_ago=$(date -d "-$month_diff month" --utc +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
          echo "current_year=$(date +'%Y')" >> $GITHUB_ENV
          echo "current_quarter=$((($(date +%m)-1)/3+1))Q" >> $GITHUB_ENV

      - name: Query repositories
        id: graphql
        uses: octokit/graphql-action@v2.3.2
        with:
          query: |
            query ($org: String!) {
              organization(login: $org) {
                repositories(first: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
                  nodes {
                    name
                    updatedAt
                  }
                }
              }
            }
          variables: |
            {
              "org": "rindrics-sandbox-org"
            }
        env:
          GITHUB_TOKEN: ${{ steps.generate_query_token.outputs.token }}

      - name: Filter old repositories and create issue data
        id: create_issue_data
        run: |
          echo "Creating issue body..."
          python3 - <<EOF
          import os
          from datetime import datetime, timedelta

          response = ${{
            steps.graphql.outputs.data
          }}
          nodes = response['organization']['repositories']['nodes']

          month_ago = datetime.strptime(os.getenv('month_ago'), '%Y-%m-%dT%H:%M:%SZ')
          month_diff = os.getenv('month_diff')

          old_repos = [repo for repo in nodes if datetime.strptime(repo['updatedAt'], '%Y-%m-%dT%H:%M:%SZ') < month_ago]

          if not old_repos:
              print(f"No repositories found that were updated more than {month_diff} months ago.")
              with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
                  fh.write(f"has_old_repos=false\n")
              exit(0)

          current_year = os.getenv('current_year')
          current_quarter = os.getenv('current_quarter')
          year_quarter = f"[{current_year} {current_quarter}]"
          issue_title = f"{year_quarter} リポジトリ棚卸し"
          issue_body = f"下記は直近 {month_diff} か月間で更新のないリポジトリのリストです。アーカイブを検討してください。\n"

          for repo in old_repos:
              repo_name = repo['name']
              issue_body += f"- [ ] {year_quarter} {repo_name} のアーカイブを検討する\n"

          # Write the issue title and body to the output environment variable
          with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
              fh.write(f"has_old_repos=true\n")
              fh.write(f"issue_title={issue_title}\n")
              fh.write(f"issue_body<<EOF\n{issue_body}\nEOF\n")
          EOF

      - name: Create issue
        if: ${{ steps.create_issue_data.outputs.has_old_repos }} == 'true'
        uses: actions-ecosystem/action-create-issue@v1
        with:
          github_token: ${{ steps.generate_issue_token.outputs.token }}
          repo: rindrics-sandbox-org/sre
          title: ${{ steps.create_issue_data.outputs.issue_title }}
          body: ${{ steps.create_issue_data.outputs.issue_body }}
```

ここまでで、下記のように issue がつくられるようになった

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235971/329aeb91-3296-822b-09a6-443f00556a73.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235971/7bb56b59-917d-16c4-5d53-a47a1329c3cc.png)
（リポジトリがぜんぶ作りたてなので動作確認のためにクエリ条件を ”直近 0 か月以上更新がないリポジトリ” にしている）

# 仕様変更

タスクリストではなく、子 issue まで一気に作ってしまうことにした
- もともと issue description にタスクリストを書くようにしていたのは必要に応じて issue 化できるようにだった
- しかし各子 issue での「アーカイブを検討」でも定型作業がありそう:
  - リポジトリオーナーに利用状況を確認してもらう
  - アーカイブ可否を返信してもらう

これらを反映していく

## 仕様変更のプロンプト

> （実際のプロンプトにはここにコード）
> ---
> 上記のコードで、アーカイブ対象リポジトリをタスクリストに持った issue を作れた。
> しかし仕様変更したい。
> 変更内容:
> - description について、アーカイブ対象リポジトリのタスクリストではなく、各アーカイブ対象ごとに issue をつくり、そのリストにしたい
> - 各アーカイブ対象の issue description は下記の通り:
> "本リポジトリは直近{month}ヶ月間で更新がないようです。利用状況を確認し、アーカイブ可能な場合には {my_team} にご連絡ください。@{owner_team}\n- 依頼背景: {親 issue の full URL}において Organization リポジトリの棚卸しをしており、利用されなくなったリポジトリのアーカイブを検討しています。"
> - 各アーカイブ対象の issue 作成先はそれぞれの repo
> 方針:
> issue data を作っているステップを下記のように分割する:
> - old_repos のリストを作るステップ
> - old_repos のリストの数だけ issue dataを作るステップ
>   - 出力: child_issue_data = {repo, issue_title, issue_body} のリスト
> - child_issue_data （matrix?）に対して issue を作るステップ
>   - 出力: repo 名と issue URL のマップ
> - repo 名と issue URL のマップから親 issue のデータを作るステップ
>   - 出力: parent_issue_data = {issue_title, issue_body}
> - parent_issue_data から issue を一つ作るステップ

動かしながら微（大）修正してできた action は下記の通り（ちょっと長くなってしまったが）

<details><summary>workflow.yml</summary>

```yml
name: Create Issue for Archive Candidate Repositories

on:
  schedule:
    - cron: '0 0 1 * *'
  workflow_dispatch:

env:
  my_org: rindrics-sandbox-org
  my_repo: sre  # repository name to which epic issue is created
  my_team: sre  # team to whom epic issue is assigned
  threshold_last_update: 3  # if repo `updatedAt` is greater than this parameter, it is assumed as "inactive"

jobs:
  get_archive_candidates:
    runs-on: ubuntu-latest
    outputs:
      has_old_repos: ${{ steps.get_archive_candidates.outputs.has_old_repos }}
      old_repos: ${{ steps.get_archive_candidates.outputs.old_repos }}
      current_year: ${{ env.current_year }}
      current_quarter: ${{ env.current_quarter }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Generate GitHub App Token
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ vars.APP_ID_TEAM_READER }}
          private-key: ${{ secrets.PRIVATE_KEY_TEAM_READER }}

      - name: Set date variable
        id: date
        run: |
          echo "month_ago=$(date -d '-${{ env.threshold_last_update }} month' --utc +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
          echo "current_year=$(date +'%Y')" >> $GITHUB_ENV
          echo "current_quarter=$((($(date +%m)-1)/3+1))Q" >> $GITHUB_ENV

      - name: Query all repositories under organization
        id: query_repo
        uses: octokit/graphql-action@v2.3.2
        with:
          query: |
            query ($org: String!) {
              organization(login: $org) {
                repositories(first: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
                  nodes {
                    name
                    updatedAt
                  }
                }
              }
            }
          variables: |
            {
              "org": "${{ env.my_org }}"
            }
        env:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

      - name: Query teams
        id: query_teams
        uses: octokit/graphql-action@v2.3.2
        with:
          query: |
            query ($org: String!) {
              organization(login: $org) {
                teams(first: 100) {
                  edges {
                    node {
                      slug
                      name
                      repositories(first: 100) {
                        edges {
                          node {
                            name
                          }
                          permission
                        }
                      }
                    }
                  }
                }
              }
            }
          variables: |
            {
              "org": ${{ env.my_org }}
            }
        env:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

      - name: Get archive candidate repos
        id: get_archive_candidates
        run: |
          echo "Extracting old repositories..."
          python3 - <<EOF
          import os
          from datetime import datetime, timedelta
          import json

          # Repositories response from query_repo step
          repos_response = ${{
            steps.query_repo.outputs.data
          }}
          repos_nodes = repos_response['organization']['repositories']['nodes']

          # Teams response from query_teams step
          teams_response = ${{
            steps.query_teams.outputs.data
          }}
          teams_edges = teams_response['organization']['teams']['edges']

          month_ago = datetime.strptime(os.getenv('month_ago'), '%Y-%m-%dT%H:%M:%SZ')

          # Build a map of teams and their repository permissions
          team_repo_permissions = {}
          for team in teams_edges:
            team_slug = team['node']['slug']
            for repo_edge in team['node']['repositories']['edges']:
              repo_name = repo_edge['node']['name']
              permission = repo_edge['permission']
              if permission == 'ADMIN':
                if repo_name not in team_repo_permissions:
                  team_repo_permissions[repo_name] = []
                team_repo_permissions[repo_name].append(team_slug)

          # Filter old repositories and find admin teams
          old_repos = [
            {
              'name': repo['name'],
              'admin_teams': team_repo_permissions.get(repo['name'], [])
            }
            for repo in repos_nodes if datetime.strptime(repo['updatedAt'], '%Y-%m-%dT%H:%M:%SZ') < month_ago
          ]

          if not old_repos:
              print(f"No repositories found that were updated more than {os.getenv('threshold_last_update')} months ago.")
              with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
                  fh.write(f"has_old_repos=false\n")
              exit(0)

          old_repos_str = json.dumps(old_repos)
          with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
              fh.write(f"has_old_repos=true\n")
              fh.write(f"old_repos={old_repos_str}\n")
          EOF

  create_issues:
    runs-on: ubuntu-latest
    needs: get_archive_candidates
    if: needs.get_archive_candidates.outputs.has_old_repos == 'true'
    env:
      old_repos: ${{ needs.get_archive_candidates.outputs.old_repos }}
      current_year: ${{ needs.get_archive_candidates.outputs.current_year }}
      current_quarter: ${{ needs.get_archive_candidates.outputs.current_quarter }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Generate GitHub App Token
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ vars.APP_ID_ISSUE_CREATOR }}
          private-key: ${{ secrets.PRIVATE_KEY_ISSUE_CREATOR }}

      - name: Get repository ID
        id: get_repo_id
        uses: octokit/graphql-action@v2.3.2
        with:
          query: |
            query($owner: String!, $name: String!) {
              repository(owner: $owner, name: $name) {
                id
              }
            }
          variables: |
            {
              "owner": ${{ env.my_org }},
              "name": ${{ env.my_repo }}
            }
        env:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}


      - name: Create parent issue
        id: create_parent_issue
        uses: octokit/graphql-action@v2.3.2
        with:
          query: |
            mutation($repoId: ID!, $title: String!, $body: String!) {
              createIssue(input: {repositoryId: $repoId, title: $title, body: $body}) {
                issue {
                  url
                  id
                }
              }
            }
          variables: |
            {
              "repoId": "${{ fromJSON(steps.get_repo_id.outputs.data).repository.id }}",
              "title": "[${{ env.current_year }} ${{ env.current_quarter }}] リポジトリ棚卸し",
              "body": ""
            }
        env:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

      - name: Create child issue data
        id: create_child_issue_data
        run: |
          echo "Creating child issue data..."
          python3 - <<EOF
          import os
          import json

          old_repos_str = os.getenv('old_repos')
          old_repos = json.loads(old_repos_str)

          threshold_last_update = os.getenv('threshold_last_update')
          my_org = os.getenv('my_org')
          my_team = os.getenv('my_team')
          current_year = os.getenv('current_year')
          current_quarter = os.getenv('current_quarter')
          year_quarter = f"[{current_year} {current_quarter} repo 棚卸し]"
          parent_issue_url = os.getenv('parent_issue_url')

          child_issue_data_list = []

          for repo in old_repos:
              repo_name = repo['name']
              admin_teams = [f"{my_org}/{t}" for t in repo['admin_teams']]
              admin_team_mentions = " ".join([f"@{t}" for t in admin_teams])
              issue_title = f"{year_quarter} 本リポジトリのアーカイブを検討する"
              issue_body = f"本リポジトリは直近 {threshold_last_update} か月間で更新がないようです。利用状況を確認し、アーカイブ可能な場合には @{my_org}/{my_team} にご連絡ください。{admin_team_mentions}\n- 依頼背景: {parent_issue_url} において Organization リポジトリの棚卸しをしており、利用されなくなったリポジトリのアーカイブを検討しています。"
              issue_data = {'repo': f"{my_org}/{repo_name}", 'admin_team_mentions': f"{admin_team_mentions}", 'issue_title': issue_title, 'issue_body': issue_body}
              child_issue_data_list.append(issue_data)

          child_issue_data_str = json.dumps(child_issue_data_list)
          with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
              fh.write(f"child_issue_data={child_issue_data_str}\n")
          EOF
        env:
          parent_issue_url: ${{ fromJSON(steps.create_parent_issue.outputs.data).createIssue.issue.url }}

      - name: Create child issues
        id: create_child_issues
        run: |
          echo "Creating child issues..."
          echo "child_issue_urls<<EOF" >> $GITHUB_ENV

          issue_data='${{ steps.create_child_issue_data.outputs.child_issue_data }}'
          length=$(echo $issue_data | jq length)

          for i in $(seq 0 $(($length - 1))); do
              repo=$(echo $issue_data | jq -r .[$i].repo)
              issue_title=$(echo $issue_data | jq -r .[$i].issue_title)
              issue_body=$(echo $issue_data | jq -r .[$i].issue_body)

              issue_url=$(gh issue create --repo "$repo" --title "$issue_title" --body "$issue_body" 2>&1)
              if [ $? -ne 0 ]; then
                  echo "Failed to create issue for $repo. Error: $issue_url"
                  exit 1
              fi
              echo $issue_url

              echo "- [ ] $issue_url\n" >> $GITHUB_ENV
          done

          echo EOF >> $GITHUB_ENV
        env:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

      - name: Update parent issue description
        uses: octokit/graphql-action@v2.3.2
        with:
          query: |
            mutation ($id: ID!, $body: String!) {
              updateIssue(input: {id: $id, body: $body}) {
                issue {
                  body
                }
              }
            }
          variables: |
            {
              "id": "${{ fromJSON(steps.create_parent_issue.outputs.data).createIssue.issue.id }}",
              "body": "下記は直近 ${{ env.threshold_last_update }} か月間で更新のないリポジトリのリストです。アーカイブを検討してください。\n${{ env.child_issue_urls }}"
            }
        env:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
```

</details>

# できたものの仕様

## 入力

- `threshold_inactive`: 「古い」と見なす最終更新日の閾値（単位: ヶ月）
- `my_team`: アーカイブタスクに責任を持つチーム。アーカイビング検討を依頼する子 issue の description にメンションが記載される

## 出力

なし

## 副作用

### Workflow 実行 repo に作られる親 issue

動作確認に使った repo が作りたてなのでクエリ対象を `直近 0 ヶ月間の間更新がないリポジトリ` としている（でないとなにも引っかからないため）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235971/4230522a-5916-0e1d-d10f-a396dcd63b37.png)

### アーカイブ対象リポジトリに作られる子 issue の例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235971/d1f0ec2e-9f00-6522-cab3-6f8dd0ccbb53.png)