├── .github ├── images │ ├── og.png │ ├── preview.png │ └── preview_ko.png ├── FUNDING.yml └── workflows │ └── fetch.yml ├── Gemfile ├── locales ├── ja.json ├── ko.json └── en.json ├── package.json ├── .gitignore ├── README-JAPANESE.md ├── README-KOREAN.md ├── Sources ├── fetch_app_status.rb ├── check_status.js ├── discord.js └── slack.js ├── README.md └── Gemfile.lock /.github/images/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/appstore-status-bot/HEAD/.github/images/og.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby "~> 2.6" 3 | 4 | gem "fastlane" 5 | gem "rubocop", require: false -------------------------------------------------------------------------------- /.github/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/appstore-status-bot/HEAD/.github/images/preview.png -------------------------------------------------------------------------------- /.github/images/preview_ko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/appstore-status-bot/HEAD/.github/images/preview_ko.png -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "Prepare for submission": "提出準備中", 3 | "Waiting for review": "審査待ち", 4 | "In review": "審査中", 5 | "Pending contract": "契約保留中", 6 | "Waiting for export compliance": "輸出コンプライアンス待ち", 7 | "Pending developer release": "デベロッパのリリース保留", 8 | "Processing for app store": "App Storeで処理中", 9 | "Pending apple release": "Appleのリリース待ち", 10 | "Ready for sale": "配信準備完了", 11 | "Rejected": "却下済み", 12 | "Metadata rejected": "メタデータ却下済み", 13 | "Removed from sale": "ストアから削除済み", 14 | "Developer rejected": "デベロッパにより却下済み", 15 | "Developer removed from sale": "デベロッパによりストアから削除済み", 16 | "Invalid binary": "Invalid Binary", 17 | "Status": "状態", 18 | "Version": "バージョン", 19 | "Message": "*{{appname}}*の状態が*{{status}}*に変更されました。 🚀" 20 | } 21 | -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "Prepare for submission": "제출 준비 중", 3 | "Waiting for review": "심사 대기 중", 4 | "In review": "심사 중", 5 | "Pending contract": "대기 중인 계약", 6 | "Waiting for export compliance": "수출 규정 관련 문서 승인 대기중", 7 | "Pending developer release": "개발자 출시 대기 중", 8 | "Processing for app store": "App Store 판매 준비중", 9 | "Pending apple release": "대기중인 앱 이전", 10 | "Ready for sale": "판매 준비됨", 11 | "Rejected": "거부됨", 12 | "Metadata rejected": "메타데이터가 거부됨", 13 | "Removed from sale": "판매가 중단됨", 14 | "Developer rejected": "개발자가 취소함", 15 | "Developer removed from sale": "개발자가 판매를 중단함", 16 | "Invalid binary": "Invalid Binary", 17 | "Status": "상태", 18 | "Version": "버전", 19 | "Message": "*{{appname}}* 앱의 상태가 *{{status}}* 으로 변경 되었습니다. 🚀" 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [techinpark] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appstore-status-bot", 3 | "version": "1.0.3", 4 | "description": "", 5 | "main": "Sources/check_status.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/techinpark/appstore-status-bot.git" 12 | }, 13 | "author": "Fernando", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/techinpark/appstore-status-bot/issues" 17 | }, 18 | "homepage": "https://github.com/techinpark/appstore-status-bot#readme", 19 | "dependencies": { 20 | "@slack/webhook": "^5.0.3", 21 | "child_process": "^1.0.2", 22 | "dirty": "^1.1.0", 23 | "i18n": "^0.13.2", 24 | "moment": "^2.29.2", 25 | "octokit": "^1.7.1", 26 | "request": "^2.88.2", 27 | "request-promise-native": "^1.0.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Prepare for submission": "Prepare for Submission", 3 | "Waiting for review": "Waiting For Review", 4 | "In review": "In Review", 5 | "Pending contract": "Pending Contract", 6 | "Waiting for export compliance": "Waiting For Export Compliance", 7 | "Pending developer release": "Pending Developer Release", 8 | "Processing for app store": "Processing for App Store", 9 | "Pending apple release": "Pending Apple Release", 10 | "Ready for sale": "Ready for Sale", 11 | "Rejected": "Rejected", 12 | "Metadata rejected": "Metadata Rejected", 13 | "Removed from sale": "Removed From Sale", 14 | "Developer rejected": "Developer Rejected", 15 | "Developer removed from sale": "Developer Removed From Sale", 16 | "Invalid binary": "Invalid Binary", 17 | "Status": "Status", 18 | "Version": "Version", 19 | "Message": "The status of your app *{{appname}}* has been changed to *{{status}}* 🚀" 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/fetch.yml: -------------------------------------------------------------------------------- 1 | name: Fetch Appstore Info 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0/15 * * * *" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2.4.1 14 | with: 15 | node-version: "16.x" 16 | env: 17 | ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "2.7" 21 | - run: gem install bundler:2.1.4 22 | - run: bundle install 23 | - run: npm install 24 | - run: gem install fastlane -v '2.219.0' 25 | - run: node Sources/check_status.js 26 | env: 27 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 28 | KEY_ID: ${{ secrets.KEY_ID }} 29 | ISSUER_ID: ${{ secrets.ISSUER_ID }} 30 | BUNDLE_ID: ${{ secrets.BUNDLE_ID }} 31 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 32 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 33 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 34 | GIST_ID: ${{ secrets.GIST_ID }} 35 | LANGUAGE: "ko" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # End of https://www.toptal.com/developers/gitignore/api/node -------------------------------------------------------------------------------- /README-JAPANESE.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ![Fetch Appstore Info](https://github.com/techinpark/appstore-status-bot/workflows/Fetch%20Appstore%20Info/badge.svg) 4 | ![stars](https://img.shields.io/github/stars/techinpark/appstore-status-bot?color=yellow&style=social) 5 | ![forks](https://img.shields.io/github/forks/techinpark/appstore-status-bot?style=social) 6 | 7 | [English Document](./README.md) 8 | 9 | # 初めに 🤷🏻‍♂️ 10 | App Store Connect status botはアプリの審査状態をSlackにメッセージを送ってあげるBotです。アプリの審査状態のチェックやチームと状態を共有したりができるよう作りました。 `github-actions` が使われて fastlaneの [Spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) ライブラリから手伝ってもらいました。ご利用なさる場合はこのリポジトリを `Fork` してください。 11 | 12 | 13 | # 追加された機能 🍯 14 | - 🚀 AppStore Connect APIを使って Appstoreの情報を読み込みします。 15 | - 📣 アプリの審査状態がSlackに共有されます。 16 | - 🌍 外国語のサポート (英語、韓国語、日本語) 17 | 18 | # プレビュー 🤖 19 | 20 | 21 | 22 | # 使用 👨🏻‍💻 23 | 24 | ## 1. APIをコールするためにはトークンをまず作ります。 25 | `KEY ID` を得るために [App Store Connect](https://appstoreconnect.apple.com/)へ接続します。 26 | 27 | 1. `ユーザとアクセス`をクリック、 `キー` タブをクリックします。 28 | 2. 新しいAPIキーを作成します。 29 | 3. `キー ID` をコピーしておきます。 30 | 4. `Issuer ID` もコピーしておきます。 31 | 5. 作られた `API Key file (.p8)` をダウンロードします。 32 | > ⚠️ ページを再読み込みすると二度とダウンロードが出来なるなるのでご注意を! 33 | 34 | ## 2. 事前準備 35 | 6. SlackのWebhook URLを発行します。 36 | 7. このリポジトリをForkします。 37 | 38 | 39 | ## 3. `Secrets`の設定 40 | 41 | - リポジトリの設定から `Settings` - `Secrets and variables` - `New repository secret` 順番にコピーした項目を設定します。 42 | 43 | ### コピーした項目の設定 44 | 45 | > PRIVATE_KEY: ダウンロードした `key file(.p8)`をテキストに開いて全部コピペして入れます。 46 | > KEY_ID : `キー ID`をここに入力します。 47 | > ISSUER_ID : `Issuer ID`もここに入力します。 48 | > BUNDLE_ID : 状態の確認したいアプリの `bundle identifier`を入力します。 (2個以上のアプリの場合は、「 」 スペースを入れずに、「,」記号を使うと動作します。) 49 | > 2個以上のアプリの場合は、カンマ記号を使い、スペースを入れずに入力してください 50 | > SLACK_WEBHOOK : SlackのWebhook URLを入力します。 51 | > DISCORD_WEBHOOK : DiscordのWebhook URLを入力します。 (optional) 52 | > GH_TOKEN: Githubのトークンを入力します。 (`gists`と `repo` 権限が必要です。 ) 53 | > GIST_ID: gistファイルを作成し、 URLに存在するキーをコピーして入力します。 54 | - https://gist.github.com/techinpark/**9842e074b8ee46aef76fd0d493bae0ed** 55 | 56 | ## 4. 言語設定、インターバル設定 57 | 58 | - [fetch.yml](./.github/workflows/fetch.yml) 59 | 60 | `workflow` ファイルに言語設定、スケジュールの設定ができます。基本 `15分`で動いてます。 61 | 62 | 63 | # レファレンス 🙇🏻‍♂️ 64 | 65 | - https://github.com/fastlane/fastlane/tree/master/spaceship 66 | - https://github.com/erikvillegas/itunes-connect-slack 67 | - https://github.com/rogerluan/app-store-connect-notifier 68 | 69 | 70 | # コントリビュート 71 | - オープンソースなので全てのPR大歓迎です。 🤩 72 | -------------------------------------------------------------------------------- /README-KOREAN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ![Fetch Appstore Info](https://github.com/techinpark/appstore-status-bot/workflows/Fetch%20Appstore%20Info/badge.svg) 4 | ![stars](https://img.shields.io/github/stars/techinpark/appstore-status-bot?color=yellow&style=social) 5 | ![forks](https://img.shields.io/github/forks/techinpark/appstore-status-bot?style=social) 6 | 7 | [English Document](./README.md) 8 | 9 | # 소개 🤷🏻‍♂️ 10 | App Store Connect status bot 은 앱스토어에 올라가 있는 나의 앱 심사 상태를 가져와 슬랙으로 메세지를 전송해주는 간단한 봇 입니다. 개인앱의 심사 상태를 체크하고 싶거나 팀원들에게 앱의 심사 상태를 공유하고 싶을때 사용하기 좋게 만들었습니다. `github-actions` 를 사용하였으며 fastlane 의 [Spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) 라이브러리의 도움을 받았습니다. 사용을 하시려면 해당 레포지토리를 `Fork` 하시면 됩니다. 참 쉽죠? 11 | 12 | 13 | # 추가된 기능들 🍯 14 | - 🚀 앱스토어 커넥트 API를 사용하여 앱스토어 정보를 가져옵니다. 15 | - 📣 앱의 심사상태를 슬랙을 통해 공유할 수 있습니다. 16 | - 🌍 다국어가 지원됩니다. (영어 , 한국어) 17 | 18 | # 미리보기 🤖 19 | 20 | 21 | 22 | # 사용법 👨🏻‍💻 23 | 24 | ## 1. API를 호출하기 위해서는 토큰을 먼저 생성합니다. 25 | `KEY ID` 를 얻기 위해서는 먼저 [App Store Connect](https://appstoreconnect.apple.com/) 에 접속합니다. 26 | 27 | 1. `사용자 및 액세스`를 선택하고, `키` 탭을 선택합니다. 28 | 2. 새로운 API키를 생성합니다. 29 | 3. `키 ID` 를 선택해서 복사 해둡니다. 30 | 4. `Issuer ID` 도 선택해서 복사를 해둡니다. 31 | 5. 생성된 `API Key file (.p8)` 을 다운로드 합니다. 32 | > ⚠️ 페이지를 새로고침하면 다시 다운로드 할 수 없으니 주의해주세요! 33 | 34 | ## 2. 사전 준비 35 | 6. 슬랙 Webhook URL 발급 받습니다. 36 | 7. 해당 레포지토리를 Fork 합니다. 37 | 38 | 39 | ## 3. `Secrets` 설정하기 40 | 41 | - 깃헙 레포 페이지에서 `Settings` - `Secrets and variables` - `New repository secret` 로 들어가서 위에서 복사한 정보들을 세팅해줍니다. 42 | 43 | ### 복사해야하는 정보들 44 | 45 | > PRIVATE_KEY: 다운로드한 `key file(.p8)`을 텍스트로 열어서 복사한후 넣어주시면 됩니다. 46 | > KEY_ID : `키 ID`를 이곳에 입력합니다. 47 | > ISSUER_ID : `Issuer ID`도 이곳에 입력합니다. 48 | > BUNDLE_ID : 상태를 확인하고 싶은 앱의 `bundle identifier` 을 입력해줍니다. (공백 없이 콤마로 구분하시면 2개이상의 앱도 가능합니다.) 49 | > SLACK_WEBHOOK : 슬랙 Webhook URL을 넣어줍니다. 50 | > DISCORD_WEBHOOK : 디스코드 Webhook URL을 넣어줍니다. (optional) 51 | > GH_TOKEN: 깃헙 토큰을 넣어줍니다 (`gists` 와 `repo` 권한이 필요합니다 ) 52 | > GIST_ID: gist파일을 생성하고 URL에 존재하는 키값을 복사해서 넣어줍니다. 53 | - https://gist.github.com/techinpark/**9842e074b8ee46aef76fd0d493bae0ed** 54 | 55 | ## 4. 언어 설정 및 탐색 주기 설정 56 | 57 | - [fetch.yml](./.github/workflows/fetch.yml) 58 | 59 | `workflow` 파일 내부에서 언어 설정 및 스케줄 시간을 설정 하실 수 있습니다. 기본값은 `15분` 단위로 되어있습니다. 60 | 61 | 62 | # 레퍼런스 🙇🏻‍♂️ 63 | 64 | - https://github.com/fastlane/fastlane/tree/master/spaceship 65 | - https://github.com/erikvillegas/itunes-connect-slack 66 | - https://github.com/rogerluan/app-store-connect-notifier 67 | 68 | 69 | # 기여하기 70 | - 오픈소스이므로 모든 PR은 환영합니다. 🤩 71 | -------------------------------------------------------------------------------- /Sources/fetch_app_status.rb: -------------------------------------------------------------------------------- 1 | require "spaceship" 2 | require "json" 3 | require 'tempfile' 4 | 5 | def get_app_state(app) 6 | 7 | edit_version_info = app.get_edit_app_store_version 8 | in_review_version_info = app.get_in_review_app_store_version 9 | pending_version_info = app.get_pending_release_app_store_version 10 | latest_version_info = app.get_latest_app_store_version 11 | 12 | version_string = "" 13 | app_store_state = "" 14 | 15 | if edit_version_info.nil? == false 16 | version_string = edit_version_info.version_string 17 | app_store_state = edit_version_info.app_store_state.gsub("_", " ").capitalize 18 | elsif in_review_version_info.nil? == false 19 | version_string = in_review_version_info.version_string 20 | app_store_state = in_review_version_info.app_store_state.gsub("_", " ").capitalize 21 | elsif pending_version_info.nil? == false 22 | version_string = pending_version_info.version_string 23 | app_store_state = pending_version_info.app_store_state.gsub("_", " ").capitalize 24 | elsif latest_version_info.nil? == false 25 | version_string = latest_version_info.version_string 26 | app_store_state = latest_version_info.app_store_state.gsub("_", " ").capitalize 27 | end 28 | 29 | icon_url = "" 30 | live_version_info = app.get_live_app_store_version 31 | if live_version_info.nil? == false 32 | icon_url = live_version_info.build.icon_asset_token["templateUrl"] 33 | icon_url["{w}"] = "340" 34 | icon_url["{h}"] = "340" 35 | icon_url["{f}"] = "png" 36 | end 37 | 38 | { 39 | "name" => app.name, 40 | "version" => version_string, 41 | "status" => app_store_state, 42 | "appID" => app.id, 43 | "iconURL" => icon_url 44 | } 45 | 46 | end 47 | 48 | def get_app_version_from(bundle_id) 49 | apps = [] 50 | if bundle_id 51 | apps.push(Spaceship::ConnectAPI::App.find(bundle_id)) 52 | else 53 | apps = Spaceship::ConnectAPI::App.all 54 | end 55 | apps.compact.map { |app| get_app_state(app) } 56 | end 57 | 58 | 59 | # Create temp file. 60 | p8 = ENV['PRIVATE_KEY'] 61 | p8_file = Tempfile.new('AuthKey') 62 | p8_file.write(p8) 63 | p8_file.rewind 64 | 65 | bundle_id = ENV['BUNDLE_ID'] 66 | versions = [] 67 | 68 | token = Spaceship::ConnectAPI::Token.create( 69 | key_id: ENV['KEY_ID'], 70 | issuer_id: ENV['ISSUER_ID'], 71 | filepath: File.absolute_path(p8_file.path) 72 | ) 73 | 74 | 75 | Spaceship::ConnectAPI.token = token 76 | 77 | bundle_id_array = bundle_id.to_s.split(",") 78 | 79 | if bundle_id_array.length.zero? 80 | versions += get_app_version_from(nil) 81 | else 82 | bundle_id_array.each do |bundle_id| 83 | versions += get_app_version_from(bundle_id) 84 | end 85 | end 86 | 87 | puts JSON.dump versions 88 | p8_file.unlink 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | ![Fetch Appstore Info](https://github.com/techinpark/appstore-status-bot/workflows/Fetch%20Appstore%20Info/badge.svg) 5 | ![stars](https://img.shields.io/github/stars/techinpark/appstore-status-bot?color=yellow&style=social) 6 | ![forks](https://img.shields.io/github/forks/techinpark/appstore-status-bot?style=social) 7 | 8 | [한국어로 보기](./README-KOREAN.md) 9 | 10 | # Introduce 🤷🏻‍♂️ 11 | App Store Connect status bot is a simple bot script fetches your app info directly from App Store Connect and post changes in slack as a bot using `github-actions`, help of fastlane [Spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) 12 | For using this bot, Just `fork` this repository is Super Easy 13 | 14 | 15 | # Features 🍯 16 | - 🚀 Fetch appstore connect info using apppstore connect API 17 | - 📣 Share your application `status` information to your slack workspace 18 | - 🌍 `Localization` support (`english`, `korean`) 19 | 20 | # Preview 🤖 21 | 22 | 23 | 24 | # Usage 👨🏻‍💻 25 | 26 | ## 1. Generating Tokens for API Requests 27 | To get your Key ID, copy it from App Store Connect by logging in to [App Store Connect](https://appstoreconnect.apple.com/), then: 28 | 29 | 1. Select Users and Access, then select the API Keys tab. 30 | 2. The key IDs appear in a column under the Active heading. Hover the cursor next to a key ID to display the Copy Key ID link. 31 | 3. Click Copy Key ID and paste it. 32 | 4. Click Copy Issuer ID and paste it. 33 | 5. Download the newly created API Key file (.p8) 34 | > ⚠️ This file cannot be downloaded again after the page has been refreshed 35 | 36 | 6. Generate Slack Webhook token. 37 | 7. Fork this repository. 38 | 39 | ## 3. Setting Secrets with your keys. 40 | 41 | - Go to `Settings` - `Secrets and variables` - `New repository secret` 42 | 43 | ### Secret Values 44 | 45 | > PRIVATE_KEY: Input raw data about your API Key file (.p8) 46 | > KEY_ID : Input Appstore connect `key_id` 47 | > ISSUER_ID : Input Appstore connect `issuer_id` 48 | > BUNDLE_ID : Input your bundle_identifier of application you can input multiple bundle_id with comma and no whitespace 49 | > SLACK_WEBHOOK : Input your slack webhook url 50 | > DISCORD_WEBHOOK : Input your discord webhook url (optional) 51 | > GH_TOKEN: Input your github token, (need `gists` and `repo` scope). 52 | > GIST_ID: Input portion from your gist url: 53 | - https://gist.github.com/techinpark/**9842e074b8ee46aef76fd0d493bae0ed** 54 | 55 | 56 | ## 4. Configure fetch timing or languages 57 | 58 | - [fetch.yml](./.github/workflows/fetch.yml) 59 | 60 | In `workflow` file, can change lanauges and fetch schedule default `schedule` is every 15 minutes. 61 | 62 | 63 | # References 🙇🏻‍♂️ 64 | 65 | - https://github.com/fastlane/fastlane/tree/master/spaceship 66 | - https://github.com/erikvillegas/itunes-connect-slack 67 | - https://github.com/rogerluan/app-store-connect-notifier 68 | 69 | 70 | # Contribution 71 | - Feel free to contribution for this project. 72 | - Every `PR`, `Issues` is wellcome. 🤩 73 | -------------------------------------------------------------------------------- /Sources/check_status.js: -------------------------------------------------------------------------------- 1 | const slack = require("./slack.js"); 2 | const discord = require("./discord.js"); 3 | const exec = require("child_process").exec; 4 | const dirty = require("dirty"); 5 | const { Octokit, App } = require("octokit"); 6 | const request = require("request-promise-native"); 7 | const { prependOnceListener } = require("process"); 8 | const fs = require("fs").promises; 9 | const env = Object.create(process.env); 10 | const octokit = new Octokit({ auth: `token ${process.env.GH_TOKEN}` }); 11 | 12 | const main = async () => { 13 | await getGist(); 14 | 15 | exec( 16 | "ruby Sources/fetch_app_status.rb", 17 | { env: env }, 18 | function (err, stdout, stderr) { 19 | if (stdout) { 20 | var apps = JSON.parse(stdout); 21 | console.log(apps); 22 | for (let app of apps) { 23 | checkVersion(app); 24 | } 25 | } else { 26 | console.log("There was a problem fetching the status of the app!"); 27 | console.log(stderr); 28 | } 29 | } 30 | ); 31 | }; 32 | 33 | const checkVersion = async (app) => { 34 | var appInfoKey = "appInfo-" + app.appID; 35 | var submissionStartKey = "submissionStart" + app.appID; 36 | 37 | const db = dirty("store.db"); 38 | db.on("load", async function () { 39 | var lastAppInfo = db.get(appInfoKey); 40 | if (!lastAppInfo || lastAppInfo.status != app.status) { 41 | console.log("[*] status is different"); 42 | 43 | slack.post(app, db.get(submissionStartKey)); 44 | discord.post(app, db.get(submissionStartKey)); 45 | 46 | if (app.status == "Waiting For Review") { 47 | db.set(submissionStartKey, new Date()); 48 | } 49 | } else { 50 | console.log("[*] status is same"); 51 | } 52 | 53 | db.set(appInfoKey, app); 54 | 55 | try { 56 | const data = await fs.readFile("store.db", "utf-8"); 57 | await updateGist(data); 58 | } catch (error) { 59 | console.log(error); 60 | } 61 | }); 62 | }; 63 | 64 | const getGist = async () => { 65 | const gist = await octokit.rest.gists 66 | .get({ 67 | gist_id: process.env.GIST_ID, 68 | }) 69 | .catch((error) => console.error(`[*] Unable to update gist\n${error}`)); 70 | if (!gist) return; 71 | 72 | const filename = Object.keys(gist.data.files)[0]; 73 | const rawdataURL = gist.data.files[filename].raw_url; 74 | 75 | const options = { 76 | url: rawdataURL, 77 | }; 78 | 79 | const result = await request.get(options); 80 | try { 81 | await fs.writeFile("store.db", result); 82 | console.log("[*] file saved!"); 83 | } catch (error) { 84 | console.log(error); 85 | } 86 | }; 87 | 88 | const updateGist = async (content) => { 89 | const gist = await octokit.rest.gists 90 | .get({ 91 | gist_id: process.env.GIST_ID, 92 | }) 93 | .catch((error) => console.error(`[*] Unable to update gist\n${error}`)); 94 | if (!gist) return; 95 | 96 | const filename = Object.keys(gist.data.files)[0]; 97 | await octokit.rest.gists.update({ 98 | gist_id: process.env.GIST_ID, 99 | files: { 100 | [filename]: { 101 | content: content, 102 | }, 103 | }, 104 | }); 105 | }; 106 | 107 | main(); 108 | -------------------------------------------------------------------------------- /Sources/discord.js: -------------------------------------------------------------------------------- 1 | const moment = require("moment"); 2 | const path = require("path"); 3 | const fetch = require("node-fetch"); 4 | const { I18n } = require("i18n"); 5 | 6 | const webhookURL = process.env.DISCORD_WEBHOOK; 7 | const language = process.env.LANGUAGE; 8 | const i18n = new I18n(); 9 | 10 | i18n.configure({ 11 | locales: ['en', 'ko', 'ja'], 12 | directory: path.join(__dirname, '../locales'), 13 | defaultLocale: 'en' 14 | }); 15 | 16 | i18n.setLocale(language || 'en'); 17 | 18 | function post(appInfo, submissionStartDate) { 19 | if (!webhookURL) return; 20 | const status = i18n.__(appInfo.status); 21 | const message = i18n.__("Message", { appname: appInfo.name, status: status }); 22 | const embed = discordEmbed(appInfo, submissionStartDate); 23 | 24 | hook(message, embed); 25 | } 26 | 27 | async function hook(message, embed) { 28 | const payload = { 29 | content: message, 30 | embeds: [embed] 31 | }; 32 | 33 | await fetch(webhookURL, { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | body: JSON.stringify(payload), 39 | }); 40 | } 41 | 42 | function discordEmbed(appInfo, submissionStartDate) { 43 | const embed = { 44 | title: "App Store Connect", 45 | author: { 46 | name: appInfo.name, 47 | icon_url: appInfo.iconURL 48 | }, 49 | url: `https://appstoreconnect.apple.com/apps/${appInfo.appID}/appstore`, 50 | fields: [ 51 | { 52 | name: i18n.__("Version"), 53 | value: appInfo.version, 54 | inline: true, 55 | }, 56 | { 57 | name: i18n.__("Status"), 58 | value: i18n.__(appInfo.status), 59 | inline: true, 60 | } 61 | ], 62 | footer: { 63 | text: "appstore-status-bot", 64 | icon_url: "https://icons-for-free.com/iconfiles/png/512/app+store+apple+apps+game+games+store+icon-1320085881005897327.png" 65 | }, 66 | timestamp: new Date() 67 | }; 68 | 69 | // Set elapsed time since "Waiting For Review" start 70 | if ( 71 | submissionStartDate && 72 | appInfo.status !== "Prepare for Submission" && 73 | appInfo.status !== "Waiting For Review" 74 | ) { 75 | const elapsedHours = moment().diff(moment(submissionStartDate), "hours"); 76 | embed.fields.push({ 77 | name: "Elapsed Time", 78 | value: `${elapsedHours} hours`, 79 | inline: true, 80 | }); 81 | } 82 | 83 | embed.color = colorForStatus(appInfo.status); 84 | 85 | return embed; 86 | } 87 | 88 | function colorForStatus(status) { 89 | const infoColor = 0x8e8e8e; 90 | const warningColor = 0xf4f124; 91 | const successColor1 = 0x1eb6fc; 92 | const successColor2 = 0x14ba40; 93 | const failureColor = 0xe0143d; 94 | const colorMapping = { 95 | "Prepare for Submission": infoColor, 96 | "Waiting For Review": infoColor, 97 | "In Review": successColor1, 98 | "Pending Contract": warningColor, 99 | "Waiting For Export Compliance": warningColor, 100 | "Pending Developer Release": successColor2, 101 | "Processing for App Store": successColor2, 102 | "Pending Apple Release": successColor2, 103 | "Ready for Sale": successColor2, 104 | Rejected: failureColor, 105 | "Metadata Rejected": failureColor, 106 | "Removed From Sale": failureColor, 107 | "Developer Rejected": failureColor, 108 | "Developer Removed From Sale": failureColor, 109 | "Invalid Binary": failureColor, 110 | }; 111 | 112 | return colorMapping[status]; 113 | } 114 | 115 | module.exports = { 116 | post: post, 117 | }; 118 | -------------------------------------------------------------------------------- /Sources/slack.js: -------------------------------------------------------------------------------- 1 | const moment = require("moment"); 2 | const path = require("path"); 3 | const { IncomingWebhook } = require("@slack/webhook"); 4 | const { I18n } = require("i18n"); 5 | 6 | const webhookURL = process.env.SLACK_WEBHOOK; 7 | const language = process.env.LANGUAGE; 8 | const i18n = new I18n(); 9 | 10 | i18n.configure({ 11 | locales: ['en','ko', 'ja'], 12 | directory: path.join(__dirname, '../locales'), 13 | defaultLocale: 'en' 14 | }); 15 | 16 | i18n.setLocale(language || 'en'); 17 | 18 | function post(appInfo, submissionStartDate) { 19 | const status = i18n.__(appInfo.status); 20 | const message = i18n.__("Message", { appname: appInfo.name, status: status }); 21 | const attachment = slackAttachment(appInfo, submissionStartDate); 22 | 23 | const params = { 24 | attachments: [attachment], 25 | as_user: "true", 26 | }; 27 | 28 | hook(message, attachment); 29 | } 30 | 31 | async function hook(message, attachment) { 32 | 33 | if (!webhookURL) { 34 | console.log("No Slack webhook URL provided."); 35 | return; 36 | } 37 | 38 | const webhook = new IncomingWebhook(webhookURL, {}); 39 | await webhook.send({ 40 | text: message, 41 | attachments: [attachment], 42 | }); 43 | } 44 | 45 | function slackAttachment(appInfo, submissionStartDate) { 46 | const attachment = { 47 | fallback: `The status of your app ${appInfo.name} has been changed to ${appInfo.status}`, 48 | color: colorForStatus(appInfo.status), 49 | title: "App Store Connect", 50 | author_name: appInfo.name, 51 | author_icon: appInfo.iconURL, 52 | title_link: `https://appstoreconnect.apple.com/apps/${appInfo.appID}/appstore`, 53 | fields: [ 54 | { 55 | title: i18n.__("Version"), 56 | value: appInfo.version, 57 | short: true, 58 | }, 59 | { 60 | title: i18n.__("Status"), 61 | value: i18n.__(appInfo.status), 62 | short: true, 63 | }, 64 | ], 65 | footer: "appstore-status-bot", 66 | footer_icon: 67 | "https://icons-for-free.com/iconfiles/png/512/app+store+apple+apps+game+games+store+icon-1320085881005897327.png", 68 | ts: new Date().getTime() / 1000, 69 | }; 70 | 71 | // Set elapsed time since "Waiting For Review" start 72 | if ( 73 | submissionStartDate && 74 | appInfo.status != "Prepare for Submission" && 75 | appInfo.status != "Waiting For Review" 76 | ) { 77 | const elapsedHours = moment().diff(moment(submissionStartDate), "hours"); 78 | attachment["fields"].push({ 79 | title: "Elapsed Time", 80 | value: `${elapsedHours} hours`, 81 | short: true, 82 | }); 83 | } 84 | return attachment; 85 | } 86 | 87 | function colorForStatus(status) { 88 | const infoColor = "#8e8e8e"; 89 | const warningColor = "#f4f124"; 90 | const successColor1 = "#1eb6fc"; 91 | const successColor2 = "#14ba40"; 92 | const failureColor = "#e0143d"; 93 | const colorMapping = { 94 | "Prepare for Submission": infoColor, 95 | "Waiting For Review": infoColor, 96 | "In Review": successColor1, 97 | "Pending Contract": warningColor, 98 | "Waiting For Export Compliance": warningColor, 99 | "Pending Developer Release": successColor2, 100 | "Processing for App Store": successColor2, 101 | "Pending Apple Release": successColor2, 102 | "Ready for Sale": successColor2, 103 | Rejected: failureColor, 104 | "Metadata Rejected": failureColor, 105 | "Removed From Sale": failureColor, 106 | "Developer Rejected": failureColor, 107 | "Developer Removed From Sale": failureColor, 108 | "Invalid Binary": failureColor, 109 | }; 110 | 111 | return colorMapping[status]; 112 | } 113 | 114 | module.exports = { 115 | post: post, 116 | }; 117 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | addressable (2.7.0) 6 | public_suffix (>= 2.0.2, < 5.0) 7 | ast (2.4.1) 8 | atomos (0.1.3) 9 | aws-eventstream (1.1.0) 10 | aws-partitions (1.374.0) 11 | aws-sdk-core (3.107.0) 12 | aws-eventstream (~> 1, >= 1.0.2) 13 | aws-partitions (~> 1, >= 1.239.0) 14 | aws-sigv4 (~> 1.1) 15 | jmespath (~> 1.0) 16 | aws-sdk-kms (1.38.0) 17 | aws-sdk-core (~> 3, >= 3.99.0) 18 | aws-sigv4 (~> 1.1) 19 | aws-sdk-s3 (1.81.0) 20 | aws-sdk-core (~> 3, >= 3.104.3) 21 | aws-sdk-kms (~> 1) 22 | aws-sigv4 (~> 1.1) 23 | aws-sigv4 (1.2.2) 24 | aws-eventstream (~> 1, >= 1.0.2) 25 | babosa (1.0.3) 26 | claide (1.0.3) 27 | colored (1.2) 28 | colored2 (3.1.2) 29 | commander-fastlane (4.4.6) 30 | highline (~> 1.7.2) 31 | declarative (0.0.20) 32 | declarative-option (0.1.0) 33 | digest-crc (0.6.1) 34 | rake (~> 13.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.7.6) 38 | emoji_regex (3.0.0) 39 | excon (0.76.0) 40 | faraday (1.0.1) 41 | multipart-post (>= 1.2, < 3) 42 | faraday-cookie_jar (0.0.7) 43 | faraday (>= 0.8.0) 44 | http-cookie (~> 1.0.0) 45 | faraday_middleware (1.0.0) 46 | faraday (~> 1.0) 47 | fastimage (2.2.0) 48 | fastlane (2.160.0) 49 | CFPropertyList (>= 2.3, < 4.0.0) 50 | addressable (>= 2.3, < 3.0.0) 51 | aws-sdk-s3 (~> 1.0) 52 | babosa (>= 1.0.3, < 2.0.0) 53 | bundler (>= 1.12.0, < 3.0.0) 54 | colored 55 | commander-fastlane (>= 4.4.6, < 5.0.0) 56 | dotenv (>= 2.1.1, < 3.0.0) 57 | emoji_regex (>= 0.1, < 4.0) 58 | excon (>= 0.71.0, < 1.0.0) 59 | faraday (~> 1.0) 60 | faraday-cookie_jar (~> 0.0.6) 61 | faraday_middleware (~> 1.0) 62 | fastimage (>= 2.1.0, < 3.0.0) 63 | gh_inspector (>= 1.1.2, < 2.0.0) 64 | google-api-client (>= 0.37.0, < 0.39.0) 65 | google-cloud-storage (>= 1.15.0, < 2.0.0) 66 | highline (>= 1.7.2, < 2.0.0) 67 | json (< 3.0.0) 68 | jwt (>= 2.1.0, < 3) 69 | mini_magick (>= 4.9.4, < 5.0.0) 70 | multipart-post (~> 2.0.0) 71 | plist (>= 3.1.0, < 4.0.0) 72 | rubyzip (>= 2.0.0, < 3.0.0) 73 | security (= 0.1.3) 74 | simctl (~> 1.6.3) 75 | slack-notifier (>= 2.0.0, < 3.0.0) 76 | terminal-notifier (>= 2.0.0, < 3.0.0) 77 | terminal-table (>= 1.4.5, < 2.0.0) 78 | tty-screen (>= 0.6.3, < 1.0.0) 79 | tty-spinner (>= 0.8.0, < 1.0.0) 80 | word_wrap (~> 1.0.0) 81 | xcodeproj (>= 1.13.0, < 2.0.0) 82 | xcpretty (~> 0.3.0) 83 | xcpretty-travis-formatter (>= 0.0.3) 84 | gh_inspector (1.1.3) 85 | google-api-client (0.38.0) 86 | addressable (~> 2.5, >= 2.5.1) 87 | googleauth (~> 0.9) 88 | httpclient (>= 2.8.1, < 3.0) 89 | mini_mime (~> 1.0) 90 | representable (~> 3.0) 91 | retriable (>= 2.0, < 4.0) 92 | signet (~> 0.12) 93 | google-cloud-core (1.5.0) 94 | google-cloud-env (~> 1.0) 95 | google-cloud-errors (~> 1.0) 96 | google-cloud-env (1.3.3) 97 | faraday (>= 0.17.3, < 2.0) 98 | google-cloud-errors (1.0.1) 99 | google-cloud-storage (1.29.0) 100 | addressable (~> 2.5) 101 | digest-crc (~> 0.4) 102 | google-api-client (~> 0.33) 103 | google-cloud-core (~> 1.2) 104 | googleauth (~> 0.9) 105 | mini_mime (~> 1.0) 106 | googleauth (0.13.1) 107 | faraday (>= 0.17.3, < 2.0) 108 | jwt (>= 1.4, < 3.0) 109 | memoist (~> 0.16) 110 | multi_json (~> 1.11) 111 | os (>= 0.9, < 2.0) 112 | signet (~> 0.14) 113 | highline (1.7.10) 114 | http-cookie (1.0.3) 115 | domain_name (~> 0.5) 116 | httpclient (2.8.3) 117 | jmespath (1.4.0) 118 | json (2.3.1) 119 | jwt (2.2.2) 120 | memoist (0.16.2) 121 | mini_magick (4.10.1) 122 | mini_mime (1.0.2) 123 | multi_json (1.15.0) 124 | multipart-post (2.0.0) 125 | nanaimo (0.3.0) 126 | naturally (2.2.0) 127 | os (1.1.1) 128 | parallel (1.19.2) 129 | parser (2.7.1.4) 130 | ast (~> 2.4.1) 131 | plist (3.5.0) 132 | public_suffix (4.0.6) 133 | rainbow (3.0.0) 134 | rake (13.0.1) 135 | regexp_parser (1.8.0) 136 | representable (3.0.4) 137 | declarative (< 0.1.0) 138 | declarative-option (< 0.2.0) 139 | uber (< 0.2.0) 140 | retriable (3.1.2) 141 | rexml (3.2.4) 142 | rouge (2.0.7) 143 | rubocop (0.91.1) 144 | parallel (~> 1.10) 145 | parser (>= 2.7.1.1) 146 | rainbow (>= 2.2.2, < 4.0) 147 | regexp_parser (>= 1.7) 148 | rexml 149 | rubocop-ast (>= 0.4.0, < 1.0) 150 | ruby-progressbar (~> 1.7) 151 | unicode-display_width (>= 1.4.0, < 2.0) 152 | rubocop-ast (0.4.2) 153 | parser (>= 2.7.1.4) 154 | ruby-progressbar (1.10.1) 155 | rubyzip (2.3.0) 156 | security (0.1.3) 157 | signet (0.14.0) 158 | addressable (~> 2.3) 159 | faraday (>= 0.17.3, < 2.0) 160 | jwt (>= 1.5, < 3.0) 161 | multi_json (~> 1.10) 162 | simctl (1.6.8) 163 | CFPropertyList 164 | naturally 165 | slack-notifier (2.3.2) 166 | terminal-notifier (2.0.0) 167 | terminal-table (1.8.0) 168 | unicode-display_width (~> 1.1, >= 1.1.1) 169 | tty-cursor (0.7.1) 170 | tty-screen (0.8.1) 171 | tty-spinner (0.9.3) 172 | tty-cursor (~> 0.7) 173 | uber (0.1.0) 174 | unf (0.1.4) 175 | unf_ext 176 | unf_ext (0.0.7.7) 177 | unicode-display_width (1.7.0) 178 | word_wrap (1.0.0) 179 | xcodeproj (1.18.0) 180 | CFPropertyList (>= 2.3.3, < 4.0) 181 | atomos (~> 0.1.3) 182 | claide (>= 1.0.2, < 2.0) 183 | colored2 (~> 3.1) 184 | nanaimo (~> 0.3.0) 185 | xcpretty (0.3.0) 186 | rouge (~> 2.0.7) 187 | xcpretty-travis-formatter (1.0.0) 188 | xcpretty (~> 0.2, >= 0.0.7) 189 | 190 | PLATFORMS 191 | ruby 192 | 193 | DEPENDENCIES 194 | fastlane 195 | rubocop 196 | 197 | RUBY VERSION 198 | ruby 2.6.4p104 199 | 200 | BUNDLED WITH 201 | 2.1.4 202 | --------------------------------------------------------------------------------