├── test ├── github-api-cache │ ├── ahrefs_monorobot_release_tag_master │ ├── sewenthy_monorobot_release_tag_master │ ├── sewenthy_monorobot_release_tag_119-unfurling-commit-range-links │ ├── ahrefs_monorobot_release_tag_yasu_slack-msg-fix-escaping-and-fallback │ ├── ahrefs_monorobot_issue_124 │ ├── sewenthy_monorobot_release_tag_v1.0.0 │ ├── sewenthy_monorobot_branch_master │ ├── sewenthy_monorobot_branch_119-unfurling-commit-range-links │ ├── d95302addd66c1816bce1b1d495ed1c93ccd478 │ ├── 78492c2467876259d787538d600cfa0b18a2b814 │ ├── ahrefs_monorepo_commit_0d95302addd66c1816bce1b1d495ed1c93ccd478 │ ├── ahrefs_monorepo_commit_7e0a933e9c71b4ca107680ca958ca1888d5e479b │ ├── ahrefs_monorobot_commit_0d95302addd66c1816bce1b1d495ed1c93ccd478 │ ├── 184a35dd234e846cb5fe0723063f8bba03262e43 │ ├── ahrefs_monorobot_pull_107 │ ├── 6113728f27ae82c7b1a177c8d03f9e96e0adf246 │ ├── xinyuluo_monorepo_commit_cd5b85afa306840e0790b62e349ee1f828b2a3c2 │ ├── 2129f69a03641c39dfeb8e928cc08d7a8d029c6b │ ├── xinyuluo_monorepo_commit_41dad1d3d41f329f00836f166a7103a262e69889 │ ├── ahrefs_monorobot_branch_master │ ├── ahrefs_monorobot_branch_yasu_slack-msg-fix-escaping-and-fallback │ └── xinyuluo_monorepo_commit_1edcdf5e45a8b7ed01c247b981d8f42d426a7794 ├── secrets.json ├── dune ├── longest_prefix_test.ml ├── monorobot.json ├── buildkite-api-cache │ ├── organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5e-4e4a-9e0b-b8b64e381c90_logs │ ├── organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5f-44e0-ae9b-0b6f33f48ac8_logs │ └── organizations_org_pipelines_pipeline2_builds_181733 └── test.ml ├── lib ├── text_cleanup │ ├── test │ │ ├── dune │ │ ├── text_cleanup_bin │ │ │ ├── dune │ │ │ └── main.ml │ │ └── job_log.t │ │ │ ├── log3 │ │ │ ├── log │ │ │ ├── organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5e-4e4a-9e0b-b8b64e381c90_logs │ │ │ └── organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5f-44e0-ae9b-0b6f33f48ac8_logs │ ├── dune │ ├── text_cleanup.ml │ └── lexer.mll ├── colors.ml ├── debug_db.atd ├── common.atd ├── api.ml ├── dune ├── state.atd ├── rule.atd ├── atd_adapters.ml ├── debug_db.ml ├── buildkite.atd ├── context.ml ├── common.ml ├── config.atd └── slack.atd ├── monorobot ├── monorobot.opam.template ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── src ├── gen_version.sh ├── dune └── request_handler.ml ├── mock_states ├── pull_request_review.approved.json ├── pull_request_review.commented.json ├── pull_request_review.request_changes.json ├── pull_request_review.submitted_comment.json ├── pull_request_review_comment.created.json ├── pull_request.labeled_two_labels_with_thread.json ├── status.success_no_previous_builds.json ├── status.state_hide_success_test_disallowed_pipeline.json ├── status.state_hide_success_test.json ├── status.failure_test_no_failed_builds_chan.json ├── status.failure_test_main_branch.json ├── status.commit1-02-failed_diff_pipeline.json ├── status.commit1-02-failed_main_branch.json ├── status.commit1-02-failed_no_failed_builds_chan.json ├── status.commit1-02-failed.json ├── status.success_fix_failed_builds.json ├── status.failure_test.json ├── status.failed_no_build_state.json ├── status.canceled_no_build_state.json ├── status.failed_multiple_branches.json ├── status.failed_empty_failed_jobs.json ├── status.canceled_empty_failed_jobs.json ├── status.canceled_diff_failed_jobs.json └── status.failed_diff_failed_jobs.json ├── .ocamlformat ├── Makefile ├── mock_slack_events ├── issue.open.json ├── pr.multiple_involved.json ├── compare.repo_and_forked_branches_ahead.json ├── compare.simple_branches.json ├── commits.one_file_modified.json ├── compare.repo_and_forked_branches_behind.json └── compare.repo_and_forked_branch_with_repo_name.json ├── dune-project ├── monorobot.opam ├── db └── sql │ └── failed_builds_webhook.sql └── documentation └── secret_docs.md /test/github-api-cache/ahrefs_monorobot_release_tag_master: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/text_cleanup/test/dune: -------------------------------------------------------------------------------- 1 | (cram (deps %{bin:text_cleanup})) -------------------------------------------------------------------------------- /test/github-api-cache/sewenthy_monorobot_release_tag_master: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /monorobot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | dune exec -- ./src/monorobot.exe $@ 4 | -------------------------------------------------------------------------------- /lib/text_cleanup/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name text_cleanup)) 3 | 4 | (ocamllex lexer) 5 | -------------------------------------------------------------------------------- /test/github-api-cache/sewenthy_monorobot_release_tag_119-unfurling-commit-range-links: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorobot_release_tag_yasu_slack-msg-fix-escaping-and-fallback: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /monorobot.opam.template: -------------------------------------------------------------------------------- 1 | available: [ os-family != "windows" & os != "macos" & os != "openbsd"] 2 | -------------------------------------------------------------------------------- /test/secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "repos": [ 3 | { 4 | "url": "" 5 | } 6 | ], 7 | "slack_access_token": "" 8 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.merlin 3 | *.install 4 | src/version.ml 5 | _opam 6 | .vscode 7 | .DS_Store 8 | secrets.json 9 | db/monorobot.db 10 | state.json 11 | -------------------------------------------------------------------------------- /lib/text_cleanup/test/text_cleanup_bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (public_name text_cleanup) 4 | (package monorobot) 5 | (libraries text_cleanup monorobotlib)) -------------------------------------------------------------------------------- /lib/text_cleanup/test/job_log.t/log3: -------------------------------------------------------------------------------- 1 | 2 | [?25lInstalling to existing venv 'something' 3 | 4 | 5 | [?25h[09:52:01 #] sudo aptitude install -y package1 package2 6 | 7 |  8 | -------------------------------------------------------------------------------- /src/gen_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$(git describe --always --tags --dirty=+M) 4 | cat > version.ml <<- EOF 5 | (* do not edit; generated by gen_version.sh *) 6 | 7 | let current = "$VERSION" 8 | EOF 9 | -------------------------------------------------------------------------------- /lib/colors.ml: -------------------------------------------------------------------------------- 1 | (* https://styleguide.github.com/primer/utilities/colors/#background-colors *) 2 | 3 | let gray = "#f6f8fa" 4 | let blue = "#0366d6" 5 | let yellow = "#ffd33d" 6 | let red = "#d73a49" 7 | let green = "#28a745" 8 | let purple = "#6f42c1" 9 | -------------------------------------------------------------------------------- /mock_states/pull_request_review.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": {}, 3 | "pipeline_commits": {}, 4 | "slack_threads": { 5 | "https://github.com/Khady/monorepo/pull/6": [ 6 | { 7 | "channel": "default", 8 | "ts": "1728399554.482879", 9 | "cid": "Cdefault" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mock_states/pull_request_review.commented.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": {}, 3 | "pipeline_commits": {}, 4 | "slack_threads": { 5 | "https://github.com/xinyuluo/monorepo/pull/3": [ 6 | { 7 | "channel": "frontend-bot", 8 | "ts": "1728399554.482879", 9 | "cid": "Cfebot" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mock_states/pull_request_review.request_changes.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": {}, 3 | "pipeline_commits": {}, 4 | "slack_threads": { 5 | "https://github.com/Khady/monorepo/pull/6": [ 6 | { 7 | "channel": "default", 8 | "ts": "1728399554.482879", 9 | "cid": "Cdefault" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mock_states/pull_request_review.submitted_comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": {}, 3 | "pipeline_commits": {}, 4 | "slack_threads": { 5 | "https://github.com/xinyuluo/monorepo/pull/3": [ 6 | { 7 | "channel": "frontend-bot", 8 | "ts": "1728399554.482879", 9 | "cid": "Cfebot" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (libraries 3 | monorobotlib 4 | cmdliner 5 | devkit 6 | devkit.core 7 | extlib 8 | lwt.unix 9 | mirage-crypto-rng.unix 10 | uri 11 | unix) 12 | (preprocess 13 | (pps lwt_ppx)) 14 | (public_name monorobot)) 15 | 16 | (rule 17 | (targets version.ml) 18 | (deps (universe) gen_version.sh) 19 | (action 20 | (run "./gen_version.sh"))) 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description of the task 2 | 3 | Please describe the task related to this PR. Or the reason behind the 4 | change. 5 | 6 | ## How to test 7 | 8 | How to test the changes? Which command to run? Which output is expected? 9 | 10 | ``` 11 | ./monorobot check mock_payloads/SOME_FILE 12 | ``` 13 | 14 | ## References 15 | 16 | - existing issue: 17 | - Slack discussion: 18 | - other? 19 | -------------------------------------------------------------------------------- /lib/text_cleanup/test/text_cleanup_bin/main.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | In_channel.with_open_bin Sys.argv.(1) (fun file -> 3 | let content = In_channel.input_all file in 4 | let content = 5 | if Array.length Sys.argv = 3 && Sys.argv.(2) = "--json" then 6 | (Monorobotlib.Buildkite_j.job_log_of_string content).content 7 | else content 8 | in 9 | 10 | content |> Text_cleanup.cleanup |> Printf.printf "%S") 11 | -------------------------------------------------------------------------------- /mock_states/pull_request_review_comment.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": {}, 3 | "pipeline_commits": {}, 4 | "slack_threads": { 5 | "https://github.com/xinyuluo/monorepo/pull/4": [ 6 | { 7 | "channel": "a1-bot", 8 | "ts": "1728399554.482879", 9 | "cid": "Ca1bot" 10 | }, 11 | { 12 | "channel": "backend", 13 | "ts": "1728399554.444444", 14 | "cid": "Cbackend" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /mock_states/pull_request.labeled_two_labels_with_thread.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": {}, 3 | "pipeline_commits": {}, 4 | "slack_threads": { 5 | "https://github.com/xinyuluo/monorepo/pull/4": [ 6 | { 7 | "channel": "a1-bot", 8 | "ts": "1728399554.482879", 9 | "cid": "Ca1bot" 10 | }, 11 | { 12 | "channel": "backend", 13 | "ts": "1728399554.444444", 14 | "cid": "Cbackend" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.26.2 2 | profile=default 3 | ocaml-version=4.14.0 4 | 5 | break-cases=toplevel # deprecated 6 | break-infix-before-func=false 7 | break-infix=fit-or-vertical 8 | cases-exp-indent=2 9 | doc-comments=before 10 | exp-grouping=preserve 11 | leading-nested-match-parens=true 12 | let-and=sparse 13 | m=120 14 | max-indent=2 15 | module-item-spacing=preserve # deprecated 16 | nested-match=align # deprecated 17 | parens-ite=true 18 | parens-tuple=multi-line-only 19 | type-decl=sparse 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build clean start default fmt test 2 | 3 | default: build 4 | 5 | start: 6 | dune exec -- ./src/monorobot.exe 7 | 8 | build: 9 | dune build src/monorobot.exe 10 | 11 | watch: 12 | dune build -w src/monorobot.exe 13 | 14 | release: 15 | dune build --profile=release src/monorobot.exe 16 | 17 | test: 18 | dune runtest 19 | 20 | test_promote: 21 | dune runtest --auto-promote 22 | 23 | all: build 24 | 25 | fmt: 26 | dune build @fmt --auto-promote 27 | 28 | clean: 29 | dune clean 30 | -------------------------------------------------------------------------------- /lib/text_cleanup/text_cleanup.ml: -------------------------------------------------------------------------------- 1 | let lex lexer string = 2 | let lexbuf = Lexing.from_string string in 3 | let buf = Buffer.create (String.length string) in 4 | lexer buf lexbuf; 5 | Buffer.contents buf 6 | 7 | let cleanup string = 8 | let string = lex Lexer.cleanup_emoji_choice string in 9 | let string = lex Lexer.cleanup_crlf string in 10 | let string = lex Lexer.cleanup_esc_codes string in 11 | let string = lex Lexer.cleanup_cr string in 12 | let string = lex Lexer.cleanup_double_lf string in 13 | string 14 | -------------------------------------------------------------------------------- /lib/debug_db.atd: -------------------------------------------------------------------------------- 1 | type commit_hash = abstract 2 | type status_state = abstract 3 | type branch = abstract 4 | 5 | (* Similar to the github.atd status_notification, but removing the commit and repository fields, 6 | since they are quite verbose and not needed for debugging *) 7 | type status_notification = { 8 | id: int; 9 | name: string; 10 | sha: commit_hash; 11 | state: status_state; 12 | ?description: string nullable; 13 | ?target_url: string nullable; 14 | context: string; 15 | branches: branch list; 16 | updated_at: string; 17 | } 18 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorobot_issue_124: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "login": "Khady", 4 | "id": 974142, 5 | "url": "https://api.github.com/users/Khady", 6 | "html_url": "https://github.com/Khady", 7 | "avatar_url": "https://avatars.githubusercontent.com/u/974142?v=4" 8 | }, 9 | "number": 124, 10 | "body": "Currently running ocaml 4.09, could move to ocaml 4.14.\r\n\r\nMight also be possible to enable some kind of caching.", 11 | "title": "Update the CI to a recent ocaml", 12 | "html_url": "https://github.com/ahrefs/monorobot/issues/124", 13 | "labels": [], 14 | "state": "open" 15 | } 16 | -------------------------------------------------------------------------------- /lib/common.atd: -------------------------------------------------------------------------------- 1 | type 'v map_as_object = 2 | (string * 'v) list 3 | wrap 4 | 5 | type 'v int_map_as_object = 6 | (string * 'v) list 7 | wrap 8 | 9 | type 'v table_as_object = 10 | (string * 'v) list 11 | wrap 12 | 13 | type string_set = 14 | string list 15 | wrap 16 | 17 | type failed_step = abstract 18 | 19 | type failed_step_set = 20 | failed_step list 21 | wrap 22 | -------------------------------------------------------------------------------- /mock_states/status.success_no_previous_builds.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "3": { 6 | "status": "pending", 7 | "build_number": "3", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/3", 9 | "commit": { 10 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 11 | "author": "mail@example.org", 12 | "commit_message": "update readme.md" 13 | }, 14 | "is_finished": false, 15 | "failed_steps": [], 16 | "created_at": "2024-06-02t04:57:47+00:00", 17 | "finished_at": "2024-06-02t04:58:47+00:00" 18 | } 19 | } 20 | } 21 | }, 22 | "pipeline_commits": {}, 23 | "slack_threads": {} 24 | } 25 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names test github_link_test longest_prefix_test) 3 | (libraries monorobotlib devkit devkit.core extlib lwt.unix yojson) 4 | (preprocess 5 | (pps lwt_ppx))) 6 | 7 | (rule 8 | (deps 9 | (env_var PRINT_TEST_STATE) 10 | (source_tree ../mock_states) 11 | (source_tree ../mock_payloads) 12 | (source_tree ../mock_slack_events) 13 | (source_tree github-api-cache) 14 | (source_tree buildkite-api-cache) 15 | (source_tree slack-api-cache) 16 | monorobot.json 17 | secrets.json) 18 | (action 19 | (with-stdout-to 20 | slack_payloads.out 21 | (run ./test.exe)))) 22 | 23 | (rule 24 | (alias runtest) 25 | (action 26 | (diff slack_payloads.expected slack_payloads.out))) 27 | 28 | (rule 29 | (alias runtest) 30 | (action 31 | (run ./github_link_test.exe))) 32 | 33 | (rule 34 | (alias runtest) 35 | (action 36 | (run ./longest_prefix_test.exe))) 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: build and test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - ubuntu-22.04 14 | ocaml-compiler: 15 | - 4.14 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 24 | uses: ocaml/setup-ocaml@v3 25 | with: 26 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 27 | dune-cache: true 28 | allow-prerelease-opam: true 29 | 30 | - run: opam install . ocamlformat.0.26.2 31 | 32 | - name: compile 33 | run: opam exec -- make 34 | 35 | - name: run test 36 | run: opam exec -- make test 37 | 38 | - name: check ocamlformat 39 | run: opam exec -- make fmt 40 | -------------------------------------------------------------------------------- /mock_states/status.state_hide_success_test_disallowed_pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "master": { 5 | "2": { 6 | "status": "failure", 7 | "build_number": "2", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/2", 9 | "commit": { 10 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 11 | "author": "mail@example.org", 12 | "commit_message": "Update README.md" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/2#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | } 26 | }, 27 | "pipeline_commits": {}, 28 | "slack_threads": {} 29 | } 30 | -------------------------------------------------------------------------------- /mock_states/status.state_hide_success_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "default": { 4 | "master": { 5 | "2": { 6 | "status": "failure", 7 | "build_number": "2", 8 | "build_url": "https://buildkite.com/org/default/builds/2", 9 | "commit": { 10 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 11 | "author": "mail@example.org", 12 | "commit_message": "Update README.md" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "default/failed-step", 18 | "build_url": "https://buildkite.com/org/default/builds/2#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | }, 26 | "pipeline2": { 27 | "master": {} 28 | } 29 | }, 30 | "pipeline_commits": {}, 31 | "slack_threads": {} 32 | } 33 | -------------------------------------------------------------------------------- /mock_states/status.failure_test_no_failed_builds_chan.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "bau": { 4 | "master": { 5 | "2": { 6 | "status": "pending", 7 | "build_number": "2", 8 | "build_url": "https://buildkite.com/org/bau/builds/2", 9 | "commit": { 10 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 11 | "author": "mail@example.org", 12 | "commit_message": "Update README.md" 13 | }, 14 | "is_finished": false, 15 | "failed_steps": [ 16 | { 17 | "name": "bau/failed-step", 18 | "build_url": "https://buildkite.com/org/bau/builds/2#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | } 26 | }, 27 | "pipeline_commits": { 28 | "bau": { 29 | "master": { 30 | "s1": ["0d95302addd66c1816bce1b1d495ed1c93ccd478"], 31 | "s2": [] 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mock_states/status.failure_test_main_branch.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "2": { 6 | "status": "pending", 7 | "build_number": "2", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/2", 9 | "commit": { 10 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 11 | "author": "mail@example.org", 12 | "commit_message": "Update README.md" 13 | }, 14 | "is_finished": false, 15 | "failed_steps": [ 16 | { 17 | "name": "pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/2#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | } 26 | }, 27 | "pipeline_commits": { 28 | "pipeline2": { 29 | "develop": { 30 | "s1": ["0d95302addd66c1816bce1b1d495ed1c93ccd478"], 31 | "s2": [] 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mock_states/status.commit1-02-failed_diff_pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "qa": { 4 | "author/patches/js-storage": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/qa/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "qa/failed-step", 18 | "build_url": "https://buildkite.com/org/qa/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | } 26 | }, 27 | "pipeline_commits": { 28 | "qa": { 29 | "author/patches/js-storage": { 30 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 31 | "s2": [] 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mock_states/status.commit1-02-failed_main_branch.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "pending", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": false, 15 | "failed_steps": [ 16 | { 17 | "name": "pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | } 26 | }, 27 | "pipeline_commits": { 28 | "pipeline2": { 29 | "develop": { 30 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 31 | "s2": [] 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mock_states/status.commit1-02-failed_no_failed_builds_chan.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "bau": { 4 | "author/patches/js-storage": { 5 | "181732": { 6 | "status": "pending", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/bau/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": false, 15 | "failed_steps": [ 16 | { 17 | "name": "bau/failed-step", 18 | "build_url": "https://buildkite.com/org/bau/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | } 26 | }, 27 | "pipeline_commits": { 28 | "bau": { 29 | "author/patches/js-storage": { 30 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 31 | "s2": [] 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mock_slack_events/issue.open.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "team_id": "T0475L7BATY", 4 | "api_app_id": "A047JBWSX26", 5 | "event": { 6 | "type": "link_shared", 7 | "user": "U046XN0M2R5", 8 | "channel": "C047QTRD1CH", 9 | "message_ts": "1666772724.077999", 10 | "links": [ 11 | { 12 | "url": "https://github.com/ahrefs/monorobot/issues/124", 13 | "domain": "github.com" 14 | } 15 | ], 16 | "source": "conversations_history", 17 | "unfurl_id": "C047QTRD1CH.1666772724.077999.ee3d267ed655f98a9676ad00db7664503a791604bcd6540447bea666329bd18d", 18 | "is_bot_user_member": false, 19 | "event_ts": "1666772724.409821" 20 | }, 21 | "type": "event_callback", 22 | "event_id": "Ev04876SDGJY", 23 | "event_time": 1666772724, 24 | "authorizations": [ 25 | { 26 | "enterprise_id": null, 27 | "team_id": "T0475L7BATY", 28 | "user_id": "U047X1F32SD", 29 | "is_bot": true, 30 | "is_enterprise_install": false 31 | } 32 | ], 33 | "is_ext_shared_channel": false, 34 | "event_context": "4-eyJldCI6Imxpbmtfc2hhcmVkIiwidGlkIjoiVDA0NzVMN0JBVFkiLCJhaWQiOiJBMDQ3SkJXU1gyNiIsImNpZCI6IkMwNDdRVFJEMUNIIn0" 35 | } 36 | -------------------------------------------------------------------------------- /mock_slack_events/pr.multiple_involved.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "team_id": "T0475L7BATY", 4 | "api_app_id": "A047JBWSX26", 5 | "event": { 6 | "type": "link_shared", 7 | "user": "U046XN0M2R5", 8 | "channel": "C047QTRD1CH", 9 | "message_ts": "1666771760.054409", 10 | "links": [ 11 | { 12 | "url": "https://github.com/ahrefs/monorobot/pull/107/", 13 | "domain": "github.com" 14 | } 15 | ], 16 | "source": "conversations_history", 17 | "unfurl_id": "C047QTRD1CH.1666771760.054409.1c923c289cf7a3dbc473bcd56f5687b448e6c53f57be71b2bc85eafcb71be3f7", 18 | "is_bot_user_member": false, 19 | "event_ts": "1666771760.435898" 20 | }, 21 | "type": "event_callback", 22 | "event_id": "Ev048H9WH0DP", 23 | "event_time": 1666771760, 24 | "authorizations": [ 25 | { 26 | "enterprise_id": null, 27 | "team_id": "T0475L7BATY", 28 | "user_id": "U047X1F32SD", 29 | "is_bot": true, 30 | "is_enterprise_install": false 31 | } 32 | ], 33 | "is_ext_shared_channel": false, 34 | "event_context": "4-eyJldCI6Imxpbmtfc2hhcmVkIiwidGlkIjoiVDA0NzVMN0JBVFkiLCJhaWQiOiJBMDQ3SkJXU1gyNiIsImNpZCI6IkMwNDdRVFJEMUNIIn0" 35 | } 36 | -------------------------------------------------------------------------------- /mock_states/status.commit1-02-failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "author/patches/js-storage": { 5 | "181732": { 6 | "status": "pending", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": false, 15 | "failed_steps": [ 16 | { 17 | "name": "pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | } 25 | } 26 | }, 27 | "pipeline_commits": { 28 | "pipeline2": { 29 | "author/patches/js-storage": { 30 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 31 | "s2": [] 32 | } 33 | } 34 | }, 35 | "slack_threads": {} 36 | } 37 | -------------------------------------------------------------------------------- /mock_slack_events/compare.repo_and_forked_branches_ahead.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "team_id": "T0475L7BATY", 4 | "api_app_id": "A047JBWSX26", 5 | "event": { 6 | "type": "link_shared", 7 | "user": "U046XN0M2R5", 8 | "channel": "C047QTRD1CH", 9 | "message_ts": "1666839755.099699", 10 | "links": [ 11 | { 12 | "url": "https://github.com/sewenthy/monorobot/compare/master...ahrefs:master", 13 | "domain": "github.com" 14 | } 15 | ], 16 | "source": "conversations_history", 17 | "unfurl_id": "C047QTRD1CH.1666839755.099699.5f56034bfeb3b02b4a9be7559ab15e1bd673f1d83c908768937b79e00beac5a4", 18 | "is_bot_user_member": false, 19 | "event_ts": "1666839755.580221" 20 | }, 21 | "type": "event_callback", 22 | "event_id": "Ev0483GEJB6J", 23 | "event_time": 1666839755, 24 | "authorizations": [ 25 | { 26 | "enterprise_id": null, 27 | "team_id": "T0475L7BATY", 28 | "user_id": "U047X1F32SD", 29 | "is_bot": true, 30 | "is_enterprise_install": false 31 | } 32 | ], 33 | "is_ext_shared_channel": false, 34 | "event_context": "4-eyJldCI6Imxpbmtfc2hhcmVkIiwidGlkIjoiVDA0NzVMN0JBVFkiLCJhaWQiOiJBMDQ3SkJXU1gyNiIsImNpZCI6IkMwNDdRVFJEMUNIIn0" 35 | } 36 | -------------------------------------------------------------------------------- /mock_slack_events/compare.simple_branches.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "team_id": "T0475L7BATY", 4 | "api_app_id": "A047JBWSX26", 5 | "event": { 6 | "type": "link_shared", 7 | "user": "U046XN0M2R5", 8 | "channel": "C047QTRD1CH", 9 | "message_ts": "1666773089.275449", 10 | "links": [ 11 | { 12 | "url": "https://github.com/ahrefs/monorobot/compare/master...yasu/slack-msg-fix-escaping-and-fallback", 13 | "domain": "github.com" 14 | } 15 | ], 16 | "source": "conversations_history", 17 | "unfurl_id": "C047QTRD1CH.1666773089.275449.a48ccd950c3122178618023ad492de1307ae3b7b249cc315b46c9c617ca6b40d", 18 | "is_bot_user_member": false, 19 | "event_ts": "1666773089.663926" 20 | }, 21 | "type": "event_callback", 22 | "event_id": "Ev0481R38J69", 23 | "event_time": 1666773089, 24 | "authorizations": [ 25 | { 26 | "enterprise_id": null, 27 | "team_id": "T0475L7BATY", 28 | "user_id": "U047X1F32SD", 29 | "is_bot": true, 30 | "is_enterprise_install": false 31 | } 32 | ], 33 | "is_ext_shared_channel": false, 34 | "event_context": "4-eyJldCI6Imxpbmtfc2hhcmVkIiwidGlkIjoiVDA0NzVMN0JBVFkiLCJhaWQiOiJBMDQ3SkJXU1gyNiIsImNpZCI6IkMwNDdRVFJEMUNIIn0" 35 | } 36 | -------------------------------------------------------------------------------- /mock_slack_events/commits.one_file_modified.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "unknown_token", 3 | "team_id": "T0475L7BATY", 4 | "api_app_id": "A047JBWSX26", 5 | "event": { 6 | "type": "link_shared", 7 | "user": "U046XN0M2R5", 8 | "channel": "C047QTRD1CH", 9 | "message_ts": "1666757730.846499", 10 | "links": [ 11 | { 12 | "url": "https://github.com/ahrefs/monorobot/commit/0d95302addd66c1816bce1b1d495ed1c93ccd478", 13 | "domain": "github.com" 14 | } 15 | ], 16 | "source": "conversations_history", 17 | "unfurl_id": "C047QTRD1CH.1666757730.846499.c8818b241b981c0a62367919445fd5f184ccd340f0bc7b103e8d31e86ce1ee0f", 18 | "is_bot_user_member": false, 19 | "event_ts": "1666757731.442870" 20 | }, 21 | "type": "event_callback", 22 | "event_id": "Ev047X9A1V6J", 23 | "event_time": 1666757731, 24 | "authorizations": [ 25 | { 26 | "enterprise_id": null, 27 | "team_id": "T0475L7BATY", 28 | "user_id": "U047X1F32SD", 29 | "is_bot": true, 30 | "is_enterprise_install": false 31 | } 32 | ], 33 | "is_ext_shared_channel": false, 34 | "event_context": "4-eyJldCI6Imxpbmtfc2hhcmVkIiwidGlkIjoiVDA0NzVMN0JBVFkiLCJhaWQiOiJBMDQ3SkJXU1gyNiIsImNpZCI6IkMwNDdRVFJEMUNIIn0" 35 | } 36 | -------------------------------------------------------------------------------- /mock_slack_events/compare.repo_and_forked_branches_behind.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "team_id": "T0475L7BATY", 4 | "api_app_id": "A047JBWSX26", 5 | "event": { 6 | "type": "link_shared", 7 | "user": "U046XN0M2R5", 8 | "channel": "C047QTRD1CH", 9 | "message_ts": "1666839755.099699", 10 | "links": [ 11 | { 12 | "url": "https://github.com/ahrefs/monorobot/compare/master...sewenthy:119-unfurling-commit-range-links", 13 | "domain": "github.com" 14 | } 15 | ], 16 | "source": "conversations_history", 17 | "unfurl_id": "C047QTRD1CH.1666839755.099699.5f56034bfeb3b02b4a9be7559ab15e1bd673f1d83c908768937b79e00beac5a4", 18 | "is_bot_user_member": false, 19 | "event_ts": "1666839755.580221" 20 | }, 21 | "type": "event_callback", 22 | "event_id": "Ev0483GEJB6J", 23 | "event_time": 1666839755, 24 | "authorizations": [ 25 | { 26 | "enterprise_id": null, 27 | "team_id": "T0475L7BATY", 28 | "user_id": "U047X1F32SD", 29 | "is_bot": true, 30 | "is_enterprise_install": false 31 | } 32 | ], 33 | "is_ext_shared_channel": false, 34 | "event_context": "4-eyJldCI6Imxpbmtfc2hhcmVkIiwidGlkIjoiVDA0NzVMN0JBVFkiLCJhaWQiOiJBMDQ3SkJXU1gyNiIsImNpZCI6IkMwNDdRVFJEMUNIIn0" 35 | } 36 | -------------------------------------------------------------------------------- /mock_slack_events/compare.repo_and_forked_branch_with_repo_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "team_id": "T0475L7BATY", 4 | "api_app_id": "A047JBWSX26", 5 | "event": { 6 | "type": "link_shared", 7 | "user": "U046XN0M2R5", 8 | "channel": "C047QTRD1CH", 9 | "message_ts": "1666839755.099699", 10 | "links": [ 11 | { 12 | "url": "https://github.com/ahrefs/monorobot/compare/master...sewenthy:monorobot:119-unfurling-commit-range-links", 13 | "domain": "github.com" 14 | } 15 | ], 16 | "source": "conversations_history", 17 | "unfurl_id": "C047QTRD1CH.1666839755.099699.5f56034bfeb3b02b4a9be7559ab15e1bd673f1d83c908768937b79e00beac5a4", 18 | "is_bot_user_member": false, 19 | "event_ts": "1666839755.580221" 20 | }, 21 | "type": "event_callback", 22 | "event_id": "Ev0483GEJB6J", 23 | "event_time": 1666839755, 24 | "authorizations": [ 25 | { 26 | "enterprise_id": null, 27 | "team_id": "T0475L7BATY", 28 | "user_id": "U047X1F32SD", 29 | "is_bot": true, 30 | "is_enterprise_install": false 31 | } 32 | ], 33 | "is_ext_shared_channel": false, 34 | "event_context": "4-eyJldCI6Imxpbmtfc2hhcmVkIiwidGlkIjoiVDA0NzVMN0JBVFkiLCJhaWQiOiJBMDQ3SkJXU1gyNiIsImNpZCI6IkMwNDdRVFJEMUNIIn0" 35 | } 36 | -------------------------------------------------------------------------------- /test/longest_prefix_test.ml: -------------------------------------------------------------------------------- 1 | open Monorobotlib 2 | open Github_t 3 | open Slack_message 4 | 5 | let make_test_file filename = 6 | { 7 | sha = "something something"; 8 | filename; 9 | status = "something something"; 10 | additions = 5; 11 | deletions = 5; 12 | changes = 10; 13 | url = "test_link"; 14 | } 15 | 16 | let single_file = [ make_test_file "testdir1/testdir2/changed_file.txt" ] 17 | 18 | let multiple_files_same_dir = 19 | List.map make_test_file 20 | [ 21 | "testdir1/testdir2/changed_file2.txt"; 22 | "testdir1/testdir2/changed_file3.txt"; 23 | "testdir1/testdir2/changed_file1.txt"; 24 | ] 25 | 26 | let multiple_files_common_root_dir = 27 | List.map make_test_file [ "testdir1/changed_file2.txt"; "backend/changed_file3.txt"; "changed_file1.txt" ] 28 | 29 | let multiple_files_common_dir = 30 | List.map make_test_file 31 | [ "backend/something/changed_file2.txt"; "backend/something/changed_file3.txt"; "backend/some/changed_file1.txt" ] 32 | 33 | let () = 34 | assert (condense_file_changes single_file = "_modified `testdir1/testdir2/changed_file.txt` (+5-5)_"); 35 | assert (condense_file_changes multiple_files_same_dir = "modified 3 files in `testdir1/testdir2/`"); 36 | assert (condense_file_changes multiple_files_common_root_dir = "modified 3 files"); 37 | assert (condense_file_changes multiple_files_common_dir = "modified 3 files in `backend/`") 38 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.9) 2 | 3 | (implicit_transitive_deps false) 4 | 5 | (generate_opam_files true) 6 | 7 | (formatting 8 | (enabled_for ocaml reason)) 9 | 10 | (cram enable) 11 | 12 | (name monorobot) 13 | 14 | (using menhir 2.0) 15 | 16 | (license "MIT") 17 | 18 | (version "0.1") 19 | 20 | (source 21 | (github ahrefs/monorobot)) 22 | 23 | (authors "Ahrefs Pte Ltd ") 24 | 25 | (maintainers "Ahrefs Pte Ltd ") 26 | 27 | (package 28 | (name monorobot) 29 | (synopsis "Notification bot for monorepos") 30 | (description 31 | "Notification bot to handle webhook events from monorepos and post notifications to Slack.") 32 | (depends 33 | (ocaml 34 | (>= 4.14.0)) 35 | (atd 36 | (>= 2.14.0)) 37 | (atdgen 38 | (>= 2.14.0)) 39 | (atdgen-runtime 40 | (>= 2.14.0)) 41 | (base64 42 | (>= 3.0.0)) 43 | biniou 44 | (cmdliner 45 | (>= 1.1.0)) 46 | (digestif 47 | (>= 1.2.0)) 48 | (devkit 49 | (>= 1.20240429)) 50 | (extlib 51 | (>= 1.7.8)) 52 | (lwt 53 | (>= 5.7.0)) 54 | (lwt_ppx 55 | (>= 2.0.0)) 56 | (ptime 57 | (>= 1.2.0)) 58 | (ocamldiff 59 | (>= 1.2)) 60 | (sqlite3 61 | (>= 5.1.0)) 62 | (sqlgg 63 | (>= 20231201)) 64 | (re2 65 | (>= 0.16.0)) 66 | uri 67 | (omd 68 | (< 2)) 69 | yojson 70 | (ocamlformat 71 | (and 72 | :with-dev-setup 73 | (= 0.26.2))) 74 | (curl 75 | (>= 0.10.0)) 76 | (sexplib0 77 | (>= v0.16.0)))) 78 | -------------------------------------------------------------------------------- /mock_states/status.success_fix_failed_builds.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "2": { 6 | "status": "failure", 7 | "build_number": "2", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/2", 9 | "commit": { 10 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 11 | "author": "mail@example.org", 12 | "commit_message": "Update README.md" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/2#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | }, 24 | "3": { 25 | "status": "pending", 26 | "build_number": "3", 27 | "build_url": "https://buildkite.com/org/pipeline2/builds/3", 28 | "commit": { 29 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 30 | "author": "mail@example.org", 31 | "commit_message": "Update README.md" 32 | }, 33 | "is_finished": false, 34 | "failed_steps": [], 35 | "created_at": "2024-06-02T04:57:47+00:00", 36 | "finished_at": "2024-06-02T04:58:47+00:00" 37 | } 38 | } 39 | } 40 | }, 41 | "pipeline_commits": {}, 42 | "slack_threads": {} 43 | } 44 | -------------------------------------------------------------------------------- /monorobot.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "0.1" 4 | synopsis: "Notification bot for monorepos" 5 | description: 6 | "Notification bot to handle webhook events from monorepos and post notifications to Slack." 7 | maintainer: ["Ahrefs Pte Ltd "] 8 | authors: ["Ahrefs Pte Ltd "] 9 | license: "MIT" 10 | homepage: "https://github.com/ahrefs/monorobot" 11 | bug-reports: "https://github.com/ahrefs/monorobot/issues" 12 | depends: [ 13 | "dune" {>= "3.9"} 14 | "ocaml" {>= "4.14.0"} 15 | "atd" {>= "2.14.0"} 16 | "atdgen" {>= "2.14.0"} 17 | "atdgen-runtime" {>= "2.14.0"} 18 | "base64" {>= "3.0.0"} 19 | "biniou" 20 | "cmdliner" {>= "1.1.0"} 21 | "digestif" {>= "1.2.0"} 22 | "devkit" {>= "1.20240429"} 23 | "extlib" {>= "1.7.8"} 24 | "lwt" {>= "5.7.0"} 25 | "lwt_ppx" {>= "2.0.0"} 26 | "ptime" {>= "1.2.0"} 27 | "ocamldiff" {>= "1.2"} 28 | "sqlite3" {>= "5.1.0"} 29 | "sqlgg" {>= "20231201"} 30 | "re2" {>= "0.16.0"} 31 | "uri" 32 | "omd" {< "2"} 33 | "yojson" 34 | "ocamlformat" {with-dev-setup & = "0.26.2"} 35 | "curl" {>= "0.10.0"} 36 | "sexplib0" {>= "v0.16.0"} 37 | "odoc" {with-doc} 38 | ] 39 | build: [ 40 | ["dune" "subst"] {dev} 41 | [ 42 | "dune" 43 | "build" 44 | "-p" 45 | name 46 | "-j" 47 | jobs 48 | "@install" 49 | "@runtest" {with-test} 50 | "@doc" {with-doc} 51 | ] 52 | ] 53 | dev-repo: "git+https://github.com/ahrefs/monorobot.git" 54 | available: [ os-family != "windows" & os != "macos" & os != "openbsd"] 55 | -------------------------------------------------------------------------------- /mock_states/status.failure_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "master": { 5 | "1": { 6 | "status": "failure", 7 | "build_number": "1", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/1", 9 | "commit": { 10 | "sha": "ee5c539cad37c77348ce7a55756acc542b41cfc7", 11 | "author": "mail@example.org", 12 | "commit_message": "first build" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/1#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | }, 24 | "2": { 25 | "status": "pending", 26 | "build_number": "2", 27 | "build_url": "https://buildkite.com/org/pipeline2/builds/2", 28 | "commit": { 29 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 30 | "author": "mail@example.org", 31 | "commit_message": "Update README.md" 32 | }, 33 | "is_finished": false, 34 | "failed_steps": [ 35 | { 36 | "name": "pipeline2/failed-step", 37 | "build_url": "https://buildkite.com/org/pipeline2/builds/1#0192347d-e4ee-4072-9da4-f441eeb65ed4" 38 | } 39 | ], 40 | "created_at": "2024-06-02T04:57:47+00:00", 41 | "finished_at": "2024-06-02T04:58:47+00:00" 42 | } 43 | } 44 | } 45 | }, 46 | "pipeline_commits": { 47 | "pipeline2": { 48 | "master": { 49 | "s1": ["0d95302addd66c1816bce1b1d495ed1c93ccd478"], 50 | "s2": [] 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/text_cleanup/lexer.mll: -------------------------------------------------------------------------------- 1 | {} 2 | 3 | let digit = ['0' - '9'] 4 | 5 | let esc = '\027' 6 | 7 | let timestamp = esc "_bk;t=" digit* '\007' 8 | 9 | let color_code = esc '[' (digit | ';') * ['a'-'z' 'A'-'Z'] 10 | 11 | let cursor_show_hide = esc "[?25" ('l' | 'h') 12 | 13 | let to_delete = timestamp | color_code | cursor_show_hide 14 | let space = ' ' | '\t' 15 | 16 | rule cleanup_esc_codes buf = parse 17 | | esc "[K" ([^'\n']*) '\n' 18 | | '\n' ([^'\n']*) ((esc "[1K")) 19 | | '\n' ([^'\n']*) esc "[2K" ([^'\n']*) '\n' 20 | { Buffer.add_char buf '\n'; 21 | cleanup_esc_codes buf lexbuf } 22 | | to_delete { cleanup_esc_codes buf lexbuf } 23 | | esc 24 | { Buffer.add_string buf "\\027"; 25 | cleanup_esc_codes buf lexbuf } 26 | | _ 27 | { Buffer.add_string buf (Lexing.lexeme lexbuf); 28 | cleanup_esc_codes buf lexbuf } 29 | | eof { () } 30 | 31 | and cleanup_crlf buf = parse 32 | | ('\r'+) '\n' 33 | { Buffer.add_char buf '\n'; 34 | cleanup_crlf buf lexbuf } 35 | | _ 36 | { Buffer.add_string buf (Lexing.lexeme lexbuf); 37 | cleanup_crlf buf lexbuf } 38 | | eof { () } 39 | 40 | and cleanup_cr buf = parse 41 | | ((_ # ['\n''\r'])* ) '\r' 42 | { cleanup_cr buf lexbuf } 43 | | _ 44 | { Buffer.add_string buf (Lexing.lexeme lexbuf); 45 | cleanup_cr buf lexbuf } 46 | | eof { () } 47 | 48 | and cleanup_double_lf buf = parse 49 | | '\n' (space*) ('\n'+) 50 | { Buffer.add_char buf '\n'; 51 | cleanup_double_lf buf lexbuf } 52 | | _ 53 | { Buffer.add_string buf (Lexing.lexeme lexbuf); 54 | cleanup_double_lf buf lexbuf } 55 | | eof { () } 56 | 57 | and cleanup_emoji_choice buf = parse 58 | | "^^^ +++" 59 | { cleanup_emoji_choice_state_2 buf lexbuf } 60 | | _ 61 | { Buffer.add_string buf (Lexing.lexeme lexbuf); 62 | cleanup_emoji_choice buf lexbuf } 63 | | eof { () } 64 | 65 | and cleanup_emoji_choice_state_2 buf = parse 66 | | "^^^ +++" | eof { () } 67 | | _ 68 | { Buffer.add_string buf (Lexing.lexeme lexbuf); 69 | cleanup_emoji_choice_state_2 buf lexbuf } -------------------------------------------------------------------------------- /test/github-api-cache/sewenthy_monorobot_release_tag_v1.0.0: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/sewenthy/monorobot/releases/81507155", 3 | "assets_url": "https://api.github.com/repos/sewenthy/monorobot/releases/81507155/assets", 4 | "upload_url": "https://uploads.github.com/repos/sewenthy/monorobot/releases/81507155/assets{?name,label}", 5 | "html_url": "https://github.com/sewenthy/monorobot/releases/tag/v1.0.0", 6 | "id": 81507155, 7 | "author": { 8 | "login": "sewenthy", 9 | "id": 23148113, 10 | "node_id": "MDQ6VXNlcjIzMTQ4MTEz", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/23148113?v=4", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/sewenthy", 14 | "html_url": "https://github.com/sewenthy", 15 | "followers_url": "https://api.github.com/users/sewenthy/followers", 16 | "following_url": "https://api.github.com/users/sewenthy/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/sewenthy/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/sewenthy/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/sewenthy/subscriptions", 20 | "organizations_url": "https://api.github.com/users/sewenthy/orgs", 21 | "repos_url": "https://api.github.com/users/sewenthy/repos", 22 | "events_url": "https://api.github.com/users/sewenthy/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/sewenthy/received_events", 24 | "type": "User", 25 | "site_admin": false 26 | }, 27 | "node_id": "RE_kwDOIRbPmc4E27NT", 28 | "tag_name": "v1.0.0", 29 | "target_commitish": "119-unfurling-commit-range-links-b", 30 | "name": "NEW OUT (test compare)", 31 | "draft": false, 32 | "prerelease": false, 33 | "created_at": "2022-10-27T08:20:42Z", 34 | "published_at": "2022-10-31T02:18:14Z", 35 | "assets": [ 36 | 37 | ], 38 | "tarball_url": "https://api.github.com/repos/sewenthy/monorobot/tarball/v1.0.0", 39 | "zipball_url": "https://api.github.com/repos/sewenthy/monorobot/zipball/v1.0.0", 40 | "body": "" 41 | } 42 | -------------------------------------------------------------------------------- /mock_states/status.failed_no_build_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "buildkite/pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | }, 25 | "other-branch": { 26 | "181733": { 27 | "status": "pending", 28 | "build_number": "181733", 29 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733", 30 | "commit": { 31 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 32 | "author": "author@ahrefs.com", 33 | "commit_message": "c1 message" 34 | }, 35 | "is_finished": false, 36 | "failed_steps": [ 37 | { 38 | "name": "buildkite/pipeline2/failed-step", 39 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733#0192347d-e4ee-4072-9da4-f441eeb65ed4" 40 | } 41 | ], 42 | "created_at": "2024-06-02T04:57:47+00:00", 43 | "finished_at": null 44 | } 45 | } 46 | } 47 | }, 48 | "pipeline_commits": { 49 | "pipeline2": { 50 | "develop": { 51 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 52 | "s2": [] 53 | }, 54 | "other-branch": { 55 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 56 | "s2": [] 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mock_states/status.canceled_no_build_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "buildkite/pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | }, 25 | "other-branch": { 26 | "181733": { 27 | "status": "pending", 28 | "build_number": "181733", 29 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733", 30 | "commit": { 31 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 32 | "author": "author@ahrefs.com", 33 | "commit_message": "c1 message" 34 | }, 35 | "is_finished": false, 36 | "failed_steps": [ 37 | { 38 | "name": "buildkite/pipeline2/failed-step", 39 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733#0192347d-e4ee-4072-9da4-f441eeb65ed4" 40 | } 41 | ], 42 | "created_at": "2024-06-02T04:57:47+00:00", 43 | "finished_at": null 44 | } 45 | } 46 | } 47 | }, 48 | "pipeline_commits": { 49 | "pipeline2": { 50 | "develop": { 51 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 52 | "s2": [] 53 | }, 54 | "other-branch": { 55 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 56 | "s2": [] 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mock_states/status.failed_multiple_branches.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "buildkite/pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | } 24 | }, 25 | "other-branch": { 26 | "181733": { 27 | "status": "pending", 28 | "build_number": "181733", 29 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733", 30 | "commit": { 31 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 32 | "author": "author@ahrefs.com", 33 | "commit_message": "c1 message" 34 | }, 35 | "is_finished": false, 36 | "failed_steps": [ 37 | { 38 | "name": "buildkite/pipeline2/failed-step", 39 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733#0192347d-e4ee-4072-9da4-f441eeb65ed4" 40 | } 41 | ], 42 | "created_at": "2024-06-02T04:57:47+00:00", 43 | "finished_at": null 44 | } 45 | } 46 | } 47 | }, 48 | "pipeline_commits": { 49 | "pipeline2": { 50 | "develop": { 51 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 52 | "s2": [] 53 | }, 54 | "other-branch": { 55 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 56 | "s2": [] 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/api.ml: -------------------------------------------------------------------------------- 1 | open Common 2 | open Github_t 3 | open Slack_t 4 | 5 | module type Github = sig 6 | val get_config : ctx:Context.t -> repo:repository -> (Config_t.config, string) Result.t Lwt.t 7 | val get_api_commit : ctx:Context.t -> repo:repository -> sha:string -> (api_commit, string) Result.t Lwt.t 8 | val get_api_commit_webhook : 9 | ctx:Context.t -> commits_url:string -> repo_url:string -> sha:string -> (api_commit, string) Result.t Lwt.t 10 | val get_pull_request : ctx:Context.t -> repo:repository -> number:int -> (pull_request, string) Result.t Lwt.t 11 | val get_issue : ctx:Context.t -> repo:repository -> number:int -> (issue, string) Result.t Lwt.t 12 | val get_compare : ctx:Context.t -> repo:repository -> basehead:Github.basehead -> (compare, string) Result.t Lwt.t 13 | 14 | val request_reviewers : 15 | ctx:Context.t -> repo:repository -> number:int -> reviewers:request_reviewers_req -> (unit, string) Result.t Lwt.t 16 | end 17 | 18 | module type Slack = sig 19 | val lookup_user : 20 | ?cache:[ `Use | `Refresh ] -> 21 | ctx:Context.t -> 22 | cfg:Config_t.config -> 23 | email:string -> 24 | unit -> 25 | lookup_user_res slack_response Lwt.t 26 | 27 | val list_users : ?cursor:string -> ?limit:int -> ctx:Context.t -> unit -> list_users_res slack_response Lwt.t 28 | val send_notification : ctx:Context.t -> msg:post_message_req -> post_message_res option slack_response Lwt.t 29 | 30 | val send_chat_unfurl : 31 | ctx:Context.t -> 32 | channel:Slack_channel.Ident.t -> 33 | ts:Slack_timestamp.t -> 34 | unfurls:message_attachment StringMap.t -> 35 | unit -> 36 | unit slack_response Lwt.t 37 | 38 | val send_auth_test : ctx:Context.t -> unit -> auth_test_res slack_response Lwt.t 39 | 40 | val get_thread_permalink : ctx:Context.t -> State_t.slack_thread -> string option Lwt.t 41 | 42 | val send_file : ctx:Context.t -> file:Slack.file_req -> (unit, string) result Lwt.t 43 | end 44 | 45 | module type Buildkite = sig 46 | val get_job_log : 47 | ctx:Context.t -> Github_t.status_notification -> Buildkite_t.job -> (Buildkite_t.job_log, string) result Lwt.t 48 | val get_build_branch : ctx:Context.t -> Github_t.status_notification -> (Github_t.branch, string) Result.t Lwt.t 49 | 50 | val get_build : 51 | ?cache:[ `Use | `Refresh ] -> ctx:Context.t -> string -> (Buildkite_t.get_build_res, string) Result.t Lwt.t 52 | end 53 | -------------------------------------------------------------------------------- /test/monorobot.json: -------------------------------------------------------------------------------- 1 | { 2 | "main_branch_name": "develop", 3 | "status_rules": { 4 | "allowed_pipelines": [ 5 | { 6 | "name": "default", 7 | "failed_builds_channel": "failed-builds" 8 | }, 9 | { 10 | "name": "buildkite/pipeline2", 11 | "failed_builds_channel": "failed-builds" 12 | }, 13 | { 14 | "name": "buildkite/qa", 15 | "failed_builds_channel": "qa-failed-builds" 16 | }, 17 | { 18 | "name": "buildkite/bau" 19 | } 20 | ], 21 | "rules": [ 22 | { 23 | "on": ["failure"], 24 | "when": { 25 | "match": { 26 | "field": "description", 27 | "re": "^(Build #[0-9]+ canceled by .+|Failed \\(exit status 255\\))$" 28 | } 29 | }, 30 | "policy": "ignore" 31 | }, 32 | { 33 | "on": ["failure", "error"], 34 | "policy": "allow_once", 35 | "notify_dm": true 36 | } 37 | ] 38 | }, 39 | "ignored_users": ["ignored_user"], 40 | "prefix_rules": { 41 | "default_channel": "default", 42 | "filter_main_branch": true, 43 | "rules": [ 44 | { 45 | "match": ["backend/a1"], 46 | "channel": "a1" 47 | }, 48 | { 49 | "match": ["backend/a1/longest"], 50 | "channel": "longest-a1" 51 | }, 52 | { 53 | "match": ["backend/a5", "backend/a4"], 54 | "channel": "backend" 55 | }, 56 | { 57 | "match": ["backend/branch-filter1"], 58 | "channel": "backend1", 59 | "branch_filters": "any" 60 | }, 61 | { 62 | "match": ["backend/branch-filter2"], 63 | "channel": "backend2", 64 | "branch_filters": ["master"] 65 | }, 66 | { 67 | "channel": "all-push-events" 68 | } 69 | ] 70 | }, 71 | "label_rules": { 72 | "default_channel": "default", 73 | "rules": [ 74 | { 75 | "match": ["backend"], 76 | "channel": "backend" 77 | }, 78 | { 79 | "match": ["a1"], 80 | "channel": "a1-bot" 81 | }, 82 | { 83 | "match": ["a3"], 84 | "channel": "a3" 85 | }, 86 | { 87 | "ignore": ["backend", "a1", "a3"], 88 | "channel": "frontend-bot" 89 | } 90 | ] 91 | }, 92 | "user_mappings": { 93 | "mail@example.org": "slack_mail@example.com" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/github-api-cache/sewenthy_monorobot_branch_master: -------------------------------------------------------------------------------- 1 | { 2 | "name": "master", 3 | "commit": { 4 | "sha": "c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 5 | "node_id": "C_kwDOIRbPmdoAKGM3ZDUxZmI4Yzg1YTUwYWZjOTI2ZGMxMmI5NGVhZWRlYTk4YWIyMGU", 6 | "commit": { 7 | "author": { 8 | "name": "Sewen Thy", 9 | "email": "mail@example.org", 10 | "date": "2022-10-25T03:55:43Z" 11 | }, 12 | "committer": { 13 | "name": "Sewen Thy", 14 | "email": "mail@example.org", 15 | "date": "2022-10-25T03:55:43Z" 16 | }, 17 | "message": "notify me?", 18 | "tree": { 19 | "sha": "6a4b81f18e8fb5869782fb13c11792fffba3ba08", 20 | "url": "https://api.github.com/repos/sewenthy/monorobot/git/trees/6a4b81f18e8fb5869782fb13c11792fffba3ba08" 21 | }, 22 | "url": "https://api.github.com/repos/sewenthy/monorobot/git/commits/c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 23 | "comment_count": 0, 24 | "verification": { 25 | "verified": false, 26 | "reason": "unsigned", 27 | "signature": null, 28 | "payload": null 29 | } 30 | }, 31 | "url": "https://api.github.com/repos/sewenthy/monorobot/commits/c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 32 | "html_url": "https://github.com/sewenthy/monorobot/commit/c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 33 | "comments_url": "https://api.github.com/repos/sewenthy/monorobot/commits/c7d51fb8c85a50afc926dc12b94eaedea98ab20e/comments", 34 | "author": null, 35 | "committer": null, 36 | "parents": [ 37 | { 38 | "sha": "3612e3c0d3f9d000db907d2141f4a641800363b1", 39 | "url": "https://api.github.com/repos/sewenthy/monorobot/commits/3612e3c0d3f9d000db907d2141f4a641800363b1", 40 | "html_url": "https://github.com/sewenthy/monorobot/commit/3612e3c0d3f9d000db907d2141f4a641800363b1" 41 | } 42 | ] 43 | }, 44 | "_links": { 45 | "self": "https://api.github.com/repos/sewenthy/monorobot/branches/master", 46 | "html": "https://github.com/sewenthy/monorobot/tree/master" 47 | }, 48 | "protected": false, 49 | "protection": { 50 | "enabled": false, 51 | "required_status_checks": { 52 | "enforcement_level": "off", 53 | "contexts": [ 54 | 55 | ], 56 | "checks": [ 57 | 58 | ] 59 | } 60 | }, 61 | "protection_url": "https://api.github.com/repos/sewenthy/monorobot/branches/master/protection" 62 | } 63 | -------------------------------------------------------------------------------- /test/github-api-cache/sewenthy_monorobot_branch_119-unfurling-commit-range-links: -------------------------------------------------------------------------------- 1 | { 2 | "name": "119-unfurling-commit-range-links", 3 | "commit": { 4 | "sha": "c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 5 | "node_id": "C_kwDOIRbPmdoAKGM3ZDUxZmI4Yzg1YTUwYWZjOTI2ZGMxMmI5NGVhZWRlYTk4YWIyMGU", 6 | "commit": { 7 | "author": { 8 | "name": "Sewen Thy", 9 | "email": "mail@example.org", 10 | "date": "2022-10-25T03:55:43Z" 11 | }, 12 | "committer": { 13 | "name": "Sewen Thy", 14 | "email": "mail@example.org", 15 | "date": "2022-10-25T03:55:43Z" 16 | }, 17 | "message": "notify me?", 18 | "tree": { 19 | "sha": "6a4b81f18e8fb5869782fb13c11792fffba3ba08", 20 | "url": "https://api.github.com/repos/sewenthy/monorobot/git/trees/6a4b81f18e8fb5869782fb13c11792fffba3ba08" 21 | }, 22 | "url": "https://api.github.com/repos/sewenthy/monorobot/git/commits/c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 23 | "comment_count": 0, 24 | "verification": { 25 | "verified": false, 26 | "reason": "unsigned", 27 | "signature": null, 28 | "payload": null 29 | } 30 | }, 31 | "url": "https://api.github.com/repos/sewenthy/monorobot/commits/c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 32 | "html_url": "https://github.com/sewenthy/monorobot/commit/c7d51fb8c85a50afc926dc12b94eaedea98ab20e", 33 | "comments_url": "https://api.github.com/repos/sewenthy/monorobot/commits/c7d51fb8c85a50afc926dc12b94eaedea98ab20e/comments", 34 | "author": null, 35 | "committer": null, 36 | "parents": [ 37 | { 38 | "sha": "3612e3c0d3f9d000db907d2141f4a641800363b1", 39 | "url": "https://api.github.com/repos/sewenthy/monorobot/commits/3612e3c0d3f9d000db907d2141f4a641800363b1", 40 | "html_url": "https://github.com/sewenthy/monorobot/commit/3612e3c0d3f9d000db907d2141f4a641800363b1" 41 | } 42 | ] 43 | }, 44 | "_links": { 45 | "self": "https://api.github.com/repos/sewenthy/monorobot/branches/master", 46 | "html": "https://github.com/sewenthy/monorobot/tree/master" 47 | }, 48 | "protected": false, 49 | "protection": { 50 | "enabled": false, 51 | "required_status_checks": { 52 | "enforcement_level": "off", 53 | "contexts": [ 54 | 55 | ], 56 | "checks": [ 57 | 58 | ] 59 | } 60 | }, 61 | "protection_url": "https://api.github.com/repos/sewenthy/monorobot/branches/master/protection" 62 | } 63 | -------------------------------------------------------------------------------- /lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name monorobotlib) 3 | (libraries 4 | atdgen-runtime 5 | base64 6 | biniou 7 | curl 8 | curl.lwt 9 | devkit 10 | devkit.core 11 | digestif 12 | extlib 13 | lwt 14 | lwt.unix 15 | mirage-crypto-pk 16 | ocamldiff 17 | omd 18 | ptime 19 | ptime.clock 20 | re2 21 | sexplib0 22 | sqlgg.sqlite3 23 | sqlgg.traits 24 | sqlite3 25 | unix 26 | x509 27 | uri 28 | yojson 29 | text_cleanup) 30 | (preprocess 31 | (pps lwt_ppx))) 32 | 33 | (rule 34 | (targets common_t.ml common_t.mli common_j.ml common_j.mli) 35 | (deps common.atd) 36 | (action 37 | (progn 38 | (run %{bin:atdgen} -j -j-std %{deps}) 39 | (run %{bin:atdgen} -t %{deps})))) 40 | 41 | (rule 42 | (targets github_t.ml github_t.mli github_j.ml github_j.mli) 43 | (deps github.atd) 44 | (action 45 | (progn 46 | (run %{bin:atdgen} -j -j-std %{deps}) 47 | (run %{bin:atdgen} -t %{deps})))) 48 | 49 | (rule 50 | (targets buildkite_t.ml buildkite_t.mli buildkite_j.ml buildkite_j.mli) 51 | (deps buildkite.atd) 52 | (action 53 | (progn 54 | (run %{bin:atdgen} -j -j-std %{deps}) 55 | (run %{bin:atdgen} -t %{deps})))) 56 | 57 | (rule 58 | (targets slack_t.ml slack_t.mli slack_j.ml slack_j.mli) 59 | (deps slack.atd) 60 | (action 61 | (progn 62 | (run %{bin:atdgen} -j -j-std %{deps}) 63 | (run %{bin:atdgen} -t %{deps})))) 64 | 65 | (rule 66 | (targets rule_t.ml rule_t.mli rule_j.ml rule_j.mli) 67 | (deps rule.atd) 68 | (action 69 | (progn 70 | (run %{bin:atdgen} -j -j-std %{deps}) 71 | (run %{bin:atdgen} -t %{deps})))) 72 | 73 | (rule 74 | (targets config_t.ml config_t.mli config_j.ml config_j.mli) 75 | (deps config.atd) 76 | (action 77 | (progn 78 | (run %{bin:atdgen} -j -j-std %{deps}) 79 | (run %{bin:atdgen} -t %{deps})))) 80 | 81 | (rule 82 | (targets state_t.ml state_t.mli state_j.ml state_j.mli) 83 | (deps state.atd) 84 | (action 85 | (progn 86 | (run %{bin:atdgen} -j -j-std %{deps}) 87 | (run %{bin:atdgen} -t %{deps})))) 88 | 89 | (rule 90 | (targets debug_db_t.ml debug_db_t.mli debug_db_j.ml debug_db_j.mli) 91 | (deps debug_db.atd) 92 | (action 93 | (progn 94 | (run %{bin:atdgen} -j -j-std %{deps}) 95 | (run %{bin:atdgen} -t %{deps})))) 96 | 97 | (rule 98 | (target failed_builds_webhook_gen.ml) 99 | (deps ../db/sql/failed_builds_webhook.sql) 100 | (action 101 | (with-stdout-to 102 | %{target} 103 | (run %{bin:sqlgg} -gen caml_io -name make -params unnamed %{deps})))) 104 | -------------------------------------------------------------------------------- /mock_states/status.failed_empty_failed_jobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "buildkite/pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | }, 24 | "181734": { 25 | "status": "pending", 26 | "build_number": "181734", 27 | "build_url": "https://buildkite.com/org/pipeline2/builds/181734", 28 | "commit": { 29 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 30 | "author": "author@ahrefs.com", 31 | "commit_message": "c1 message" 32 | }, 33 | "is_finished": false, 34 | "failed_steps": [], 35 | "created_at": "2024-06-02T04:57:47+00:00", 36 | "finished_at": null 37 | } 38 | }, 39 | "other-branch": { 40 | "181733": { 41 | "status": "pending", 42 | "build_number": "181733", 43 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733", 44 | "commit": { 45 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 46 | "author": "author@ahrefs.com", 47 | "commit_message": "c1 message" 48 | }, 49 | "is_finished": false, 50 | "failed_steps": [ 51 | { 52 | "name": "buildkite/pipeline2/failed-step", 53 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733#0192347d-e4ee-4072-9da4-f441eeb65ed4" 54 | } 55 | ], 56 | "created_at": "2024-06-02T04:57:47+00:00", 57 | "finished_at": null 58 | } 59 | } 60 | } 61 | }, 62 | "pipeline_commits": { 63 | "pipeline2": { 64 | "develop": { 65 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 66 | "s2": [] 67 | }, 68 | "other-branch": { 69 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 70 | "s2": [] 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mock_states/status.canceled_empty_failed_jobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "buildkite/pipeline2/failed-step", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | } 20 | ], 21 | "created_at": "2024-06-02T04:57:47+00:00", 22 | "finished_at": "2024-06-02T04:58:47+00:00" 23 | }, 24 | "181734": { 25 | "status": "pending", 26 | "build_number": "181734", 27 | "build_url": "https://buildkite.com/org/pipeline2/builds/181734", 28 | "commit": { 29 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 30 | "author": "author@ahrefs.com", 31 | "commit_message": "c1 message" 32 | }, 33 | "is_finished": false, 34 | "failed_steps": [], 35 | "created_at": "2024-06-02T04:57:47+00:00", 36 | "finished_at": null 37 | } 38 | }, 39 | "other-branch": { 40 | "181733": { 41 | "status": "pending", 42 | "build_number": "181733", 43 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733", 44 | "commit": { 45 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 46 | "author": "author@ahrefs.com", 47 | "commit_message": "c1 message" 48 | }, 49 | "is_finished": false, 50 | "failed_steps": [ 51 | { 52 | "name": "buildkite/pipeline2/failed-step", 53 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733#0192347d-e4ee-4072-9da4-f441eeb65ed4" 54 | } 55 | ], 56 | "created_at": "2024-06-02T04:57:47+00:00", 57 | "finished_at": null 58 | } 59 | } 60 | } 61 | }, 62 | "pipeline_commits": { 63 | "pipeline2": { 64 | "develop": { 65 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 66 | "s2": [] 67 | }, 68 | "other-branch": { 69 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 70 | "s2": [] 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /db/sql/failed_builds_webhook.sql: -------------------------------------------------------------------------------- 1 | -- @ensure_failed_builds_webhook_table 2 | CREATE TABLE IF NOT EXISTS failed_builds_webhook ( 3 | id TEXT PRIMARY KEY, 4 | sha VARCHAR(40) NOT NULL, 5 | build_payload TEXT NOT NULL, 6 | pipeline_payload TEXT NOT NULL, 7 | jobs TEXT NOT NULL, 8 | commit_author VARCHAR(255) NOT NULL, 9 | commit_url VARCHAR(255) NOT NULL, 10 | build_state VARCHAR(10) NOT NULL, 11 | build_url VARCHAR(255) NOT NULL, 12 | build_number INTEGER NOT NULL, 13 | is_canceled BOOLEAN NOT NULL, 14 | pipeline VARCHAR(255) NOT NULL, 15 | repository VARCHAR(255) NOT NULL, 16 | branch VARCHAR(255) NOT NULL, 17 | state_before_notification TEXT NOT NULL, 18 | state_after_notification TEXT NOT NULL, 19 | last_handled_in TEXT NOT NULL, 20 | has_state_update BOOLEAN NOT NULL, 21 | notification_created_at TIMESTAMP NOT NULL, 22 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL 23 | ); 24 | 25 | -- @create 26 | INSERT INTO failed_builds_webhook VALUES; 27 | 28 | -- @update_state_after_notification 29 | UPDATE failed_builds_webhook 30 | SET has_state_update = @has_state_update, state_after_notification = @state_after_notification, last_handled_in = @last_handled_in 31 | WHERE id = @id; 32 | 33 | -- @get_by_sha 34 | SELECT id, sha, build_state, build_number, is_canceled, has_state_update, last_handled_in, state_before_notification, state_after_notification 35 | FROM failed_builds_webhook WHERE sha = ? and pipeline = @pipeline and branch = @branch ORDER BY id DESC; 36 | 37 | -- @get_by_build_number 38 | SELECT id, sha, build_state, build_number, is_canceled, has_state_update, last_handled_in, state_before_notification, state_after_notification 39 | FROM failed_builds_webhook WHERE build_number = ? and pipeline = @pipeline and branch = @branch ORDER BY id DESC; 40 | 41 | -- @get_after 42 | SELECT id, sha, build_state, build_number, is_canceled, has_state_update, last_handled_in, state_before_notification, state_after_notification 43 | FROM failed_builds_webhook WHERE build_number >= ? and pipeline = @pipeline and branch = @branch ORDER BY build_number desc, id desc; 44 | 45 | -- @get_from_to 46 | SELECT id, sha, build_state, build_number, is_canceled, has_state_update, last_handled_in, state_before_notification, state_after_notification 47 | FROM failed_builds_webhook WHERE build_number >= ? and build_number <= @to_ and pipeline = @pipeline and branch = @branch ORDER BY build_number desc, id desc; 48 | -------------------------------------------------------------------------------- /lib/state.atd: -------------------------------------------------------------------------------- 1 | type status_state = abstract 2 | type 'v map_as_object = abstract 3 | type 'v int_map_as_object = abstract 4 | type 'v table_as_object = abstract 5 | type string_set = abstract 6 | type failed_step_set = abstract 7 | type slack_timestamp = string wrap 8 | type timestamp = string wrap 9 | type user_id = string wrap 10 | type channel_id = string wrap 11 | type any_channel = string wrap 12 | 13 | type build_status = { 14 | status: status_state; 15 | created_at: timestamp; 16 | } 17 | 18 | (* A map from builds numbers to build statuses *) 19 | type build_statuses = build_status int_map_as_object 20 | 21 | (* A map from branch names to [build_statuses] maps *) 22 | type branch_statuses = build_statuses map_as_object 23 | 24 | (* A map from pipeline names to [branch_statuses] maps. 25 | This tracks the last build state matched by the status_rules for each pipeline and branch *) 26 | type pipeline_statuses = branch_statuses map_as_object 27 | 28 | type commit_sets = { 29 | s1: string_set; 30 | s2: string_set; 31 | } 32 | 33 | (* A map from pipeline names to a set of commits. This tracks the commits 34 | that have triggered a direct message notification. *) 35 | type branch_commits = commit_sets map_as_object 36 | 37 | (* A map from pipeline names to [branch_commits] maps *) 38 | type pipeline_commits = branch_commits map_as_object 39 | 40 | type slack_thread = { 41 | ts: slack_timestamp; 42 | channel: any_channel; 43 | cid: channel_id; 44 | } 45 | 46 | type slack_threads = slack_thread list map_as_object 47 | 48 | type failed_steps = { 49 | steps: failed_step_set; 50 | last_build: int; 51 | } 52 | 53 | (* The runtime state of a given GitHub repository *) 54 | type repo_state = { 55 | ~pipeline_statuses : pipeline_statuses; 56 | ~pipeline_commits : pipeline_commits; 57 | ~slack_threads : slack_threads; 58 | ~failed_steps : failed_steps table_as_object; 59 | } 60 | 61 | (* The serializable runtime state of the bot *) 62 | type state = { 63 | repos : repo_state table_as_object; 64 | ?bot_user_id : user_id nullable; 65 | } 66 | -------------------------------------------------------------------------------- /mock_states/status.canceled_diff_failed_jobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "buildkite/pipeline2/fail", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | }, 20 | { 21 | "name": "buildkite/pipeline2/trigger-infra", 22 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 23 | } 24 | ], 25 | "created_at": "2024-06-02T04:57:47+00:00", 26 | "finished_at": "2024-06-02T04:58:47+00:00" 27 | }, 28 | "181734": { 29 | "status": "pending", 30 | "build_number": "181734", 31 | "build_url": "https://buildkite.com/org/pipeline2/builds/181734", 32 | "commit": { 33 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 34 | "author": "author@ahrefs.com", 35 | "commit_message": "c1 message" 36 | }, 37 | "is_finished": false, 38 | "failed_steps": [], 39 | "created_at": "2024-06-02T04:57:47+00:00", 40 | "finished_at": null 41 | } 42 | }, 43 | "other-branch": { 44 | "181733": { 45 | "status": "pending", 46 | "build_number": "181733", 47 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733", 48 | "commit": { 49 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 50 | "author": "author@ahrefs.com", 51 | "commit_message": "c1 message" 52 | }, 53 | "is_finished": false, 54 | "failed_steps": [ 55 | { 56 | "name": "buildkite/pipeline2/failed-step", 57 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733#0192347d-e4ee-4072-9da4-f441eeb65ed4" 58 | } 59 | ], 60 | "created_at": "2024-06-02T04:57:47+00:00", 61 | "finished_at": null 62 | } 63 | } 64 | } 65 | }, 66 | "pipeline_commits": { 67 | "pipeline2": { 68 | "develop": { 69 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 70 | "s2": [] 71 | }, 72 | "other-branch": { 73 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 74 | "s2": [] 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mock_states/status.failed_diff_failed_jobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline_statuses": { 3 | "pipeline2": { 4 | "develop": { 5 | "181732": { 6 | "status": "failure", 7 | "build_number": "181732", 8 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732", 9 | "commit": { 10 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 11 | "author": "author@ahrefs.com", 12 | "commit_message": "c1 message" 13 | }, 14 | "is_finished": true, 15 | "failed_steps": [ 16 | { 17 | "name": "buildkite/pipeline2/fail", 18 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 19 | }, 20 | { 21 | "name": "buildkite/pipeline2/trigger-infra", 22 | "build_url": "https://buildkite.com/org/pipeline2/builds/181732#0192347d-e4ee-4072-9da4-f441eeb65ed4" 23 | } 24 | ], 25 | "created_at": "2024-06-02T04:57:47+00:00", 26 | "finished_at": "2024-06-02T04:58:47+00:00" 27 | }, 28 | "181734": { 29 | "status": "pending", 30 | "build_number": "181734", 31 | "build_url": "https://buildkite.com/org/pipeline2/builds/181734", 32 | "commit": { 33 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 34 | "author": "author@ahrefs.com", 35 | "commit_message": "c1 message" 36 | }, 37 | "is_finished": false, 38 | "failed_steps": [], 39 | "created_at": "2024-06-02T04:57:47+00:00", 40 | "finished_at": null 41 | } 42 | }, 43 | "other-branch": { 44 | "181733": { 45 | "status": "pending", 46 | "build_number": "181733", 47 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733", 48 | "commit": { 49 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 50 | "author": "author@ahrefs.com", 51 | "commit_message": "c1 message" 52 | }, 53 | "is_finished": false, 54 | "failed_steps": [ 55 | { 56 | "name": "buildkite/pipeline2/failed-step", 57 | "build_url": "https://buildkite.com/org/pipeline2/builds/181733#0192347d-e4ee-4072-9da4-f441eeb65ed4" 58 | } 59 | ], 60 | "created_at": "2024-06-02T04:57:47+00:00", 61 | "finished_at": null 62 | } 63 | } 64 | } 65 | }, 66 | "pipeline_commits": { 67 | "pipeline2": { 68 | "develop": { 69 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 70 | "s2": [] 71 | }, 72 | "other-branch": { 73 | "s1": ["7e0a933e9c71b4ca107680ca958ca1888d5e479b"], 74 | "s2": [] 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/text_cleanup/test/job_log.t/log: -------------------------------------------------------------------------------- 1 | _bk;t=1737562943517~~~ Preparing working directory 2 | _bk;t=1737562943517$ cd /home/user/mydir 3 | _bk;t=1737562943532Pseudo-terminal will not be allocated because stdin is not a terminal. 4 | 5 | _bk;t=1737562943532# Host "github.com" already in list of known hosts at "/home/user/.ssh/known_hosts" 6 | _bk;t=1737562943533$ git clean -ffxdq 7 | _bk;t=1737562943535$ git fetch -v --prune -- origin 404671053ebacf0245dc5b60566eb85e19465860 8 | _bk;t=1737562944988From github.com:Account/repo 9 | 10 | _bk;t=1737562944988 * branch 404671053ebacf0245dc5b60566eb85e19465860 -> FETCH_HEAD 11 | 12 | _bk;t=1737562945101$ git checkout -f 404671053ebacf0245dc5b60566eb85e19465860 13 | _bk;t=1737562945103HEAD is now at 4046710 test change 14 | 15 | _bk;t=1737562945103# Cleaning again to catch any post-checkout changes 16 | _bk;t=1737562945103$ git clean -ffxdq 17 | _bk;t=1737562945104# Checking to see if git commit information needs to be sent to Buildkite... 18 | _bk;t=1737562945104$ buildkite-agent meta-data exists buildkite:git:commit 19 | _bk;t=1737562945264# Git commit information has already been sent to Buildkite 20 | _bk;t=1737562945264~~~ Running commands 21 | _bk;t=1737562945264$ echo "Test the rocket" 22 | _bk;t=1737562945264dune test 23 | _bk;t=1737562945264exit 0 24 | _bk;t=1737562945264 25 | _bk;t=1737562945268Test the rocket 26 | 27 | _bk;t=1737562945297Done: 60% (3/5, 2 left) (jobs: 0) 28 | 29 | Done: 55% (5/9, 4 left) (jobs: 0) 30 | 31 | Done: 66% (6/9, 3 left) (jobs: 1) 32 | 33 | Done: 77% (7/9, 2 left) (jobs: 1) 34 | 35 | Done: 90% (9/10, 1 left) (jobs: 1) 36 | 37 | File "tests/test.t", line 1, characters 0-0: 38 | 39 | _bk;t=1737562945430/usr/bin/git --no-pager diff --no-index --color=always -u _build/.sandbox/b2994389d75f031a56c7ed865141cfe7/default/tests/test.t _build/.sandbox/b2994389d75f031a56c7ed865141cfe7/default/tests/test.t.corrected 40 | 41 | _bk;t=1737562945430diff --git a/_build/.sandbox/b2994389d75f031a56c7ed865141cfe7/default/tests/test.t b/_build/.sandbox/b2994389d75f031a56c7ed865141cfe7/default/tests/test.t.corrected 42 | 43 | _bk;t=1737562945430index 88404ca..a9e2880 100644 44 | 45 | _bk;t=1737562945430--- a/_build/.sandbox/b2994389d75f031a56c7ed865141cfe7/default/tests/test.t 46 | 47 | _bk;t=1737562945430+++ b/_build/.sandbox/b2994389d75f031a56c7ed865141cfe7/default/tests/test.t.corrected 48 | 49 | _bk;t=1737562945430@@ -1,2 +1,2 @@ 50 | 51 | _bk;t=1737562945430 $ dune exec backend 52 | 53 | _bk;t=1737562945430- aaabvaaa 54 | 55 | _bk;t=1737562945430+ aaaa 56 | 57 | _bk;t=1737562945430Done: 90% (9/10, 1 left) (jobs: 1) 58 | 59 | ^^^ +++ 60 | _bk;t=1737562945433🚨 Error: The command exited with status 1 61 | _bk;t=1737562945433^^^ +++ 62 | _bk;t=1737562945433user command error: exit status 1 63 | -------------------------------------------------------------------------------- /src/request_handler.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | open Devkit 3 | open Monorobotlib 4 | 5 | let log = Log.from "request_handler" 6 | 7 | module Action = Action.Action (Api_remote.Github) (Api_remote.Slack) (Api_remote.Buildkite) 8 | 9 | let run ~ctx ~addr ~port = 10 | let open Httpev in 11 | let ip = Unix.inet_addr_of_string addr in 12 | let signature = sprintf "listen %s:%d" (Unix.string_of_inet_addr ip) port in 13 | let connection = Unix.ADDR_INET (ip, port) in 14 | let request_handler_lwt = 15 | Httpev.setup_lwt { default with name = "monorobot"; connection; access_log_enabled = false } (fun _http request -> 16 | let module Arg = Args (struct 17 | let req = request 18 | end) in 19 | let body r = Lwt.return (`Body r) in 20 | let ret ?(status = `Ok) ?(typ = "text/plain") ?extra r = body @@ serve ~status ?extra request typ r in 21 | let ret_err status s = body @@ serve_text ~status request s in 22 | try%lwt 23 | let path = 24 | match String.split_on_char '/' request.path with 25 | | "" :: p -> p 26 | | _ -> Exn.fail "you are on a wrong path" 27 | in 28 | match request.meth, List.map Web.urldecode path with 29 | | _, [ "stats" ] -> ret (sprintf "%s %s uptime\n" signature Devkit.Action.uptime#get_str) 30 | | _, [ "ping" ] -> ret "" 31 | | `GET, [ "config" ] -> 32 | let repo_url = Arg.str "repo" |> Web.urldecode in 33 | (match%lwt Action.print_config ctx repo_url with 34 | | Error (code, msg) -> ret_err code msg 35 | | Ok res -> ret ~typ:"application/json" res) 36 | | _, [ "github" ] | _, [ "external"; "github" ] -> 37 | log#debug "github event: %s" request.body; 38 | let%lwt () = Action.process_github_notification ctx request.headers request.body in 39 | ret "ok" 40 | | _, [ "slack"; "events" ] | _, [ "external"; "slack"; "events" ] -> 41 | log#debug "slack event: %s" request.body; 42 | let%lwt res = Action.process_slack_event ctx request.headers request.body in 43 | ret res 44 | | _, [ "bk_webhook" ] | _, [ "external"; "bk_webhook" ] -> 45 | log#debug "buildkite webhook: %s" request.body; 46 | let%lwt () = Action.process_buildkite_webhook ctx request.headers request.body in 47 | ret "ok" 48 | | _, _ -> 49 | log#error "unknown path : %s" (Httpev.show_request request); 50 | ret_err `Not_found "not found" 51 | with 52 | | Arg.Bad s -> 53 | log#error "bad parameter %S : %s" s (Httpev.show_request request); 54 | ret_err `Not_found (sprintf "bad parameter %s" s) 55 | | exn -> 56 | log#error ~exn "internal error : %s" (Httpev.show_request request); 57 | ret_err `Internal_server_error 58 | (match exn with 59 | | Failure s -> s 60 | | Invalid_argument s -> s 61 | | exn -> Exn.str exn)) 62 | in 63 | let refresh_username_to_slack_id_tbl_background_lwt = 64 | try%lwt Daemon.unless_exit @@ Action.refresh_username_to_slack_id_tbl_background_lwt ~ctx 65 | with Daemon.ShouldExit -> Lwt.return_unit 66 | in 67 | let%lwt () = Lwt.join [ refresh_username_to_slack_id_tbl_background_lwt; request_handler_lwt ] in 68 | Lwt.return_unit 69 | -------------------------------------------------------------------------------- /test/github-api-cache/d95302addd66c1816bce1b1d495ed1c93ccd478: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "Louis", 7 | "email": "mail@example.org", 8 | "date": "2020-06-02T03:14:51Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub Enterprise", 12 | "email": "mail@example.org", 13 | "date": "2020-06-02T03:14:51Z" 14 | }, 15 | "message": "Update README.md", 16 | "tree": { 17 | "sha": "ee5c539cad37c77348ce7a55756acc542b41cfc7", 18 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/ee5c539cad37c77348ce7a55756acc542b41cfc7" 19 | }, 20 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478", 21 | "comment_count": 0, 22 | "verification": { 23 | "verified": false, 24 | "reason": "unsigned", 25 | "signature": null, 26 | "payload": null 27 | } 28 | }, 29 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478", 30 | "html_url": "https://github.com/ahrefs/monorepo/commit/0d95302addd66c1816bce1b1d495ed1c93ccd478", 31 | "comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478/comments", 32 | "author": { 33 | "login": "Khady", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://github.com/avatars/u/0", 37 | "gravatar_id": "", 38 | "url": "https://github.com/api/v3/users/Khady", 39 | "html_url": "https://github.com/Khady", 40 | "followers_url": "https://github.com/api/v3/users/Khady/followers", 41 | "following_url": "https://github.com/api/v3/users/Khady/following{/other_user}", 42 | "gists_url": "https://github.com/api/v3/users/Khady/gists{/gist_id}", 43 | "starred_url": "https://github.com/api/v3/users/Khady/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://github.com/api/v3/users/Khady/subscriptions", 45 | "organizations_url": "https://github.com/api/v3/users/Khady/orgs", 46 | "repos_url": "https://github.com/api/v3/users/Khady/repos", 47 | "events_url": "https://github.com/api/v3/users/Khady/events{/privacy}", 48 | "received_events_url": "https://github.com/api/v3/users/Khady/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": null, 53 | "parents": [ 54 | { 55 | "sha": "04cb72d6dc8d92131282a7eff57f6caf632f0a39", 56 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/04cb72d6dc8d92131282a7eff57f6caf632f0a39", 57 | "html_url": "https://github.com/ahrefs/monorepo/commit/04cb72d6dc8d92131282a7eff57f6caf632f0a39" 58 | } 59 | ], 60 | "stats": { 61 | "total": 2, 62 | "additions": 1, 63 | "deletions": 1 64 | }, 65 | "files": [ 66 | { 67 | "sha": "e7d353f786a2f29d0cf7ed6fdad08ec446c1b9b1", 68 | "filename": "README.md", 69 | "status": "modified", 70 | "additions": 1, 71 | "deletions": 1, 72 | "changes": 2, 73 | "blob_url": "https://github.com/ahrefs/monorepo/blob/0d95302addd66c1816bce1b1d495ed1c93ccd478/README.md", 74 | "raw_url": "https://github.com/ahrefs/monorepo/raw/0d95302addd66c1816bce1b1d495ed1c93ccd478/README.md", 75 | "contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/README.md?ref=0d95302addd66c1816bce1b1d495ed1c93ccd478", 76 | "patch": "" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /test/github-api-cache/78492c2467876259d787538d600cfa0b18a2b814: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "78492c2467876259d787538d600cfa0b18a2b814", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "Louis", 7 | "email": "mail@example.org", 8 | "date": "2020-06-23T08:13:21Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub Enterprise", 12 | "email": "mail@example.org", 13 | "date": "2020-06-23T08:13:21Z" 14 | }, 15 | "message": "long ci step", 16 | "tree": { 17 | "sha": "7dafff78ed5c2b44e443d9c9165f55f41bbc0eeb", 18 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/7dafff78ed5c2b44e443d9c9165f55f41bbc0eeb" 19 | }, 20 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/78492c2467876259d787538d600cfa0b18a2b814", 21 | "comment_count": 0, 22 | "verification": { 23 | "verified": false, 24 | "reason": "unsigned", 25 | "signature": null, 26 | "payload": null 27 | } 28 | }, 29 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/78492c2467876259d787538d600cfa0b18a2b814", 30 | "html_url": "https://github.com/ahrefs/monorepo/commit/78492c2467876259d787538d600cfa0b18a2b814", 31 | "comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/78492c2467876259d787538d600cfa0b18a2b814/comments", 32 | "author": { 33 | "login": "Khady", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://github.com/avatars/u/0", 37 | "gravatar_id": "", 38 | "url": "https://github.com/api/v3/users/Khady", 39 | "html_url": "https://github.com/Khady", 40 | "followers_url": "https://github.com/api/v3/users/Khady/followers", 41 | "following_url": "https://github.com/api/v3/users/Khady/following{/other_user}", 42 | "gists_url": "https://github.com/api/v3/users/Khady/gists{/gist_id}", 43 | "starred_url": "https://github.com/api/v3/users/Khady/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://github.com/api/v3/users/Khady/subscriptions", 45 | "organizations_url": "https://github.com/api/v3/users/Khady/orgs", 46 | "repos_url": "https://github.com/api/v3/users/Khady/repos", 47 | "events_url": "https://github.com/api/v3/users/Khady/events{/privacy}", 48 | "received_events_url": "https://github.com/api/v3/users/Khady/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": null, 53 | "parents": [ 54 | { 55 | "sha": "c7071df521211921fb2478ce877d61d936d5d9f0", 56 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/c7071df521211921fb2478ce877d61d936d5d9f0", 57 | "html_url": "https://github.com/ahrefs/monorepo/commit/c7071df521211921fb2478ce877d61d936d5d9f0" 58 | } 59 | ], 60 | "stats": { 61 | "total": 1, 62 | "additions": 1, 63 | "deletions": 0 64 | }, 65 | "files": [ 66 | { 67 | "sha": "c61033bda19d2d2ce2b883ad144bc8c20620c15b", 68 | "filename": ".script.sh", 69 | "status": "modified", 70 | "additions": 1, 71 | "deletions": 0, 72 | "changes": 1, 73 | "blob_url": "https://github.com/ahrefs/monorepo/blob/78492c2467876259d787538d600cfa0b18a2b814/.script.sh", 74 | "raw_url": "https://github.com/ahrefs/monorepo/raw/78492c2467876259d787538d600cfa0b18a2b814/.script.sh", 75 | "contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/.script.sh?ref=78492c2467876259d787538d600cfa0b18a2b814", 76 | "patch": "" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorepo_commit_0d95302addd66c1816bce1b1d495ed1c93ccd478: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "Louis", 7 | "email": "mail@example.org", 8 | "date": "2020-06-02T03:14:51Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub Enterprise", 12 | "email": "mail@example.org", 13 | "date": "2020-06-02T03:14:51Z" 14 | }, 15 | "message": "Update README.md", 16 | "tree": { 17 | "sha": "ee5c539cad37c77348ce7a55756acc542b41cfc7", 18 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/ee5c539cad37c77348ce7a55756acc542b41cfc7" 19 | }, 20 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478", 21 | "comment_count": 0, 22 | "verification": { 23 | "verified": false, 24 | "reason": "unsigned", 25 | "signature": null, 26 | "payload": null 27 | } 28 | }, 29 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478", 30 | "html_url": "https://github.com/ahrefs/monorepo/commit/0d95302addd66c1816bce1b1d495ed1c93ccd478", 31 | "comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478/comments", 32 | "author": { 33 | "login": "Khady", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://github.com/avatars/u/0", 37 | "gravatar_id": "", 38 | "url": "https://github.com/api/v3/users/Khady", 39 | "html_url": "https://github.com/Khady", 40 | "followers_url": "https://github.com/api/v3/users/Khady/followers", 41 | "following_url": "https://github.com/api/v3/users/Khady/following{/other_user}", 42 | "gists_url": "https://github.com/api/v3/users/Khady/gists{/gist_id}", 43 | "starred_url": "https://github.com/api/v3/users/Khady/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://github.com/api/v3/users/Khady/subscriptions", 45 | "organizations_url": "https://github.com/api/v3/users/Khady/orgs", 46 | "repos_url": "https://github.com/api/v3/users/Khady/repos", 47 | "events_url": "https://github.com/api/v3/users/Khady/events{/privacy}", 48 | "received_events_url": "https://github.com/api/v3/users/Khady/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": null, 53 | "parents": [ 54 | { 55 | "sha": "04cb72d6dc8d92131282a7eff57f6caf632f0a39", 56 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/04cb72d6dc8d92131282a7eff57f6caf632f0a39", 57 | "html_url": "https://github.com/ahrefs/monorepo/commit/04cb72d6dc8d92131282a7eff57f6caf632f0a39" 58 | } 59 | ], 60 | "stats": { 61 | "total": 2, 62 | "additions": 1, 63 | "deletions": 1 64 | }, 65 | "files": [ 66 | { 67 | "sha": "e7d353f786a2f29d0cf7ed6fdad08ec446c1b9b1", 68 | "filename": "README.md", 69 | "status": "modified", 70 | "additions": 1, 71 | "deletions": 1, 72 | "changes": 2, 73 | "blob_url": "https://github.com/ahrefs/monorepo/blob/0d95302addd66c1816bce1b1d495ed1c93ccd478/README.md", 74 | "raw_url": "https://github.com/ahrefs/monorepo/raw/0d95302addd66c1816bce1b1d495ed1c93ccd478/README.md", 75 | "contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/README.md?ref=0d95302addd66c1816bce1b1d495ed1c93ccd478", 76 | "patch": "" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorepo_commit_7e0a933e9c71b4ca107680ca958ca1888d5e479b: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "7e0a933e9c71b4ca107680ca958ca1888d5e479b", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "Louis", 7 | "email": "author@ahrefs.com", 8 | "date": "2020-06-23T08:13:21Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub Enterprise", 12 | "email": "author@ahrefs.com", 13 | "date": "2020-06-23T08:13:21Z" 14 | }, 15 | "message": "c1 message", 16 | "tree": { 17 | "sha": "7dafff78ed5c2b44e443d9c9165f55f41bbc0eeb", 18 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/7dafff78ed5c2b44e443d9c9165f55f41bbc0eeb" 19 | }, 20 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/7e0a933e9c71b4ca107680ca958ca1888d5e479b", 21 | "comment_count": 0, 22 | "verification": { 23 | "verified": false, 24 | "reason": "unsigned", 25 | "signature": null, 26 | "payload": null 27 | } 28 | }, 29 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/7e0a933e9c71b4ca107680ca958ca1888d5e479b", 30 | "html_url": "https://github.com/ahrefs/monorepo/commit/7e0a933e9c71b4ca107680ca958ca1888d5e479b", 31 | "comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/7e0a933e9c71b4ca107680ca958ca1888d5e479b/comments", 32 | "author": { 33 | "login": "Khady", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://github.com/avatars/u/0", 37 | "gravatar_id": "", 38 | "url": "https://github.com/api/v3/users/Khady", 39 | "html_url": "https://github.com/Khady", 40 | "followers_url": "https://github.com/api/v3/users/Khady/followers", 41 | "following_url": "https://github.com/api/v3/users/Khady/following{/other_user}", 42 | "gists_url": "https://github.com/api/v3/users/Khady/gists{/gist_id}", 43 | "starred_url": "https://github.com/api/v3/users/Khady/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://github.com/api/v3/users/Khady/subscriptions", 45 | "organizations_url": "https://github.com/api/v3/users/Khady/orgs", 46 | "repos_url": "https://github.com/api/v3/users/Khady/repos", 47 | "events_url": "https://github.com/api/v3/users/Khady/events{/privacy}", 48 | "received_events_url": "https://github.com/api/v3/users/Khady/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": null, 53 | "parents": [ 54 | { 55 | "sha": "c7071df521211921fb2478ce877d61d936d5d9f0", 56 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/c7071df521211921fb2478ce877d61d936d5d9f0", 57 | "html_url": "https://github.com/ahrefs/monorepo/commit/c7071df521211921fb2478ce877d61d936d5d9f0" 58 | } 59 | ], 60 | "stats": { 61 | "total": 1, 62 | "additions": 1, 63 | "deletions": 0 64 | }, 65 | "files": [ 66 | { 67 | "sha": "c61033bda19d2d2ce2b883ad144bc8c20620c15b", 68 | "filename": ".script.sh", 69 | "status": "modified", 70 | "additions": 1, 71 | "deletions": 0, 72 | "changes": 1, 73 | "blob_url": "https://github.com/ahrefs/monorepo/blob/7e0a933e9c71b4ca107680ca958ca1888d5e479b/.script.sh", 74 | "raw_url": "https://github.com/ahrefs/monorepo/raw/7e0a933e9c71b4ca107680ca958ca1888d5e479b/.script.sh", 75 | "contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/.script.sh?ref=7e0a933e9c71b4ca107680ca958ca1888d5e479b", 76 | "patch": "" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorobot_commit_0d95302addd66c1816bce1b1d495ed1c93ccd478: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "0d95302addd66c1816bce1b1d495ed1c93ccd478", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "Louis", 7 | "email": "mail@example.org", 8 | "date": "2020-06-02T03:14:51Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub Enterprise", 12 | "email": "mail@example.org", 13 | "date": "2020-06-02T03:14:51Z" 14 | }, 15 | "message": "Update README.md", 16 | "tree": { 17 | "sha": "ee5c539cad37c77348ce7a55756acc542b41cfc7", 18 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/ee5c539cad37c77348ce7a55756acc542b41cfc7" 19 | }, 20 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478", 21 | "comment_count": 0, 22 | "verification": { 23 | "verified": false, 24 | "reason": "unsigned", 25 | "signature": null, 26 | "payload": null 27 | } 28 | }, 29 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478", 30 | "html_url": "https://github.com/ahrefs/monorepo/commit/0d95302addd66c1816bce1b1d495ed1c93ccd478", 31 | "comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/0d95302addd66c1816bce1b1d495ed1c93ccd478/comments", 32 | "author": { 33 | "login": "Khady", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://github.com/avatars/u/0", 37 | "gravatar_id": "", 38 | "url": "https://github.com/api/v3/users/Khady", 39 | "html_url": "https://github.com/Khady", 40 | "followers_url": "https://github.com/api/v3/users/Khady/followers", 41 | "following_url": "https://github.com/api/v3/users/Khady/following{/other_user}", 42 | "gists_url": "https://github.com/api/v3/users/Khady/gists{/gist_id}", 43 | "starred_url": "https://github.com/api/v3/users/Khady/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://github.com/api/v3/users/Khady/subscriptions", 45 | "organizations_url": "https://github.com/api/v3/users/Khady/orgs", 46 | "repos_url": "https://github.com/api/v3/users/Khady/repos", 47 | "events_url": "https://github.com/api/v3/users/Khady/events{/privacy}", 48 | "received_events_url": "https://github.com/api/v3/users/Khady/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": null, 53 | "parents": [ 54 | { 55 | "sha": "04cb72d6dc8d92131282a7eff57f6caf632f0a39", 56 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/04cb72d6dc8d92131282a7eff57f6caf632f0a39", 57 | "html_url": "https://github.com/ahrefs/monorepo/commit/04cb72d6dc8d92131282a7eff57f6caf632f0a39" 58 | } 59 | ], 60 | "stats": { 61 | "total": 2, 62 | "additions": 1, 63 | "deletions": 1 64 | }, 65 | "files": [ 66 | { 67 | "sha": "e7d353f786a2f29d0cf7ed6fdad08ec446c1b9b1", 68 | "filename": "README.md", 69 | "status": "modified", 70 | "additions": 1, 71 | "deletions": 1, 72 | "changes": 2, 73 | "blob_url": "https://github.com/ahrefs/monorepo/blob/0d95302addd66c1816bce1b1d495ed1c93ccd478/README.md", 74 | "raw_url": "https://github.com/ahrefs/monorepo/raw/0d95302addd66c1816bce1b1d495ed1c93ccd478/README.md", 75 | "contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/README.md?ref=0d95302addd66c1816bce1b1d495ed1c93ccd478", 76 | "patch": "" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /test/github-api-cache/184a35dd234e846cb5fe0723063f8bba03262e43: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "184a35dd234e846cb5fe0723063f8bba03262e43", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "xinyuluo", 7 | "email": "mail@example.org", 8 | "date": "2020-06-04T06:04:32Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub Enterprise", 12 | "email": "mail@example.org", 13 | "date": "2020-06-04T06:04:32Z" 14 | }, 15 | "message": "add a new dir\n\nlabeled with integer 3", 16 | "tree": { 17 | "sha": "ca23f208ac146b44837c93b3f96f52b2790c311d", 18 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/ca23f208ac146b44837c93b3f96f52b2790c311d" 19 | }, 20 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/184a35dd234e846cb5fe0723063f8bba03262e43", 21 | "comment_count": 0, 22 | "verification": { 23 | "verified": false, 24 | "reason": "unsigned", 25 | "signature": null, 26 | "payload": null 27 | } 28 | }, 29 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/184a35dd234e846cb5fe0723063f8bba03262e43", 30 | "html_url": "https://github.com/ahrefs/monorepo/commit/184a35dd234e846cb5fe0723063f8bba03262e43", 31 | "comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/184a35dd234e846cb5fe0723063f8bba03262e43/comments", 32 | "author": { 33 | "login": "xinyuluo", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://github.com/avatars/u/0", 37 | "gravatar_id": "", 38 | "url": "https://github.com/api/v3/users/xinyuluo", 39 | "html_url": "https://github.com/xinyuluo", 40 | "followers_url": "https://github.com/api/v3/users/xinyuluo/followers", 41 | "following_url": "https://github.com/api/v3/users/xinyuluo/following{/other_user}", 42 | "gists_url": "https://github.com/api/v3/users/xinyuluo/gists{/gist_id}", 43 | "starred_url": "https://github.com/api/v3/users/xinyuluo/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://github.com/api/v3/users/xinyuluo/subscriptions", 45 | "organizations_url": "https://github.com/api/v3/users/xinyuluo/orgs", 46 | "repos_url": "https://github.com/api/v3/users/xinyuluo/repos", 47 | "events_url": "https://github.com/api/v3/users/xinyuluo/events{/privacy}", 48 | "received_events_url": "https://github.com/api/v3/users/xinyuluo/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": null, 53 | "parents": [ 54 | { 55 | "sha": "a35de6fbf226029c018791c99060acb94d8f8be6", 56 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/a35de6fbf226029c018791c99060acb94d8f8be6", 57 | "html_url": "https://github.com/ahrefs/monorepo/commit/a35de6fbf226029c018791c99060acb94d8f8be6" 58 | } 59 | ], 60 | "stats": { 61 | "total": 1, 62 | "additions": 1, 63 | "deletions": 0 64 | }, 65 | "files": [ 66 | { 67 | "sha": "00750edc07d6415dcc07ae0351e9397b0222b7ba", 68 | "filename": "3/3.md", 69 | "status": "added", 70 | "additions": 1, 71 | "deletions": 0, 72 | "changes": 1, 73 | "blob_url": "https://github.com/ahrefs/monorepo/blob/184a35dd234e846cb5fe0723063f8bba03262e43/3/3.md", 74 | "raw_url": "https://github.com/ahrefs/monorepo/raw/184a35dd234e846cb5fe0723063f8bba03262e43/3/3.md", 75 | "contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/3/3.md?ref=184a35dd234e846cb5fe0723063f8bba03262e43", 76 | "patch": "" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /lib/rule.atd: -------------------------------------------------------------------------------- 1 | type channel_name = string wrap 2 | 3 | type regex = string wrap 4 | 5 | (* Text fields from the GitHub payload that can be used in a condition *) 6 | type comparable_field = [ 7 | | Context 8 | | Description 9 | | Target_url 10 | ] 11 | 12 | (* Checks whether a payload field matches the provided regex pattern *) 13 | type match_condition = { 14 | field : comparable_field; 15 | re : regex; 16 | } 17 | 18 | (* Specifies additional conditions the payload must meet for the status rule to match *) 19 | type status_condition = [ 20 | | Match of match_condition 21 | | All_of of status_condition list (* match all conditions in list *) 22 | | One_of of status_condition list (* match at least one condition in list *) 23 | | Not of status_condition (* don't match the sub-condition *) 24 | ] 25 | 26 | (* Filtering options; allow_once matches if the last matched status differs 27 | from the current one *) 28 | type status_policy = [ 29 | | Allow 30 | | Allow_once 31 | | Ignore 32 | ] 33 | 34 | type build_status = abstract 35 | 36 | (* A status matches a status rule with the policy with the build status is in 37 | the list, and the condition is true. 38 | 39 | The default behavior for each status state is: 40 | - pending: ignore 41 | - failure: allow 42 | - error: allow 43 | - success: allow_once 44 | *) 45 | type status_rule = { 46 | trigger : build_status list; 47 | ?condition : status_condition nullable; 48 | policy : status_policy; 49 | ~notify_channels : bool; 50 | ~notify_dm : bool; 51 | } 52 | 53 | (* A filename matches a prefix rule with the channel name if it isn't in the ignore 54 | list and it is in the allow list. If multiple prefix rules match for a given 55 | file, the one to match with the longest prefix is used. Both [allow] and 56 | [ignore] are optional. If [allow] is undefined, then the rule matches all 57 | payloads. 58 | 59 | If a commit affects 3 files: 60 | - some/dir/a 61 | - some/dir/b 62 | - some/other/dir/c 63 | 64 | And we are only interested by commits affecting files in some/dir 65 | 66 | allow should be ["some/dir"] 67 | 68 | or 69 | 70 | ignore should be ["some/other"] 71 | *) 72 | type prefix_rule = { 73 | ?allow : string list nullable; 74 | ?ignore : string list nullable; 75 | ?branch_filters : string list nullable; 76 | channel_name : channel_name; 77 | } 78 | 79 | (* A payload matches a label rule with a channel name if absent from the ignore list 80 | and present in the allow list. Both [allow] and [ignore] are optional. If [allow] 81 | is undefined, then the rule matches all payloads. *) 82 | type label_rule = { 83 | ?allow : string list nullable; 84 | ?ignore : string list nullable; 85 | channel_name : channel_name; 86 | } 87 | 88 | (* Requests reviews from [owners] if a PR is labeled with [label]. Owner format: 89 | - users: "username" 90 | - teams: "org/team" 91 | *) 92 | type project_owners_rule = { 93 | ?label: string option; 94 | ~labels : string list; 95 | owners: string list; 96 | } 97 | -------------------------------------------------------------------------------- /lib/atd_adapters.ml: -------------------------------------------------------------------------------- 1 | module List_or_default_field = struct 2 | open Atdgen_runtime.Json_adapter 3 | 4 | module type Param = sig 5 | (** the name of the field *) 6 | val field_name : string 7 | 8 | (** string alias a user can use to denote default value *) 9 | val default_alias : string 10 | 11 | (** Yojson representation of default value *) 12 | val default_value : Yojson.Safe.t 13 | end 14 | 15 | module Make (P : Param) : S = struct 16 | open P 17 | 18 | let assoc_replace l k v = List.map (fun x -> if fst x = k then k, v else x) l 19 | 20 | let normalize x = 21 | match x with 22 | | `Assoc fields -> begin 23 | match List.assoc field_name fields with 24 | | `String s when String.equal s default_alias -> `Assoc (assoc_replace fields field_name default_value) 25 | | _ | (exception Not_found) -> x 26 | end 27 | | malformed -> malformed 28 | 29 | let restore x = 30 | match x with 31 | | `Assoc fields -> begin 32 | match List.assoc field_name fields with 33 | | value when Yojson.Safe.equal value default_value -> `Assoc (assoc_replace fields field_name (`String "any")) 34 | | _ | (exception Not_found) -> x 35 | end 36 | | malformed -> malformed 37 | end 38 | end 39 | 40 | module Branch_filters_adapter = List_or_default_field.Make (struct 41 | let field_name = "branch_filters" 42 | let default_alias = "any" 43 | let default_value = `List [] 44 | end) 45 | 46 | (** Error detection in Slack API response. The web API communicates errors using 47 | an [error] field rather than status codes. Note, on the other hand, that 48 | webhooks do use status codes to communicate errors. *) 49 | module Slack_response_adapter : Atdgen_runtime.Json_adapter.S = struct 50 | let normalize (x : Yojson.Safe.t) = 51 | match x with 52 | | `Assoc fields -> begin 53 | match List.assoc "ok" fields with 54 | | `Bool true -> `List [ `String "Ok"; x ] 55 | | `Bool false -> begin 56 | match List.assoc "error" fields with 57 | | `String msg -> `List [ `String "Error"; `String msg ] 58 | | _ -> x 59 | end 60 | | _ | (exception Not_found) -> x 61 | end 62 | | _ -> x 63 | 64 | let restore (x : Yojson.Safe.t) = 65 | let mk_fields ok fields = ("ok", `Bool ok) :: List.filter (fun (k, _) -> k <> "ok") fields in 66 | match x with 67 | | `List [ `String "Ok"; `Assoc fields ] -> `Assoc (mk_fields true fields) 68 | | `List [ `String "Error"; `String msg ] -> `Assoc (mk_fields false [ "error", `String msg ]) 69 | | _ -> x 70 | end 71 | 72 | (* This adapter is meant to avoid breaking changes in the config because the type for 73 | [allowed_pipelines] was changed from a string list to a pipeline record list. *) 74 | module Strings_to_pipelines_adapter : Atdgen_runtime.Json_adapter.S = struct 75 | let normalize (x : Yojson.Safe.t) = 76 | match x with 77 | | `String s -> `Assoc [ "name", `String s ] 78 | | _ -> x 79 | 80 | let restore (x : Yojson.Safe.t) = 81 | match x with 82 | | `Assoc [ ("name", `String s) ] -> `String s 83 | | _ -> x 84 | end 85 | 86 | (* This adapter is meant to avoid breaking changes in the config because the type for 87 | [repo_config] was changed and [gh_token] was removed and [auth] was added. *) 88 | module GH_token_to_auth_adapter : Atdgen_runtime.Json_adapter.S = struct 89 | let normalize (x : Yojson.Safe.t) = 90 | match x with 91 | | `Assoc ks -> 92 | `Assoc 93 | (List.map 94 | (fun (k, v) -> 95 | match k with 96 | | "gh_token" -> "auth", `List [ `String "GH_token"; v ] 97 | | _ -> k, v) 98 | ks) 99 | | _ -> x 100 | 101 | let restore (x : Yojson.Safe.t) = x 102 | end 103 | -------------------------------------------------------------------------------- /test/buildkite-api-cache/organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5e-4e4a-9e0b-b8b64e381c90_logs: -------------------------------------------------------------------------------- 1 | {"url":"https://api.buildkite.com/v2/organizations/monorobot-test-org/pipelines/monorobot-test-pipeline/builds/87/jobs/0194d568-06d5-4785-9af9-8766874cd8ab/log","content":"\u001B_bk;t=1738747164589\u0007~~~ Preparing working directory\r\n\u001B_bk;t=1738747164589\u0007\u001B[90m$\u001B[0m cd /home/emile/.buildkite-agent/builds/emile-tarides-1/monorobot-test-org/monorobot-test-pipeline\r\n\u001B_bk;t=1738747164601\u0007Pseudo-terminal will not be allocated because stdin is not a terminal.\r\r\n\u001B_bk;t=1738747164602\u0007\u001B[90m# Host \"github.com\" already in list of known hosts at \"/home/emile/.ssh/known_hosts\"\u001B[0m\r\n\u001B_bk;t=1738747164604\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747164607\u0007\u001B[90m$\u001B[0m git fetch -v --prune -- origin 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167597\u0007From github.com:EmileTrotignon/monorobot_test\r\r\n\u001B_bk;t=1738747167597\u0007 * branch 404671053ebacf0245dc5b60566eb85e19465860 -> FETCH_HEAD\r\r\n\u001B_bk;t=1738747167873\u0007\u001B[90m$\u001B[0m git checkout -f 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167877\u0007HEAD is now at 4046710 test change\r\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m# Cleaning again to catch any post-checkout changes\u001B[0m\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m# Checking to see if git commit information needs to be sent to Buildkite...\u001B[0m\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m$\u001B[0m buildkite-agent meta-data exists buildkite:git:commit\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m# Git commit information has already been sent to Buildkite\u001B[0m\r\n\u001B_bk;t=1738747168296\u0007~~~ Running commands\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m$\u001B[0m echo \"Test the rocket\"\r\n\u001B_bk;t=1738747168296\u0007dune test\r\n\u001B_bk;t=1738747168296\u0007exit 0\r\n\u001B_bk;t=1738747168296\u0007\r\n\u001B_bk;t=1738747168300\u0007Test the rocket\r\r\n\u001B_bk;t=1738747168330\u0007Done: 60% (3/5, 2 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 1)\r \rDone: 77% (7/9, 2 left) (jobs: 1)\r \rDone: 90% (9/10, 1 left) (jobs: 1)\r \r\u001B[0;1mFile \"tests/test.t\", line 1, characters 0-0:\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[2;37m/usr/bin/git --no-pager diff --no-index --color=always -u _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mdiff --git a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mindex 88404ca..a9e2880 100644\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m--- a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m+++ b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[36m@@ -1,2 +1,2 @@\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007 $ dune exec backend\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[31m- aaabvaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[32m+\u001B[0m\u001B[32m aaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007Done: 90% (9/10, 1 left) (jobs: 1)\r \r^^^ +++\r\n\u001B_bk;t=1738747168448\u0007\u001B[31m🚨 Error: The command exited with status 1\u001B[0m\r\n\u001B_bk;t=1738747168448\u0007^^^ +++\r\n\u001B_bk;t=1738747168448\u0007user command error: exit status 1\r\n","size":3099,"header_times":[]} 2 | -------------------------------------------------------------------------------- /test/buildkite-api-cache/organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5f-44e0-ae9b-0b6f33f48ac8_logs: -------------------------------------------------------------------------------- 1 | {"url":"https://api.buildkite.com/v2/organizations/monorobot-test-org/pipelines/monorobot-test-pipeline/builds/87/jobs/0194d568-06d5-4785-9af9-8766874cd8ab/log","content":"\u001B_bk;t=1738747164589\u0007~~~ Preparing working directory\r\n\u001B_bk;t=1738747164589\u0007\u001B[90m$\u001B[0m cd /home/emile/.buildkite-agent/builds/emile-tarides-1/monorobot-test-org/monorobot-test-pipeline\r\n\u001B_bk;t=1738747164601\u0007Pseudo-terminal will not be allocated because stdin is not a terminal.\r\r\n\u001B_bk;t=1738747164602\u0007\u001B[90m# Host \"github.com\" already in list of known hosts at \"/home/emile/.ssh/known_hosts\"\u001B[0m\r\n\u001B_bk;t=1738747164604\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747164607\u0007\u001B[90m$\u001B[0m git fetch -v --prune -- origin 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167597\u0007From github.com:EmileTrotignon/monorobot_test\r\r\n\u001B_bk;t=1738747167597\u0007 * branch 404671053ebacf0245dc5b60566eb85e19465860 -> FETCH_HEAD\r\r\n\u001B_bk;t=1738747167873\u0007\u001B[90m$\u001B[0m git checkout -f 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167877\u0007HEAD is now at 4046710 test change\r\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m# Cleaning again to catch any post-checkout changes\u001B[0m\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m# Checking to see if git commit information needs to be sent to Buildkite...\u001B[0m\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m$\u001B[0m buildkite-agent meta-data exists buildkite:git:commit\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m# Git commit information has already been sent to Buildkite\u001B[0m\r\n\u001B_bk;t=1738747168296\u0007~~~ Running commands\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m$\u001B[0m echo \"Test the rocket\"\r\n\u001B_bk;t=1738747168296\u0007dune test\r\n\u001B_bk;t=1738747168296\u0007exit 0\r\n\u001B_bk;t=1738747168296\u0007\r\n\u001B_bk;t=1738747168300\u0007Test the rocket\r\r\n\u001B_bk;t=1738747168330\u0007Done: 60% (3/5, 2 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 1)\r \rDone: 77% (7/9, 2 left) (jobs: 1)\r \rDone: 90% (9/10, 1 left) (jobs: 1)\r \r\u001B[0;1mFile \"tests/test.t\", line 1, characters 0-0:\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[2;37m/usr/bin/git --no-pager diff --no-index --color=always -u _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mdiff --git a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mindex 88404ca..a9e2880 100644\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m--- a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m+++ b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[36m@@ -1,2 +1,2 @@\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007 $ dune exec backend\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[31m- aaabvaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[32m+\u001B[0m\u001B[32m aaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007Done: 90% (9/10, 1 left) (jobs: 1)\r \r^^^ +++\r\n\u001B_bk;t=1738747168448\u0007\u001B[31m🚨 Error: The command exited with status 1\u001B[0m\r\n\u001B_bk;t=1738747168448\u0007^^^ +++\r\n\u001B_bk;t=1738747168448\u0007user command error: exit status 1\r\n","size":3099,"header_times":[]} 2 | -------------------------------------------------------------------------------- /lib/text_cleanup/test/job_log.t/organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5e-4e4a-9e0b-b8b64e381c90_logs: -------------------------------------------------------------------------------- 1 | {"url":"https://api.buildkite.com/v2/organizations/monorobot-test-org/pipelines/monorobot-test-pipeline/builds/87/jobs/0194d568-06d5-4785-9af9-8766874cd8ab/log","content":"\u001B_bk;t=1738747164589\u0007~~~ Preparing working directory\r\n\u001B_bk;t=1738747164589\u0007\u001B[90m$\u001B[0m cd /home/emile/.buildkite-agent/builds/emile-tarides-1/monorobot-test-org/monorobot-test-pipeline\r\n\u001B_bk;t=1738747164601\u0007Pseudo-terminal will not be allocated because stdin is not a terminal.\r\r\n\u001B_bk;t=1738747164602\u0007\u001B[90m# Host \"github.com\" already in list of known hosts at \"/home/emile/.ssh/known_hosts\"\u001B[0m\r\n\u001B_bk;t=1738747164604\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747164607\u0007\u001B[90m$\u001B[0m git fetch -v --prune -- origin 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167597\u0007From github.com:EmileTrotignon/monorobot_test\r\r\n\u001B_bk;t=1738747167597\u0007 * branch 404671053ebacf0245dc5b60566eb85e19465860 -> FETCH_HEAD\r\r\n\u001B_bk;t=1738747167873\u0007\u001B[90m$\u001B[0m git checkout -f 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167877\u0007HEAD is now at 4046710 test change\r\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m# Cleaning again to catch any post-checkout changes\u001B[0m\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m# Checking to see if git commit information needs to be sent to Buildkite...\u001B[0m\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m$\u001B[0m buildkite-agent meta-data exists buildkite:git:commit\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m# Git commit information has already been sent to Buildkite\u001B[0m\r\n\u001B_bk;t=1738747168296\u0007~~~ Running commands\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m$\u001B[0m echo \"Test the rocket\"\r\n\u001B_bk;t=1738747168296\u0007dune test\r\n\u001B_bk;t=1738747168296\u0007exit 0\r\n\u001B_bk;t=1738747168296\u0007\r\n\u001B_bk;t=1738747168300\u0007Test the rocket\r\r\n\u001B_bk;t=1738747168330\u0007Done: 60% (3/5, 2 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 1)\r \rDone: 77% (7/9, 2 left) (jobs: 1)\r \rDone: 90% (9/10, 1 left) (jobs: 1)\r \r\u001B[0;1mFile \"tests/test.t\", line 1, characters 0-0:\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[2;37m/usr/bin/git --no-pager diff --no-index --color=always -u _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mdiff --git a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mindex 88404ca..a9e2880 100644\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m--- a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m+++ b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[36m@@ -1,2 +1,2 @@\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007 $ dune exec backend\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[31m- aaabvaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[32m+\u001B[0m\u001B[32m aaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007Done: 90% (9/10, 1 left) (jobs: 1)\r \r^^^ +++\r\n\u001B_bk;t=1738747168448\u0007\u001B[31m🚨 Error: The command exited with status 1\u001B[0m\r\n\u001B_bk;t=1738747168448\u0007^^^ +++\r\n\u001B_bk;t=1738747168448\u0007user command error: exit status 1\r\n","size":3099,"header_times":[]} 2 | -------------------------------------------------------------------------------- /lib/text_cleanup/test/job_log.t/organizations_ahrefs_pipelines_pipeline2_builds_181734_jobs_01948e8b-5b5f-44e0-ae9b-0b6f33f48ac8_logs: -------------------------------------------------------------------------------- 1 | {"url":"https://api.buildkite.com/v2/organizations/monorobot-test-org/pipelines/monorobot-test-pipeline/builds/87/jobs/0194d568-06d5-4785-9af9-8766874cd8ab/log","content":"\u001B_bk;t=1738747164589\u0007~~~ Preparing working directory\r\n\u001B_bk;t=1738747164589\u0007\u001B[90m$\u001B[0m cd /home/emile/.buildkite-agent/builds/emile-tarides-1/monorobot-test-org/monorobot-test-pipeline\r\n\u001B_bk;t=1738747164601\u0007Pseudo-terminal will not be allocated because stdin is not a terminal.\r\r\n\u001B_bk;t=1738747164602\u0007\u001B[90m# Host \"github.com\" already in list of known hosts at \"/home/emile/.ssh/known_hosts\"\u001B[0m\r\n\u001B_bk;t=1738747164604\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747164607\u0007\u001B[90m$\u001B[0m git fetch -v --prune -- origin 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167597\u0007From github.com:EmileTrotignon/monorobot_test\r\r\n\u001B_bk;t=1738747167597\u0007 * branch 404671053ebacf0245dc5b60566eb85e19465860 -> FETCH_HEAD\r\r\n\u001B_bk;t=1738747167873\u0007\u001B[90m$\u001B[0m git checkout -f 404671053ebacf0245dc5b60566eb85e19465860\r\n\u001B_bk;t=1738747167877\u0007HEAD is now at 4046710 test change\r\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m# Cleaning again to catch any post-checkout changes\u001B[0m\r\n\u001B_bk;t=1738747167878\u0007\u001B[90m$\u001B[0m git clean -ffxdq\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m# Checking to see if git commit information needs to be sent to Buildkite...\u001B[0m\r\n\u001B_bk;t=1738747167881\u0007\u001B[90m$\u001B[0m buildkite-agent meta-data exists buildkite:git:commit\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m# Git commit information has already been sent to Buildkite\u001B[0m\r\n\u001B_bk;t=1738747168296\u0007~~~ Running commands\r\n\u001B_bk;t=1738747168296\u0007\u001B[90m$\u001B[0m echo \"Test the rocket\"\r\n\u001B_bk;t=1738747168296\u0007dune test\r\n\u001B_bk;t=1738747168296\u0007exit 0\r\n\u001B_bk;t=1738747168296\u0007\r\n\u001B_bk;t=1738747168300\u0007Test the rocket\r\r\n\u001B_bk;t=1738747168330\u0007Done: 60% (3/5, 2 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 0)\r \rDone: 55% (5/9, 4 left) (jobs: 1)\r \rDone: 77% (7/9, 2 left) (jobs: 1)\r \rDone: 90% (9/10, 1 left) (jobs: 1)\r \r\u001B[0;1mFile \"tests/test.t\", line 1, characters 0-0:\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[2;37m/usr/bin/git --no-pager diff --no-index --color=always -u _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t _build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mdiff --git a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1mindex 88404ca..a9e2880 100644\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m--- a/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[1m+++ b/_build/.sandbox/ca5ef14ba31cbcd6e129c4c5b8cdc872/default/tests/test.t.corrected\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[36m@@ -1,2 +1,2 @@\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007 $ dune exec backend\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[31m- aaabvaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007\u001B[32m+\u001B[0m\u001B[32m aaaa\u001B[0m\r\r\n\u001B_bk;t=1738747168446\u0007Done: 90% (9/10, 1 left) (jobs: 1)\r \r^^^ +++\r\n\u001B_bk;t=1738747168448\u0007\u001B[31m🚨 Error: The command exited with status 1\u001B[0m\r\n\u001B_bk;t=1738747168448\u0007^^^ +++\r\n\u001B_bk;t=1738747168448\u0007user command error: exit status 1\r\n","size":3099,"header_times":[]} 2 | -------------------------------------------------------------------------------- /lib/debug_db.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let replay_action db_path pipeline branch only_with_changes after state build_number sha from to_ = 4 | Lwt_main.run 5 | (let db_path = Option.default "db/monorobot.db" db_path in 6 | let%lwt () = Database.init db_path in 7 | match pipeline, branch with 8 | | None, _ | _, None -> failwith "pipeline and branch are required" 9 | | Some pipeline, Some branch -> 10 | let q, v = 11 | let open Database.Debug_db in 12 | let args = 13 | [ 14 | "after", Option.map string_of_int after; 15 | "build_number", Option.map string_of_int build_number; 16 | "sha", sha; 17 | "from", Option.map string_of_int from; 18 | "to", Option.map string_of_int to_; 19 | ] 20 | in 21 | match List.filter (fun (_, arg) -> Option.is_some arg) args with 22 | | [] -> failwith "no options provided. Please provide a search criteria, like --after, --build-number, --sha" 23 | | [ ("after", Some after) ] -> get_after ~branch ~pipeline, after 24 | | [ ("build_number", Some build_number) ] -> get_by_build_number ~branch ~pipeline, build_number 25 | | [ ("sha", Some sha) ] -> get_by_sha ~branch ~pipeline, sha 26 | | [ ("from", Some from); ("to", Some to_) ] | [ ("to", Some to_); ("from", Some from) ] -> 27 | get_from_to ~branch ~pipeline ~from ~to_, "" 28 | | _ :: _ :: _ -> 29 | failwith "too many options: the after, build_number, sha and step_name options cannot be used together" 30 | | _ -> failwith "unexpected options" 31 | in 32 | let fold ~id ~sha ~build_state ~build_number ~is_canceled ~has_state_update ~last_handled_in 33 | ~state_before_notification ~state_after_notification acc = 34 | let row_log = 35 | let print_commit_hash s = String.sub s 0 (min 8 @@ String.length s) in 36 | let state_diff = 37 | match has_state_update with 38 | | false -> "\n" 39 | | true -> 40 | let json = function 41 | | "" -> "" 42 | | s -> Yojson.Basic.(from_string s |> pretty_to_string) 43 | in 44 | sprintf "# STATE DIFF:\n\n%s\n" 45 | Odiff.( 46 | strings_diffs (json state_before_notification) (json state_after_notification) |> string_of_diffs) 47 | in 48 | let header = 49 | sprintf 50 | "==============================================================\n\ 51 | [%s/%Ld] branch=%s, commit=%s, state=%s, canceled=%b, db_id=%s" pipeline build_number branch 52 | (print_commit_hash sha) build_state is_canceled id 53 | in 54 | let build_state' = 55 | match Buildkite_j.build_state_of_string build_state with 56 | | Passed when has_state_update -> "FIXED" 57 | | Passed -> "PASSED" 58 | | Failed -> "FAILED" 59 | | Canceled -> "CANCELED" 60 | | _ -> "UNKNOWNED: " ^ build_state 61 | in 62 | sprintf {|%s 63 | 64 | # BUILD NUMBER: %Ld 65 | # BUILD STATE: %s 66 | # LAST HANDLED IN: %s 67 | # HAS STATE UPDATE: %b 68 | %s|} header 69 | build_number build_state' last_handled_in has_state_update state_diff 70 | in 71 | let keep_row = 72 | let with_changes_filter = 73 | match only_with_changes with 74 | | true when has_state_update -> true 75 | | true -> false 76 | | false -> true 77 | in 78 | let state_filter = 79 | match state with 80 | | None -> true 81 | | Some state -> state = Buildkite_j.build_state_of_string build_state 82 | in 83 | with_changes_filter && state_filter 84 | in 85 | match keep_row with 86 | | false -> acc 87 | | true -> sprintf "%s\n%s" row_log acc 88 | in 89 | (match%lwt q v fold with 90 | | Ok log -> 91 | print_endline log; 92 | Lwt.return_unit 93 | | Error e -> failwith e 94 | | Db_unavailable -> failwith "database unavailable")) 95 | -------------------------------------------------------------------------------- /lib/buildkite.atd: -------------------------------------------------------------------------------- 1 | (* We keep this type an open enum because buildkite's documentation is not up to date and might have 2 | more states. It shouldn't be an issue because we don't care about all the possible states. *) 3 | type build_state = [ 4 | | Blocked 5 | | Canceled 6 | | Canceling 7 | | Failed 8 | | Failing 9 | | Finished 10 | | Not_run 11 | | Passed 12 | | Running 13 | | Scheduled 14 | | Skipped 15 | | Other of string 16 | ] 17 | 18 | 19 | type job_state = [ 20 | | Pending 21 | | Waiting 22 | | Waiting_failed 23 | | Blocked 24 | | Blocked_failed 25 | | Unblocked 26 | | Unblocked_failed 27 | | Limiting 28 | | Limited 29 | | Scheduled 30 | | Assigned 31 | | Accepted 32 | | Running 33 | | Finished 34 | | Canceling 35 | | Canceled 36 | | Expired 37 | | Timing_out 38 | | Timed_out 39 | | Skipped 40 | | Broken 41 | | Passed 42 | | Failed 43 | | Other of string 44 | ] 45 | 46 | type job_log = { 47 | url: string; 48 | content: string; 49 | size: int 50 | } 51 | 52 | type agent = { 53 | hostname: string; 54 | } 55 | 56 | type job = { 57 | id : string; 58 | ?log_url: string option; 59 | name : string; 60 | ?step_key: string nullable; 61 | type_ : string; 62 | state : job_state; 63 | web_url : string; 64 | ?agent : agent nullable; 65 | } 66 | 67 | type non_job = { 68 | id: string; 69 | } 70 | 71 | type job_type = [ 72 | | Script of job 73 | | Trigger of job 74 | | Manual of non_job 75 | | Waiter of non_job 76 | ] 77 | 78 | type jobs = job_type list 79 | 80 | type get_build_res = { 81 | inherit build_base; 82 | created_at: string; 83 | jobs: jobs; 84 | } 85 | 86 | (* Custom types for the steps state and not a Buildkite type. We have them here 87 | to avoid circular dependencies with common.atd, common.ml and state.ml *) 88 | type timestamp = string wrap 89 | 90 | type failed_step = { 91 | id: string; 92 | name: string; 93 | build_url: string; 94 | author: string; 95 | created_at: timestamp; 96 | escalated_at: timestamp nullable; 97 | } 98 | 99 | (* for now we only subscribe to build.finished *) 100 | type webhook_event = [ 101 | | Build_finished 102 | | Other of string 103 | ] 104 | 105 | type build_metadata = { 106 | ?commit : string nullable; 107 | } 108 | 109 | type build_base = { 110 | id: string; 111 | web_url: string; 112 | number: int; 113 | state: build_state; 114 | created_at: string; 115 | message: string; 116 | sha : string; 117 | branch: string; 118 | ?meta_data: build_metadata nullable; 119 | } 120 | 121 | type pipeline_provider_settings = { 122 | repository: string; 123 | } 124 | 125 | type pipeline_provider = { 126 | settings: pipeline_provider_settings; 127 | } 128 | 129 | type pipeline = { 130 | id: string; 131 | web_url: string; 132 | name: string; 133 | description: string; 134 | repository: string; 135 | slug: string nullable; 136 | provider: pipeline_provider; 137 | } 138 | 139 | type webhook_build_payload = { 140 | event: webhook_event; 141 | build: build_base; 142 | pipeline: pipeline; 143 | } 144 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorobot_pull_107: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "login": "yasunariw", 4 | "id": 7478035, 5 | "url": "https://api.github.com/users/yasunariw", 6 | "html_url": "https://github.com/yasunariw", 7 | "avatar_url": "https://avatars.githubusercontent.com/u/7478035?v=4" 8 | }, 9 | "number": 107, 10 | "body": "## Description of the task\r\n\r\nBecause two builds on the same pipeline + branch don't necessarily always run the same build steps, the current state tracking is insufficient for handling those cases. Instead of tracking build state per pipeline, we should track it per build step.\r\n\r\n**Some details regarding Buildkite notification behavior:**\r\n\r\n- There are two types to consider -- notifications for overall builds (\"Build #123 failed\") vs those for individual steps within a larger build (\"step XYZ of build #123 failed\").\r\n- The only way to differentiate b/w them is to look at the `context` field, which is either `buildkite/pipeline-name` for the former or `buildkite/pipeline-name/build-step` for the latter. \r\n- The final notification of the overall build is always sent after the final notification for any of the build steps.\r\n\r\n**The implementation:**\r\n\r\nFor each status notification that passes the rule check (which should only be overall build notifications), we:\r\n1. Retrieve all status notifications associated with this commit using [GH's API](https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#list-commit-statuses-for-a-reference)\r\n2. Filter the list to only get the status notifications that belong to the same build (in case of rebuilds)\r\n3. Filter the list again for the most recent status notification of each build step (which should either be success or failure)\r\n4. Update the runtime state, which maps pipeline/build step names to per-branch build states.\r\n5. Just like before, this is then queried whenever there is an \"allow_once\" match. An \"allow_once\" match will generate a notification if any of the build steps associated with the current build have a different status value from the previous build.\r\n\r\n**Future work:**\r\n\r\nIt would be nice if build failure notifications also told us which build step failed. This can be done easily by making `Action.partition_status` returned the build steps that failed, so that `Slack.generate_status_notification` can include them in the final message. \r\n\r\n**Final note:**\r\n\r\nLarge diff size is due to the addition of HTTP request/response stubs - apologies.\r\n\r\n## How to test\r\n\r\nExisting tests should pass.\r\n\r\nTwo cases are added; the second handles the [behavior observed in production](https://ahrefs.slack.com/archives/CKZANG2TE/p1609971822011500) that motivated this PR.\r\n\r\n`status.success_test_different_steps_from_prev`\r\n\r\nThis is for an incoming successful build on develop branch with a single build step, \"notabot-test/build\". There exist past successes for same branch + different steps, and for different branch + same step, but not for same branch + same step. Thus, it should generate a notification.\r\n\r\n`status.success_test_not_affected_by_unrelated_success_with_different_steps`\r\n\r\nThis is for an incoming successful build on develop branch with two steps, \"notabot-test/{build-infra,setup}\". Here, previously a build with steps \"build-infra\" and \"setup\" failed on step \"build-infra\". Then a subsequent build with step \"build\" succeeded, so the overall pipeline state is a success. However, the more recent successful but unrelated build should not affect generation of another success notification, given the change in status state for \"build-infra\".\r\n\r\n```\r\nmake test\r\n```\r\n\r\n## References\r\n\r\n- existing issue: #80 \r\n- Slack discussion: https://ahrefs.slack.com/archives/CKZANG2TE/p1609971822011500\r\n- other?\r\n", 11 | "title": "Track status state per build step instead of per pipeline", 12 | "html_url": "https://github.com/ahrefs/monorobot/pull/107", 13 | "labels": [], 14 | "state": "open", 15 | "requested_reviewers": [ 16 | { 17 | "login": "ygrek", 18 | "id": 104087, 19 | "url": "https://api.github.com/users/ygrek", 20 | "html_url": "https://github.com/ygrek", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/104087?v=4" 22 | }, 23 | { 24 | "login": "Khady", 25 | "id": 974142, 26 | "url": "https://api.github.com/users/Khady", 27 | "html_url": "https://github.com/Khady", 28 | "avatar_url": "https://avatars.githubusercontent.com/u/974142?v=4" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /test/github-api-cache/6113728f27ae82c7b1a177c8d03f9e96e0adf246: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "6113728f27ae82c7b1a177c8d03f9e96e0adf246", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "Codertocat", 7 | "email": "21031067+Codertocat@users.noreply.github.com", 8 | "date": "2019-05-15T15:19:25Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub", 12 | "email": "noreply@github.com", 13 | "date": "2019-05-15T15:19:25Z" 14 | }, 15 | "message": "Initial commit", 16 | "tree": { 17 | "sha": "1b13fc88733f95cc8cb16170f6990ef30d78acf4", 18 | "url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees/1b13fc88733f95cc8cb16170f6990ef30d78acf4" 19 | }, 20 | "url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits/6113728f27ae82c7b1a177c8d03f9e96e0adf246", 21 | "comment_count": 1, 22 | "verification": { 23 | "verified": true, 24 | "reason": "valid", 25 | "signature": "", 26 | "payload": "tree 1b13fc88733f95cc8cb16170f6990ef30d78acf4\nauthor Codertocat <21031067+Codertocat@users.noreply.github.com> 1557933565 -0500\ncommitter GitHub 1557933565 -0500\n\nInitial commit" 27 | } 28 | }, 29 | "url": "https://api.github.com/repos/Codertocat/Hello-World/commits/6113728f27ae82c7b1a177c8d03f9e96e0adf246", 30 | "html_url": "https://github.com/Codertocat/Hello-World/commit/6113728f27ae82c7b1a177c8d03f9e96e0adf246", 31 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/commits/6113728f27ae82c7b1a177c8d03f9e96e0adf246/comments", 32 | "author": { 33 | "login": "Codertocat", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://avatars1.githubusercontent.com/u/000000?v=4", 37 | "gravatar_id": "", 38 | "url": "https://api.github.com/users/Codertocat", 39 | "html_url": "https://github.com/Codertocat", 40 | "followers_url": "https://api.github.com/users/Codertocat/followers", 41 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 42 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 43 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 45 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 46 | "repos_url": "https://api.github.com/users/Codertocat/repos", 47 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 48 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": { 53 | "login": "web-flow", 54 | "id": 0, 55 | "node_id": "00000000000000000000", 56 | "avatar_url": "https://avatars3.githubusercontent.com/u/000000?v=4", 57 | "gravatar_id": "", 58 | "url": "https://api.github.com/users/web-flow", 59 | "html_url": "https://github.com/web-flow", 60 | "followers_url": "https://api.github.com/users/web-flow/followers", 61 | "following_url": "https://api.github.com/users/web-flow/following{/other_user}", 62 | "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", 63 | "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", 64 | "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", 65 | "organizations_url": "https://api.github.com/users/web-flow/orgs", 66 | "repos_url": "https://api.github.com/users/web-flow/repos", 67 | "events_url": "https://api.github.com/users/web-flow/events{/privacy}", 68 | "received_events_url": "https://api.github.com/users/web-flow/received_events", 69 | "type": "User", 70 | "site_admin": false 71 | }, 72 | "parents": [ 73 | 74 | ], 75 | "stats": { 76 | "total": 1, 77 | "additions": 1, 78 | "deletions": 0 79 | }, 80 | "files": [ 81 | { 82 | "sha": "94d2833914c5d2d9a0ba1f03b59cd90f65d13c90", 83 | "filename": "README.md", 84 | "status": "added", 85 | "additions": 1, 86 | "deletions": 0, 87 | "changes": 1, 88 | "blob_url": "https://github.com/Codertocat/Hello-World/blob/6113728f27ae82c7b1a177c8d03f9e96e0adf246/README.md", 89 | "raw_url": "https://github.com/Codertocat/Hello-World/raw/6113728f27ae82c7b1a177c8d03f9e96e0adf246/README.md", 90 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/README.md?ref=6113728f27ae82c7b1a177c8d03f9e96e0adf246", 91 | "patch": "" 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /test/github-api-cache/xinyuluo_monorepo_commit_cd5b85afa306840e0790b62e349ee1f828b2a3c2: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "cd5b85afa306840e0790b62e349ee1f828b2a3c2", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "xinyuluo", 7 | "email": "mail@example.org", 8 | "date": "2020-05-21T03:29:03Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub", 12 | "email": "noreply@github.com", 13 | "date": "2020-05-21T03:29:03Z" 14 | }, 15 | "message": "add new line at EOF", 16 | "tree": { 17 | "sha": "4ab482439e62c4cf48dc274c9a22a4483cfc4a2c", 18 | "url": "https://api.github.com/repos/xinyuluo/monorepo/git/trees/4ab482439e62c4cf48dc274c9a22a4483cfc4a2c" 19 | }, 20 | "url": "https://api.github.com/repos/xinyuluo/monorepo/git/commits/cd5b85afa306840e0790b62e349ee1f828b2a3c2", 21 | "comment_count": 2, 22 | "verification": { 23 | "verified": true, 24 | "reason": "valid", 25 | "signature": "", 26 | "payload": "tree 4ab482439e62c4cf48dc274c9a22a4483cfc4a2c\nparent 1edcdf5e45a8b7ed01c247b981d8f42d426a7794\nauthor xinyuluo 1590031743 +0800\ncommitter GitHub 1590031743 +0800\n\nadd new line at EOF" 27 | } 28 | }, 29 | "url": "https://api.github.com/repos/xinyuluo/monorepo/commits/cd5b85afa306840e0790b62e349ee1f828b2a3c2", 30 | "html_url": "https://github.com/xinyuluo/monorepo/commit/cd5b85afa306840e0790b62e349ee1f828b2a3c2", 31 | "comments_url": "https://api.github.com/repos/xinyuluo/monorepo/commits/cd5b85afa306840e0790b62e349ee1f828b2a3c2/comments", 32 | "author": { 33 | "login": "xinyuluo", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://avatars0.githubusercontent.com/u/000000?v=4", 37 | "gravatar_id": "", 38 | "url": "https://api.github.com/users/xinyuluo", 39 | "html_url": "https://github.com/xinyuluo", 40 | "followers_url": "https://api.github.com/users/xinyuluo/followers", 41 | "following_url": "https://api.github.com/users/xinyuluo/following{/other_user}", 42 | "gists_url": "https://api.github.com/users/xinyuluo/gists{/gist_id}", 43 | "starred_url": "https://api.github.com/users/xinyuluo/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://api.github.com/users/xinyuluo/subscriptions", 45 | "organizations_url": "https://api.github.com/users/xinyuluo/orgs", 46 | "repos_url": "https://api.github.com/users/xinyuluo/repos", 47 | "events_url": "https://api.github.com/users/xinyuluo/events{/privacy}", 48 | "received_events_url": "https://api.github.com/users/xinyuluo/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": { 53 | "login": "web-flow", 54 | "id": 0, 55 | "node_id": "00000000000000000000", 56 | "avatar_url": "https://avatars3.githubusercontent.com/u/000000?v=4", 57 | "gravatar_id": "", 58 | "url": "https://api.github.com/users/web-flow", 59 | "html_url": "https://github.com/web-flow", 60 | "followers_url": "https://api.github.com/users/web-flow/followers", 61 | "following_url": "https://api.github.com/users/web-flow/following{/other_user}", 62 | "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", 63 | "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", 64 | "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", 65 | "organizations_url": "https://api.github.com/users/web-flow/orgs", 66 | "repos_url": "https://api.github.com/users/web-flow/repos", 67 | "events_url": "https://api.github.com/users/web-flow/events{/privacy}", 68 | "received_events_url": "https://api.github.com/users/web-flow/received_events", 69 | "type": "User", 70 | "site_admin": false 71 | }, 72 | "parents": [ 73 | { 74 | "sha": "1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 75 | "url": "https://api.github.com/repos/xinyuluo/monorepo/commits/1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 76 | "html_url": "https://github.com/xinyuluo/monorepo/commit/1edcdf5e45a8b7ed01c247b981d8f42d426a7794" 77 | } 78 | ], 79 | "stats": { 80 | "total": 2, 81 | "additions": 1, 82 | "deletions": 1 83 | }, 84 | "files": [ 85 | { 86 | "sha": "275f5447f80e191a99fe680fcc6457e1889d4b76", 87 | "filename": "main.ml", 88 | "status": "modified", 89 | "additions": 1, 90 | "deletions": 1, 91 | "changes": 2, 92 | "blob_url": "https://github.com/xinyuluo/monorepo/blob/cd5b85afa306840e0790b62e349ee1f828b2a3c2/main.ml", 93 | "raw_url": "https://github.com/xinyuluo/monorepo/raw/cd5b85afa306840e0790b62e349ee1f828b2a3c2/main.ml", 94 | "contents_url": "https://api.github.com/repos/xinyuluo/monorepo/contents/main.ml?ref=cd5b85afa306840e0790b62e349ee1f828b2a3c2", 95 | "patch": "" 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /lib/context.ml: -------------------------------------------------------------------------------- 1 | open Common 2 | open Devkit 3 | 4 | let log = Log.from "context" 5 | 6 | exception Context_error of string 7 | 8 | let context_error fmt = Printf.ksprintf (fun msg -> raise (Context_error msg)) fmt 9 | 10 | type t = { 11 | config_filename : string; 12 | secrets_filepath : string; 13 | state_filepath : string option; 14 | mutable secrets : Config_t.secrets option; 15 | config : Config_t.config Stringtbl.t; 16 | state : State.t; 17 | } 18 | 19 | let default_config_filename = "monorobot.json" 20 | let default_secrets_filepath = "secrets.json" 21 | 22 | let make ?config_filename ?secrets_filepath ?state_filepath () = 23 | { 24 | config_filename = Option.default default_config_filename config_filename; 25 | secrets_filepath = Option.default default_secrets_filepath secrets_filepath; 26 | state_filepath; 27 | secrets = None; 28 | config = Stringtbl.empty (); 29 | state = State.empty (); 30 | } 31 | 32 | let get_secrets_exn ctx = 33 | match ctx.secrets with 34 | | None -> context_error "secrets is uninitialized" 35 | | Some secrets -> secrets 36 | 37 | let find_repo_config ctx repo_url = Stringtbl.find_opt ctx.config repo_url 38 | 39 | let find_repo_config_exn ctx repo_url = 40 | match find_repo_config ctx repo_url with 41 | | None -> context_error "config uninitialized for repo %s" repo_url 42 | | Some config -> config 43 | 44 | let set_repo_config ctx repo_url config = Stringtbl.replace ctx.config repo_url config 45 | 46 | let gh_repo_of_secrets (secrets : Config_t.secrets) repo_url = 47 | let drop_scheme url = 48 | let uri = Uri.of_string url in 49 | Uri.with_scheme uri None 50 | in 51 | let repo_url = drop_scheme repo_url in 52 | match List.find_opt (fun r -> Uri.equal (drop_scheme r.Config_t.url) repo_url) secrets.repos with 53 | | None -> None 54 | | Some repo -> Some repo 55 | 56 | let gh_auth_of_secrets (secrets : Config_t.secrets) repo_url = 57 | match gh_repo_of_secrets secrets repo_url with 58 | | None | Some { auth = None; _ } -> None 59 | | Some { auth; _ } -> auth 60 | 61 | let gh_hook_secret_token_of_secrets (secrets : Config_t.secrets) repo_url = 62 | match gh_repo_of_secrets secrets repo_url with 63 | | None -> None 64 | | Some repo -> repo.gh_hook_secret 65 | 66 | let hook_of_channel ctx channel_name = 67 | let secrets = get_secrets_exn ctx in 68 | match 69 | List.find_opt 70 | (fun (webhook : Config_t.webhook) -> Slack_channel.equal webhook.channel channel_name) 71 | secrets.slack_hooks 72 | with 73 | | Some hook -> Some hook.url 74 | | None -> None 75 | 76 | (** [is_pipeline_allowed ctx repo_url notification] returns [true] if [status_rules] 77 | doesn't define a whitelist of allowed pipelines in the config of [repo_url], 78 | or if the list contains the pipeline name in [notification.context]; returns [false] otherwise. *) 79 | let is_pipeline_allowed ctx (n : Github_t.status_notification) = 80 | match find_repo_config ctx n.repository.url with 81 | | None -> true 82 | | Some config -> Option.is_some (Util.Build.get_pipeline_config config n) 83 | 84 | let refresh_secrets ctx = 85 | let open Util in 86 | let path = ctx.secrets_filepath in 87 | match Config_j.secrets_of_string (Std.input_file path) with 88 | | exception exn -> fmt_error ~exn "failed to read secrets from file %s" path 89 | | secrets -> 90 | match secrets.slack_access_token, secrets.slack_hooks with 91 | | None, [] -> fmt_error "either slack_access_token or slack_hooks must be defined in file %s" path 92 | | _ -> 93 | match secrets.repos with 94 | | [] -> fmt_error "at least one repository url must be specified in the 'repos' list in file %s" path 95 | | _ :: _ -> 96 | ctx.secrets <- Some secrets; 97 | Ok ctx 98 | 99 | let refresh_state ctx = 100 | match ctx.state_filepath with 101 | | None -> Ok ctx 102 | | Some path -> 103 | if Sys.file_exists path then begin 104 | log#info "loading saved state from file %s" path; 105 | (* todo: extract state related parts to state.ml *) 106 | match State_j.state_of_string (Std.input_file path) with 107 | | exception exn -> Util.fmt_error ~exn "failed to read state from file %s" path 108 | | state -> Ok { ctx with state = { State.state } } 109 | end 110 | else Ok ctx 111 | 112 | let print_config ctx repo_url = 113 | let cfg = find_repo_config_exn ctx repo_url in 114 | let secrets = get_secrets_exn ctx in 115 | let secret_token = gh_hook_secret_token_of_secrets secrets repo_url in 116 | log#info "using prefix routing:"; 117 | Rule.Prefix.print_prefix_routing cfg.prefix_rules.rules; 118 | log#info "using label routing:"; 119 | Rule.Label.print_label_routing cfg.label_rules.rules; 120 | log#info "signature checking %s" (if Option.is_some secret_token then "enabled" else "disabled") 121 | -------------------------------------------------------------------------------- /lib/common.ml: -------------------------------------------------------------------------------- 1 | open Devkit 2 | 3 | let ( let* ) = Lwt_result.bind 4 | 5 | module Slack_timestamp = Fresh (String) () 6 | 7 | module Timestamp = struct 8 | type t = Ptime.t 9 | 10 | let wrap s = 11 | match Ptime.of_rfc3339 s with 12 | | Ok (t, _, _) -> t 13 | | Error _ -> failwith "Invalid timestamp" 14 | let unwrap t = Ptime.to_rfc3339 t 15 | 16 | let wrap_with_fallback ?(fallback = Ptime_clock.now ()) s = 17 | match Ptime.of_rfc3339 s with 18 | | Ok (t, _, _) -> t 19 | | Error _ -> fallback 20 | end 21 | 22 | module Slack_channel : sig 23 | type 'kind t 24 | 25 | val equal : 'kind t -> 'kind t -> bool 26 | val compare : 'kind t -> 'kind t -> int 27 | val hash : 'kind t -> int 28 | 29 | module Ident : sig 30 | type nonrec t = [ `Id ] t 31 | val inject : string -> t 32 | val project : t -> string 33 | end 34 | module Name : sig 35 | type nonrec t = [ `Name ] t 36 | val inject : string -> t 37 | val project : t -> string 38 | end 39 | module Any : sig 40 | type nonrec t = [ `Any ] t 41 | val inject : string -> t 42 | val project : t -> string 43 | end 44 | 45 | val to_any : 'kind t -> Any.t 46 | end = struct 47 | type 'kind t = string 48 | 49 | let equal = String.equal 50 | let compare = String.compare 51 | let hash = Hashtbl.hash 52 | let to_any = id 53 | 54 | module Ident = struct 55 | type nonrec t = [ `Id ] t 56 | let inject = id 57 | let project = id 58 | end 59 | module Name = struct 60 | type nonrec t = [ `Name ] t 61 | let inject = id 62 | let project = id 63 | end 64 | module Any = struct 65 | type nonrec t = [ `Any ] t 66 | let inject = id 67 | let project = id 68 | end 69 | end 70 | 71 | module Slack_user_id = struct 72 | include Fresh (String) () 73 | 74 | let to_channel_id = Slack_channel.Any.inject $ project 75 | end 76 | 77 | module Set (S : Set.OrderedType) = struct 78 | include Set.Make (S) 79 | 80 | let wrap = of_list 81 | let unwrap = elements 82 | end 83 | 84 | module StringSet = Set (String) 85 | module FailedStepSet = Set (struct 86 | type t = Buildkite_t.failed_step 87 | let compare (t1 : t) (t2 : t) = String.compare t1.id t2.id 88 | end) 89 | 90 | module Status_notification = struct 91 | type t = 92 | | Channel of Slack_channel.Any.t 93 | | User of Slack_user_id.t 94 | 95 | let inject_channel c = Channel (Slack_channel.to_any c) 96 | 97 | let to_slack_channel = function 98 | | Channel c -> c 99 | | User u -> Slack_user_id.to_channel_id u 100 | 101 | let is_user = function 102 | | User _ -> true 103 | | Channel _ -> false 104 | 105 | let compare a b = Slack_channel.compare (to_slack_channel a) (to_slack_channel b) 106 | let equal a b = compare a b = 0 107 | end 108 | 109 | module Map (S : Map.OrderedType) = struct 110 | include Map.Make (S) 111 | 112 | let to_list (l : 'a t) : (S.t * 'a) list = to_seq l |> List.of_seq 113 | let of_list (m : (S.t * 'a) list) : 'a t = List.to_seq m |> of_seq 114 | let wrap = of_list 115 | let unwrap = to_list 116 | 117 | let of_list_multi (m : (S.t * 'a) list) : 'a list t = 118 | let update_f v = function 119 | | None -> Some [ v ] 120 | | Some vs -> Some (v :: vs) 121 | in 122 | List.fold_right (fun (k, v) b -> update k (update_f v) b) m empty 123 | end 124 | 125 | module StringMap = struct 126 | include Map (String) 127 | 128 | (** [update_async key f map] updates the value of [key] in [map] using an async function [f]. 129 | Async equivalent of the [update] function. *) 130 | let update_async key f map = 131 | let current_value = find_opt key map in 132 | match%lwt f current_value with 133 | | None -> Lwt.return (remove key map) 134 | | Some v -> Lwt.return (add key v map) 135 | end 136 | 137 | module IntMap = Map (Int) 138 | 139 | module IntMapJson = struct 140 | type 'a t = 'a IntMap.t 141 | 142 | let to_list (m : 'a t) : (string * 'a) list = 143 | IntMap.to_seq m |> Seq.map (fun (k, v) -> string_of_int k, v) |> List.of_seq 144 | 145 | let of_list (l : (string * 'a) list) : 'a t = 146 | List.to_seq l |> Seq.map (fun (k, v) -> int_of_string k, v) |> IntMap.of_seq 147 | 148 | let wrap = of_list 149 | let unwrap = to_list 150 | end 151 | 152 | module ChannelMap = Map (struct 153 | include Slack_channel.Any 154 | let compare = Slack_channel.compare 155 | end) 156 | 157 | module Stringtbl = struct 158 | include Hashtbl 159 | 160 | type 'a t = (string, 'a) Hashtbl.t 161 | 162 | let empty () = Hashtbl.create 1 163 | let to_list (l : 'a t) : (string * 'a) list = Hashtbl.to_seq l |> List.of_seq 164 | let of_list (m : (string * 'a) list) : 'a t = List.to_seq m |> Hashtbl.of_seq 165 | let wrap = of_list 166 | let unwrap = to_list 167 | end 168 | 169 | module Re2 = struct 170 | include Re2 171 | 172 | let wrap s = create_exn s 173 | let unwrap = Re2.to_string 174 | end 175 | -------------------------------------------------------------------------------- /lib/config.atd: -------------------------------------------------------------------------------- 1 | type status_rule = abstract 2 | type prefix_rule = abstract 3 | type label_rule = abstract 4 | type project_owners_rule = abstract 5 | type any_channel = string wrap 6 | type channel_name = string wrap 7 | 8 | type pipeline = { 9 | name: string; 10 | ?failed_builds_channel: channel_name nullable; 11 | ~notify_canceled_builds : bool; 12 | ~mention_user_on_failed_builds : bool; 13 | ~escalate_notifications : bool; 14 | (* threshold in hours *) 15 | ~escalate_notification_threshold : int; 16 | ~dm_users_on_failures : bool; 17 | } 18 | 19 | (* This type of rule is used for CI build notifications. *) 20 | type status_rules = { 21 | ?allowed_pipelines : pipeline list nullable; (* keep only status events with a title matching this list *) 22 | rules: status_rule list; 23 | } 24 | 25 | (* This type of rule is used for events that must be routed based on the 26 | files they are related to. *) 27 | type prefix_rules = { 28 | ?default_channel: channel_name nullable; (* if none of the rules is matching *) 29 | ~filter_main_branch : bool; 30 | rules: prefix_rule list; 31 | } 32 | 33 | (* This type of rule is used for PR and issue notifications. *) 34 | type label_rules = { 35 | ?default_channel: channel_name nullable; (* if none of the rules is matching *) 36 | rules: label_rule list; 37 | } 38 | 39 | (* This type of rule is used for routing PR review requests to users. *) 40 | type project_owners = { 41 | rules: project_owners_rule list; 42 | } 43 | 44 | type notifications_configs = { 45 | ~dm_for_failing_build : (string * bool) list ; 46 | ~dm_after_failed_build : (string * bool) list 47 | } 48 | 49 | (* This is the structure of the repository configuration file. It should be at the 50 | root of the monorepo, on the main branch. *) 51 | type config = { 52 | prefix_rules : prefix_rules; 53 | label_rules : label_rules; 54 | ~status_rules : status_rules; 55 | ~project_owners : project_owners; 56 | ~ignored_users : string list; (* list of ignored users *) 57 | ?main_branch_name : string nullable; (* the name of the main branch; used to filter out notifications about merges of main branch into other branches *) 58 | ~user_mappings : (string * string) list ; (* list of github to slack profile mappings *) 59 | ~notifications_configs : notifications_configs; 60 | ~include_logs_in_notifs : bool; 61 | ~debug_db : bool; 62 | ?debug_db_path : string nullable; 63 | } 64 | 65 | (* This specifies the Slack webhook to query to post to the channel with the given name *) 66 | type webhook = { 67 | url : string; (* webhook URL to post the Slack message *) 68 | channel : any_channel; (* name of the Slack channel to post the message *) 69 | } 70 | 71 | type app_installation_cfg = { 72 | installation_id: string; 73 | client_id: string; 74 | pem: string; 75 | } 76 | 77 | type repo_auth = [ GH_token of string | AppInstallation of app_installation_cfg ] 78 | 79 | type repo_config = { 80 | (* Repository url. Fully qualified (include protocol), without trailing slash. e.g. https://github.com/ahrefs/monorobot *) 81 | url : string; 82 | (* GitHub personal access token, if repo access requires it *) 83 | ?auth : repo_auth nullable; 84 | (* GitHub webhook secret token to secure the webhook *) 85 | ?gh_hook_secret : string nullable; 86 | } 87 | 88 | (* This is the structure of the secrets file which stores sensitive information, and 89 | shouldn't be checked into version control. *) 90 | type secrets = { 91 | (* repo-specific secrets; overrides global values if defined for a given repo *) 92 | repos : repo_config list; 93 | (* list of Slack webhook & channel name pairs *) 94 | ~slack_hooks : webhook list; 95 | (* Slack bot token (`xoxb-XXXX`), giving the bot capabilities to interact with the workspace *) 96 | ?slack_access_token : string nullable; 97 | (* Slack uses this secret to sign requests; provide to verify incoming Slack requests *) 98 | ?slack_signing_secret : string nullable; 99 | (* Buildkite access token, used to query Buildkite API for more information about builds *) 100 | ?buildkite_access_token : string nullable; 101 | (* Buildkite uses this secret to sign webhook requests; provide to verify incoming Buildkite requests *) 102 | ?buildkite_signing_secret : string nullable; 103 | } 104 | -------------------------------------------------------------------------------- /test/github-api-cache/2129f69a03641c39dfeb8e928cc08d7a8d029c6b: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "2129f69a03641c39dfeb8e928cc08d7a8d029c6b", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "yasunariw", 7 | "email": "mail@example.org", 8 | "date": "2020-06-26T03:40:28Z" 9 | }, 10 | "committer": { 11 | "name": "yasunariw", 12 | "email": "mail@example.org", 13 | "date": "2020-06-26T03:40:28Z" 14 | }, 15 | "message": "Merge branch 'develop' into feature/f2", 16 | "tree": { 17 | "sha": "c0357c3f40aae6bbc57aac50fd4a990873cf11e7", 18 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/c0357c3f40aae6bbc57aac50fd4a990873cf11e7" 19 | }, 20 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/2129f69a03641c39dfeb8e928cc08d7a8d029c6b", 21 | "comment_count": 0, 22 | "verification": { 23 | "verified": true, 24 | "reason": "valid", 25 | "signature": "", 26 | "payload": "tree c0357c3f40aae6bbc57aac50fd4a990873cf11e7\nparent ea432dabb762ea76bddcfc04aca6af20df9cb68b\nparent f8fd6e2f4198546182fd7b7e5498fe3c07fc272b\nauthor yasunariw 1593142828 +0800\ncommitter yasunariw 1593142828 +0800\n\nMerge branch 'develop' into feature/f2\n" 27 | } 28 | }, 29 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/2129f69a03641c39dfeb8e928cc08d7a8d029c6b", 30 | "html_url": "https://github.com/ahrefs/monorepo/commit/2129f69a03641c39dfeb8e928cc08d7a8d029c6b", 31 | "comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/2129f69a03641c39dfeb8e928cc08d7a8d029c6b/comments", 32 | "author": { 33 | "login": "yasunariw", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://github.com/avatars/u/0", 37 | "gravatar_id": "", 38 | "url": "https://github.com/api/v3/users/yasunariw", 39 | "html_url": "https://github.com/yasunariw", 40 | "followers_url": "https://github.com/api/v3/users/yasunariw/followers", 41 | "following_url": "https://github.com/api/v3/users/yasunariw/following{/other_user}", 42 | "gists_url": "https://github.com/api/v3/users/yasunariw/gists{/gist_id}", 43 | "starred_url": "https://github.com/api/v3/users/yasunariw/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://github.com/api/v3/users/yasunariw/subscriptions", 45 | "organizations_url": "https://github.com/api/v3/users/yasunariw/orgs", 46 | "repos_url": "https://github.com/api/v3/users/yasunariw/repos", 47 | "events_url": "https://github.com/api/v3/users/yasunariw/events{/privacy}", 48 | "received_events_url": "https://github.com/api/v3/users/yasunariw/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": { 53 | "login": "yasunariw", 54 | "id": 0, 55 | "node_id": "00000000000000000000", 56 | "avatar_url": "https://github.com/avatars/u/0", 57 | "gravatar_id": "", 58 | "url": "https://github.com/api/v3/users/yasunariw", 59 | "html_url": "https://github.com/yasunariw", 60 | "followers_url": "https://github.com/api/v3/users/yasunariw/followers", 61 | "following_url": "https://github.com/api/v3/users/yasunariw/following{/other_user}", 62 | "gists_url": "https://github.com/api/v3/users/yasunariw/gists{/gist_id}", 63 | "starred_url": "https://github.com/api/v3/users/yasunariw/starred{/owner}{/repo}", 64 | "subscriptions_url": "https://github.com/api/v3/users/yasunariw/subscriptions", 65 | "organizations_url": "https://github.com/api/v3/users/yasunariw/orgs", 66 | "repos_url": "https://github.com/api/v3/users/yasunariw/repos", 67 | "events_url": "https://github.com/api/v3/users/yasunariw/events{/privacy}", 68 | "received_events_url": "https://github.com/api/v3/users/yasunariw/received_events", 69 | "type": "User", 70 | "site_admin": false 71 | }, 72 | "parents": [ 73 | { 74 | "sha": "ea432dabb762ea76bddcfc04aca6af20df9cb68b", 75 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/ea432dabb762ea76bddcfc04aca6af20df9cb68b", 76 | "html_url": "https://github.com/ahrefs/monorepo/commit/ea432dabb762ea76bddcfc04aca6af20df9cb68b" 77 | }, 78 | { 79 | "sha": "f8fd6e2f4198546182fd7b7e5498fe3c07fc272b", 80 | "url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/f8fd6e2f4198546182fd7b7e5498fe3c07fc272b", 81 | "html_url": "https://github.com/ahrefs/monorepo/commit/f8fd6e2f4198546182fd7b7e5498fe3c07fc272b" 82 | } 83 | ], 84 | "stats": { 85 | "total": 876, 86 | "additions": 458, 87 | "deletions": 418 88 | }, 89 | "files": [ 90 | { 91 | "sha": "68af1176d348eb5f999dbfcd6354b31c3920b939", 92 | "filename": "placeholder", 93 | "status": "modified", 94 | "additions": 1, 95 | "deletions": 1, 96 | "changes": 2, 97 | "blob_url": "https://github.com/ahrefs/monorepo/blob/2129f69a03641c39dfeb8e928cc08d7a8d029c6b/.script.sh", 98 | "raw_url": "https://github.com/ahrefs/monorepo/raw/2129f69a03641c39dfeb8e928cc08d7a8d029c6b/.script.sh", 99 | "contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/.script.sh?ref=2129f69a03641c39dfeb8e928cc08d7a8d029c6b", 100 | "patch": "" 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /test/github-api-cache/xinyuluo_monorepo_commit_41dad1d3d41f329f00836f166a7103a262e69889: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "41dad1d3d41f329f00836f166a7103a262e69889", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "Xinyu Luo", 7 | "email": "mail@example.org", 8 | "date": "2020-06-08T09:55:09Z" 9 | }, 10 | "committer": { 11 | "name": "Xinyu Luo", 12 | "email": "mail@example.org", 13 | "date": "2020-06-08T09:55:09Z" 14 | }, 15 | "message": "make changes in 2 files", 16 | "tree": { 17 | "sha": "22393b31025471050fe5ebffe1ca92963416cb9f", 18 | "url": "https://api.github.com/repos/xinyuluo/monorepo/git/trees/22393b31025471050fe5ebffe1ca92963416cb9f" 19 | }, 20 | "url": "https://api.github.com/repos/xinyuluo/monorepo/git/commits/41dad1d3d41f329f00836f166a7103a262e69889", 21 | "comment_count": 1, 22 | "verification": { 23 | "verified": false, 24 | "reason": "unsigned", 25 | "signature": null, 26 | "payload": null 27 | } 28 | }, 29 | "url": "https://api.github.com/repos/xinyuluo/monorepo/commits/41dad1d3d41f329f00836f166a7103a262e69889", 30 | "html_url": "https://github.com/xinyuluo/monorepo/commit/41dad1d3d41f329f00836f166a7103a262e69889", 31 | "comments_url": "https://api.github.com/repos/xinyuluo/monorepo/commits/41dad1d3d41f329f00836f166a7103a262e69889/comments", 32 | "author": { 33 | "login": "xinyuluo", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://avatars0.githubusercontent.com/u/000000?v=4", 37 | "gravatar_id": "", 38 | "url": "https://api.github.com/users/xinyuluo", 39 | "html_url": "https://github.com/xinyuluo", 40 | "followers_url": "https://api.github.com/users/xinyuluo/followers", 41 | "following_url": "https://api.github.com/users/xinyuluo/following{/other_user}", 42 | "gists_url": "https://api.github.com/users/xinyuluo/gists{/gist_id}", 43 | "starred_url": "https://api.github.com/users/xinyuluo/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://api.github.com/users/xinyuluo/subscriptions", 45 | "organizations_url": "https://api.github.com/users/xinyuluo/orgs", 46 | "repos_url": "https://api.github.com/users/xinyuluo/repos", 47 | "events_url": "https://api.github.com/users/xinyuluo/events{/privacy}", 48 | "received_events_url": "https://api.github.com/users/xinyuluo/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": { 53 | "login": "xinyuluo", 54 | "id": 0, 55 | "node_id": "00000000000000000000", 56 | "avatar_url": "https://avatars0.githubusercontent.com/u/000000?v=4", 57 | "gravatar_id": "", 58 | "url": "https://api.github.com/users/xinyuluo", 59 | "html_url": "https://github.com/xinyuluo", 60 | "followers_url": "https://api.github.com/users/xinyuluo/followers", 61 | "following_url": "https://api.github.com/users/xinyuluo/following{/other_user}", 62 | "gists_url": "https://api.github.com/users/xinyuluo/gists{/gist_id}", 63 | "starred_url": "https://api.github.com/users/xinyuluo/starred{/owner}{/repo}", 64 | "subscriptions_url": "https://api.github.com/users/xinyuluo/subscriptions", 65 | "organizations_url": "https://api.github.com/users/xinyuluo/orgs", 66 | "repos_url": "https://api.github.com/users/xinyuluo/repos", 67 | "events_url": "https://api.github.com/users/xinyuluo/events{/privacy}", 68 | "received_events_url": "https://api.github.com/users/xinyuluo/received_events", 69 | "type": "User", 70 | "site_admin": false 71 | }, 72 | "parents": [ 73 | { 74 | "sha": "a5dcbfdafb3d4f5ed23fdc27d04a451ebca650a3", 75 | "url": "https://api.github.com/repos/xinyuluo/monorepo/commits/a5dcbfdafb3d4f5ed23fdc27d04a451ebca650a3", 76 | "html_url": "https://github.com/xinyuluo/monorepo/commit/a5dcbfdafb3d4f5ed23fdc27d04a451ebca650a3" 77 | } 78 | ], 79 | "stats": { 80 | "total": 39, 81 | "additions": 18, 82 | "deletions": 21 83 | }, 84 | "files": [ 85 | { 86 | "sha": "29ea67e12ffe6b54e6d751138a6aa407f0dc9a29", 87 | "filename": "README.md", 88 | "status": "modified", 89 | "additions": 1, 90 | "deletions": 0, 91 | "changes": 1, 92 | "blob_url": "https://github.com/xinyuluo/monorepo/blob/41dad1d3d41f329f00836f166a7103a262e69889/README.md", 93 | "raw_url": "https://github.com/xinyuluo/monorepo/raw/41dad1d3d41f329f00836f166a7103a262e69889/README.md", 94 | "contents_url": "https://api.github.com/repos/xinyuluo/monorepo/contents/README.md?ref=41dad1d3d41f329f00836f166a7103a262e69889", 95 | "patch": "" 96 | }, 97 | { 98 | "sha": "103b78e0749ce835869239a7840d21bb14c92bcb", 99 | "filename": "main.ml", 100 | "status": "modified", 101 | "additions": 17, 102 | "deletions": 21, 103 | "changes": 38, 104 | "blob_url": "https://github.com/xinyuluo/monorepo/blob/41dad1d3d41f329f00836f166a7103a262e69889/main.ml", 105 | "raw_url": "https://github.com/xinyuluo/monorepo/raw/41dad1d3d41f329f00836f166a7103a262e69889/main.ml", 106 | "contents_url": "https://api.github.com/repos/xinyuluo/monorepo/contents/main.ml?ref=41dad1d3d41f329f00836f166a7103a262e69889", 107 | "patch": "" 108 | } 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /test/test.ml: -------------------------------------------------------------------------------- 1 | open Devkit 2 | open Monorobotlib 3 | 4 | let log = Log.from "test" 5 | let mock_payload_dir = Filename.concat Filename.parent_dir_name "mock_payloads" 6 | let mock_state_dir = Filename.concat Filename.parent_dir_name "mock_states" 7 | let mock_slack_event_dir = Filename.concat Filename.parent_dir_name "mock_slack_events" 8 | 9 | let () = 10 | (* silence most app level logging *) 11 | Log.set_filter `Error; 12 | Log.set_filter ~name:"test" `Info 13 | 14 | module Action_local = Action.Action (Api_local.Github) (Api_local.Slack) (Api_local.Buildkite) 15 | 16 | let get_sorted_files_from dir = 17 | let files = Sys.readdir dir in 18 | Array.sort String.compare files; 19 | Array.to_list files 20 | 21 | let get_mock_payloads () = 22 | get_sorted_files_from mock_payload_dir 23 | |> List.filter_map (fun fn -> Github.event_of_filename fn |> Option.map (fun kind -> kind, fn)) 24 | |> List.map (fun (kind, fn) -> 25 | let payload_path = Filename.concat mock_payload_dir fn in 26 | let state_path = Filename.concat mock_state_dir fn in 27 | if Sys.file_exists state_path then kind, payload_path, Some state_path else kind, payload_path, None) 28 | 29 | let get_mock_slack_events () = 30 | List.map (Filename.concat mock_slack_event_dir) (get_sorted_files_from mock_slack_event_dir) 31 | 32 | let process_gh_payload ~(secrets : Config_t.secrets) ~config (kind, path, state_path) = 33 | let headers = [ "x-github-event", kind ] in 34 | let make_test_context event = 35 | let ctx = Context.make () in 36 | let get_build_branch = Api_local.Buildkite.get_build_branch ~ctx in 37 | let%lwt n = Github.parse_exn headers event ~get_build_branch in 38 | let repo = Github.repo_of_notification n in 39 | (* overwrite repo url in secrets with that of notification for this test case *) 40 | let secrets = { secrets with repos = [ { url = repo.url; auth = None; gh_hook_secret = None } ] } in 41 | ctx.secrets <- Some secrets; 42 | let (_ : State_t.repo_state) = State.find_or_add_repo ctx.state repo.url in 43 | let () = 44 | match state_path with 45 | | None -> Context.set_repo_config ctx repo.url config 46 | | Some state_path -> 47 | match State_j.repo_state_of_string (Std.input_file state_path) with 48 | | repo_state -> 49 | State.set_repo_state ctx.state repo.url repo_state; 50 | Context.set_repo_config ctx repo.url config 51 | | exception exn -> log#error ~exn "failed to load state from file %s" state_path 52 | in 53 | Lwt.return ctx 54 | in 55 | Printf.printf "===== file %s =====\n" path; 56 | let headers = [ "x-github-event", kind ] in 57 | match Std.input_file path with 58 | | event -> 59 | let%lwt ctx = make_test_context event in 60 | let%lwt () = Action_local.process_github_notification ctx headers event in 61 | (match Sys.getenv_opt "PRINT_TEST_STATE" with 62 | | Some s when s = "true" || Stre.exists path s -> 63 | let repo_url = 64 | let json = Yojson.Basic.from_string event in 65 | Yojson.Basic.Util.member "repository" json |> Yojson.Basic.Util.member "html_url" |> Yojson.Basic.Util.to_string 66 | in 67 | let repo_state = State.find_or_add_repo ctx.state repo_url in 68 | Printf.printf "-------------- State ------------\n %s\n-------------- State ------------\n" 69 | (Yojson.Basic.pretty_to_string (State_j.string_of_repo_state repo_state |> Yojson.Basic.from_string)) 70 | | _ -> ()); 71 | Lwt.return_unit 72 | | exception exn -> 73 | log#error ~exn "failed to read file %s" path; 74 | Lwt.return_unit 75 | 76 | let process_slack_event ~(secrets : Config_t.secrets) path = 77 | let ctx = Context.make () in 78 | ctx.secrets <- Some secrets; 79 | State.set_bot_user_id ctx.state (Common.Slack_user_id.inject "bot_user"); 80 | Printf.printf "===== file %s =====\n" path; 81 | match Slack_j.event_notification_of_string (Std.input_file path) with 82 | | exception exn -> 83 | log#error ~exn "failed to read event notification from file %s" path; 84 | Lwt.return_unit 85 | | Url_verification _ -> Lwt.return () 86 | | Event_callback notification -> 87 | match notification.event with 88 | | Link_shared event -> 89 | let%lwt _ctx = Action_local.process_link_shared_event ctx event in 90 | Lwt.return_unit 91 | 92 | let () = 93 | let payloads = get_mock_payloads () in 94 | let repo : Github_t.repository = 95 | { 96 | name = ""; 97 | full_name = ""; 98 | url = ""; 99 | commits_url = ""; 100 | contents_url = ""; 101 | pulls_url = ""; 102 | issues_url = ""; 103 | compare_url = ""; 104 | } 105 | in 106 | let ctx = Context.make ~state_filepath:"state.json" () in 107 | let slack_events = get_mock_slack_events () in 108 | Lwt_main.run 109 | (match%lwt Api_local.Github.get_config ~ctx ~repo with 110 | | Error e -> 111 | log#error "%s" e; 112 | Lwt.return_unit 113 | | Ok config -> 114 | match Context.refresh_secrets ctx with 115 | | Ok ctx -> 116 | let%lwt () = Action_local.refresh_username_to_slack_id_tbl ~ctx in 117 | let%lwt () = Lwt_list.iter_s (process_gh_payload ~secrets:(Option.get ctx.secrets) ~config) payloads in 118 | let%lwt () = Lwt_list.iter_s (process_slack_event ~secrets:(Option.get ctx.secrets)) slack_events in 119 | Lwt.return_unit 120 | | Error e -> 121 | log#error "failed to read secrets:"; 122 | log#error "%s" e; 123 | Lwt.return_unit) 124 | -------------------------------------------------------------------------------- /documentation/secret_docs.md: -------------------------------------------------------------------------------- 1 | # Secrets 2 | 3 | A secrets file stores sensitive information. Unlike the repository configuration file, it should not be checked into the monorepo's version control. Instead, store it locally at a location accessible by the bot. 4 | 5 | # Options 6 | 7 | **Example** 8 | 9 | ```json 10 | { 11 | "repos": [ 12 | { 13 | "url": "https://github.com/ahrefs/monorobot", 14 | "gh_token": "XXX" 15 | } 16 | ], 17 | "slack_access_token": "XXX" 18 | } 19 | ``` 20 | 21 | | value | description | optional | default | 22 | |-|-|-|-| 23 | | `repos` | specify each target repository's url and its secrets | No | - | 24 | | `slack_access_token` | slack bot access token to enable message posting to the workspace | Yes | try to use webhooks defined in `slack_hooks` instead | 25 | | `slack_hooks` | list of channel names and their corresponding webhook endpoint | Yes | try to use token defined in `slack_access_token` instead | 26 | | `slack_signing_secret` | specify to verify incoming slack requests | Yes | - | 27 | | `buildkite_access_token` | Buildkite access token, used to query the Buildkite API for builds details | Yes | - | 28 | | `buildkite_signing_secret` | specify to verify incoming Buildkite webhook requests | Yes | - | 29 | 30 | Note that: 31 | - either `slack_access_token` or `slack_hooks` must be defined. If both are present, the bot will send notifications using webhooks. 32 | - the failed builds notifications require the `buildkite_access_token` to work 33 | 34 | ## `repos` 35 | 36 | Specifies which repositories to accept events from, along with any repository-specific overrides to secrets. 37 | 38 | ```json 39 | [ 40 | { 41 | "url": "https://github.com/ahrefs/runner", 42 | "gh_token": "XXX" 43 | }, 44 | { 45 | "url": "https://example.org/ahrefs/coyote", 46 | "gh_token": "XXX", 47 | "gh_hook_secret": "XXX" 48 | } 49 | ] 50 | ``` 51 | 52 | | value | description | optional | default | 53 | |-|-|-|-| 54 | | `url` | the repository url. | No | - | 55 | | `gh_token` | specify to grant the bot access to private repositories; omit for public repositories | Yes | - | 56 | | `gh_hook_secret` | shared secret token to authenticate the GitHub repository sending a notification | Yes | - | 57 | 58 | ### `repos` 59 | 60 | Repository URLs should be fully qualified (include the protocol), with no trailing backslash. 61 | 62 | ### `gh_token` 63 | 64 | Some operations, such as fetching a config file from a private repository, or the commit corresponding to a commit comment event, require a personal access token. Refer [here](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token) for detailed instructions on token generation. 65 | 66 | ### `gh_hook_secret` 67 | 68 | Refer [here](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/securing-your-webhooks) for more information on securing webhooks with a secret token. 69 | 70 | ## `slack_access_token` 71 | 72 | Required for: 73 | - Notification sending via Web API 74 | - Link unfurling 75 | 76 | You can obtain a bot token from the "OAuth & Permissions" in your app dashboard's sidebar. Note that you need a *bot* token (`xoxb-XXXX`), not a *user* token (`xoxp-XXXX`). 77 | See [here](https://api.slack.com/authentication/basics#start) for creating/installing an app and requesting scopes. 78 | 79 | Give it the following scopes: 80 | - For notifications - [`chat:write`](https://api.slack.com/scopes/chat:write) (per-channel authorization) or [`chat:write.public`](https://api.slack.com/scopes/chat:write.public) (authorization to all channels) 81 | - Note: If you use the `chat:write` scope, add the bot to each channel you want to notify. 82 | - For link unfurling - [`links:read`](https://api.slack.com/scopes/links:read) and [`links:write`](https://api.slack.com/scopes/links:write) (also see **Link Unfurling** in main README) 83 | 84 | ## `slack_hooks` 85 | 86 | Required for: 87 | - Notification sending via webhooks 88 | 89 | Expected format: 90 | 91 | ```json 92 | [ 93 | { 94 | "channel": "channel name", 95 | "url": "webhook url" 96 | }, 97 | { 98 | "channel": "channel name", 99 | "url": "webhook url" 100 | }, 101 | ... 102 | ] 103 | ``` 104 | 105 | Refer [here](https://api.slack.com/messaging/webhooks) for obtaining a webhook for a channel. 106 | 107 | ## `buildkite_access_token` 108 | This token is used to get informations regarding builds details and components. It's required to use the failed builds notifications feature. 109 | 110 | You also need to have one webhook configured on your pipelines with the `build.finished` scope. Configure the `buildkite_signing_secret` setting to validate the payloads if you require it. 111 | 112 | To manage your API access tokens, visit your [personal settings page](https://buildkite.com/user/api-access-tokens) where you can create or edit them. 113 | 114 | Admins can go to the [API Access Audit](https://buildkite.com/organizations/%7E/api-access-audit) page to review all tokens with access to the organization's data. You can also check each token's permissions and remove access if necessary. 115 | 116 | ## `buildkite_signing_secret` 117 | This secret is used to [validate the payloads that Buildkite sends on the webhook](https://buildkite.com/docs/apis/webhooks#webhook-signature). 118 | 119 | Webhooks can be added and configured on your organization's [Notification Services settings page](https://buildkite.com/organizations/-/services). 120 | -------------------------------------------------------------------------------- /lib/slack.atd: -------------------------------------------------------------------------------- 1 | type 'v map_as_object = abstract 2 | type timestamp = string wrap 3 | type user_id = string wrap 4 | type channel_id = string wrap 5 | type any_channel = string wrap 6 | 7 | type message_field = { 8 | ?title: string nullable; 9 | value: string; 10 | ~short : bool; 11 | } 12 | 13 | type message_attachment = { 14 | fallback: string nullable; 15 | ?mrkdwn_in: string list nullable; 16 | ?color: string nullable; 17 | ?pretext: string nullable; 18 | ?author_name: string nullable; 19 | ?author_link: string nullable; 20 | ?author_icon: string nullable; 21 | ?title: string nullable; 22 | ?title_link: string nullable; 23 | ?text: string nullable; 24 | ?fields: message_field list nullable; 25 | ?image_url: string nullable; 26 | ?thumb_url: string nullable; 27 | ?ts: timestamp nullable; 28 | ?footer: string nullable; 29 | } 30 | 31 | type message_section_block_type = [ 32 | Section 33 | ] 34 | 35 | type message_divider_block_type = [ 36 | Divider 37 | ] 38 | 39 | type text_object_type = [ 40 | Plain_text 41 | | Markdown 42 | ] 43 | 44 | type text_object = { 45 | text_type : text_object_type; 46 | text: string; 47 | } 48 | 49 | type message_text_block = { 50 | message_type : message_section_block_type; 51 | text: text_object; 52 | } 53 | 54 | type message_divider_block = { 55 | message_type : message_divider_block_type; 56 | } 57 | 58 | type message_block = [ 59 | Text of message_text_block 60 | | Divider of message_divider_block 61 | ] 62 | 63 | type post_message_req = { 64 | channel: any_channel; 65 | ?thread_ts: timestamp nullable; 66 | ?username : string nullable; 67 | ?text: string nullable; 68 | ?attachments: message_attachment list nullable; 69 | ?blocks: message_block list nullable; 70 | ?unfurl_media : bool nullable; 71 | ?unfurl_links : bool nullable; 72 | ~reply_broadcast : bool; 73 | } 74 | 75 | type post_message_res = { 76 | channel: channel_id; 77 | ts: timestamp; 78 | } 79 | 80 | type lookup_user_res = { 81 | user: user; 82 | } 83 | 84 | type profile = { 85 | ?email: string nullable 86 | } 87 | 88 | type user = { 89 | id: user_id; 90 | profile: profile 91 | } 92 | 93 | type list_users_res = { 94 | members: user list; 95 | } 96 | 97 | type link_shared_link = { 98 | domain: string; 99 | url: string; 100 | } 101 | 102 | type link_shared_event = { 103 | channel: channel_id; 104 | is_bot_user_member: bool; 105 | user: user_id; 106 | message_ts: timestamp; 107 | ?thread_ts: timestamp option; 108 | links: link_shared_link list; 109 | } 110 | 111 | type event = [ 112 | | Link_shared of link_shared_event 113 | ] 114 | 115 | type event_callback_notification = { 116 | token: string; 117 | team_id: string; 118 | api_app_id: string; 119 | event: event; 120 | event_id: string; 121 | event_time: int; 122 | } 123 | 124 | type url_verification_notification = { 125 | token: string; 126 | challenge: string; 127 | } 128 | 129 | type event_notification = [ 130 | | Event_callback of event_callback_notification 131 | | Url_verification of url_verification_notification 132 | ] 133 | 134 | type unfurl = message_attachment 135 | 136 | type chat_unfurl_req = { 137 | channel: channel_id; 138 | ts: timestamp; 139 | unfurls: unfurl map_as_object; 140 | } 141 | 142 | type ok_res = { 143 | ok: bool; 144 | } 145 | 146 | type auth_test_res = { 147 | url: string; 148 | team: string; 149 | user: string; 150 | team_id: string; 151 | user_id: user_id; 152 | } 153 | 154 | type permalink_res = { 155 | ok: bool; 156 | permalink: string; 157 | channel: string; 158 | ?error: string nullable; 159 | } 160 | 161 | type ('ok, 'err) http_response = [ 162 | | Ok of 'ok 163 | | Error of 'err 164 | ] 165 | 166 | type 'ok slack_response = ('ok, string) http_response 167 | 168 | 169 | type upload_url_res = { 170 | upload_url: string; 171 | file_id: string; 172 | } 173 | 174 | type file = { 175 | id: string; 176 | ?title: string nullable; 177 | } 178 | 179 | type files = file list 180 | 181 | type complete_upload_external_req = { 182 | files: files; 183 | ?channel_id: channel_id nullable; 184 | ?thread_ts: timestamp nullable; 185 | ?initial_comment: string nullable; 186 | } 187 | 188 | type complete_upload_external_res = { 189 | files: files; 190 | } 191 | 192 | type join_channel_req = { 193 | channel: channel_id; 194 | } 195 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorobot_branch_master: -------------------------------------------------------------------------------- 1 | { 2 | "name": "master", 3 | "commit": { 4 | "sha": "6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 5 | "node_id": "C_kwDOC5saPNoAKDZlYzZiNDdkMWYzMzNkMzBkNWMzODg5ZTA1ZGNhYWQwNzhmMGU3N2Q", 6 | "commit": { 7 | "author": { 8 | "name": "Louis", 9 | "email": "mail@example.org", 10 | "date": "2022-10-26T00:47:59Z" 11 | }, 12 | "committer": { 13 | "name": "GitHub", 14 | "email": "noreply@github.com", 15 | "date": "2022-10-26T00:47:59Z" 16 | }, 17 | "message": "Merge pull request #126 from sewenthy/sewen/121-ignore-code-comments-from-defined-list-of-users\n\nIgnore code comments from defined list of users", 18 | "tree": { 19 | "sha": "3306d7ce0d2ac3324755dbe410d3fa3da841fe7f", 20 | "url": "https://api.github.com/repos/ahrefs/monorobot/git/trees/3306d7ce0d2ac3324755dbe410d3fa3da841fe7f" 21 | }, 22 | "url": "https://api.github.com/repos/ahrefs/monorobot/git/commits/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 23 | "comment_count": 0, 24 | "verification": { 25 | "verified": true, 26 | "reason": "valid", 27 | "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJjWIO/CRBK7hj4Ov3rIwAAEWAIAI7P6ReUnb6bqigQ06Iqgzrm\nK16ReYELdE6Gp5lv24pw4fEa8vW7u5M8Rp4FndfpcnwjODyw9BRN1xzmJJ6sbNgZ\nusB12KyN6Q7MPAmRcoleKzndfcTpoAGR1chuVeDuMT6VcSjHdCbkNO8nhbXnhjPY\nDncx6uXrm8t+ak8IcbJ6nbxFvDPlqySGK7GvbsNIFhSdIy5FkvFg8kVYXssnD0mn\nhfGWCyKBfbkIAIR/w2Rdb6xXm6E2zjMFZp6BDRNhq0hv3R8/hj7GJdw6I+WwYq39\ntUIgPyYtYJpB8NK5DuTqfK7bIEiz3tXkXJ84iyx8Wscpldq60GYA0r0c2QBb7io=\n=yv18\n-----END PGP SIGNATURE-----\n", 28 | "payload": "tree 3306d7ce0d2ac3324755dbe410d3fa3da841fe7f\nparent 56ada1084d6b5cc71821f65cf41f013fe6c280f3\nparent 883724426505ec03e29a1e4fd11079797a30bd57\nauthor Louis 1666745279 +0800\ncommitter GitHub 1666745279 +0800\n\nMerge pull request #126 from sewenthy/sewen/121-ignore-code-comments-from-defined-list-of-users\n\nIgnore code comments from defined list of users" 29 | } 30 | }, 31 | "url": "https://api.github.com/repos/ahrefs/monorobot/commits/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 32 | "html_url": "https://github.com/ahrefs/monorobot/commit/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 33 | "comments_url": "https://api.github.com/repos/ahrefs/monorobot/commits/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d/comments", 34 | "author": { 35 | "login": "Khady", 36 | "id": 974142, 37 | "node_id": "MDQ6VXNlcjk3NDE0Mg==", 38 | "avatar_url": "https://avatars.githubusercontent.com/u/974142?v=4", 39 | "gravatar_id": "", 40 | "url": "https://api.github.com/users/Khady", 41 | "html_url": "https://github.com/Khady", 42 | "followers_url": "https://api.github.com/users/Khady/followers", 43 | "following_url": "https://api.github.com/users/Khady/following{/other_user}", 44 | "gists_url": "https://api.github.com/users/Khady/gists{/gist_id}", 45 | "starred_url": "https://api.github.com/users/Khady/starred{/owner}{/repo}", 46 | "subscriptions_url": "https://api.github.com/users/Khady/subscriptions", 47 | "organizations_url": "https://api.github.com/users/Khady/orgs", 48 | "repos_url": "https://api.github.com/users/Khady/repos", 49 | "events_url": "https://api.github.com/users/Khady/events{/privacy}", 50 | "received_events_url": "https://api.github.com/users/Khady/received_events", 51 | "type": "User", 52 | "site_admin": false 53 | }, 54 | "committer": { 55 | "login": "web-flow", 56 | "id": 19864447, 57 | "node_id": "MDQ6VXNlcjE5ODY0NDQ3", 58 | "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", 59 | "gravatar_id": "", 60 | "url": "https://api.github.com/users/web-flow", 61 | "html_url": "https://github.com/web-flow", 62 | "followers_url": "https://api.github.com/users/web-flow/followers", 63 | "following_url": "https://api.github.com/users/web-flow/following{/other_user}", 64 | "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", 65 | "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", 66 | "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", 67 | "organizations_url": "https://api.github.com/users/web-flow/orgs", 68 | "repos_url": "https://api.github.com/users/web-flow/repos", 69 | "events_url": "https://api.github.com/users/web-flow/events{/privacy}", 70 | "received_events_url": "https://api.github.com/users/web-flow/received_events", 71 | "type": "User", 72 | "site_admin": false 73 | }, 74 | "parents": [ 75 | { 76 | "sha": "56ada1084d6b5cc71821f65cf41f013fe6c280f3", 77 | "url": "https://api.github.com/repos/ahrefs/monorobot/commits/56ada1084d6b5cc71821f65cf41f013fe6c280f3", 78 | "html_url": "https://github.com/ahrefs/monorobot/commit/56ada1084d6b5cc71821f65cf41f013fe6c280f3" 79 | }, 80 | { 81 | "sha": "883724426505ec03e29a1e4fd11079797a30bd57", 82 | "url": "https://api.github.com/repos/ahrefs/monorobot/commits/883724426505ec03e29a1e4fd11079797a30bd57", 83 | "html_url": "https://github.com/ahrefs/monorobot/commit/883724426505ec03e29a1e4fd11079797a30bd57" 84 | } 85 | ] 86 | }, 87 | "_links": { 88 | "self": "https://api.github.com/repos/ahrefs/monorobot/branches/master", 89 | "html": "https://github.com/ahrefs/monorobot/tree/master" 90 | }, 91 | "protected": false, 92 | "protection": { 93 | "enabled": false, 94 | "required_status_checks": { 95 | "enforcement_level": "off", 96 | "contexts": [ 97 | 98 | ], 99 | "checks": [ 100 | 101 | ] 102 | } 103 | }, 104 | "protection_url": "https://api.github.com/repos/ahrefs/monorobot/branches/master/protection" 105 | } 106 | -------------------------------------------------------------------------------- /test/github-api-cache/ahrefs_monorobot_branch_yasu_slack-msg-fix-escaping-and-fallback: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yasu/slack-msg-fix-escaping-and-fallback", 3 | "commit": { 4 | "sha": "6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 5 | "node_id": "C_kwDOC5saPNoAKDZlYzZiNDdkMWYzMzNkMzBkNWMzODg5ZTA1ZGNhYWQwNzhmMGU3N2Q", 6 | "commit": { 7 | "author": { 8 | "name": "Louis", 9 | "email": "mail@example.org", 10 | "date": "2022-10-26T00:47:59Z" 11 | }, 12 | "committer": { 13 | "name": "GitHub", 14 | "email": "noreply@github.com", 15 | "date": "2022-10-26T00:47:59Z" 16 | }, 17 | "message": "Merge pull request #126 from sewenthy/sewen/121-ignore-code-comments-from-defined-list-of-users\n\nIgnore code comments from defined list of users", 18 | "tree": { 19 | "sha": "3306d7ce0d2ac3324755dbe410d3fa3da841fe7f", 20 | "url": "https://api.github.com/repos/ahrefs/monorobot/git/trees/3306d7ce0d2ac3324755dbe410d3fa3da841fe7f" 21 | }, 22 | "url": "https://api.github.com/repos/ahrefs/monorobot/git/commits/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 23 | "comment_count": 0, 24 | "verification": { 25 | "verified": true, 26 | "reason": "valid", 27 | "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJjWIO/CRBK7hj4Ov3rIwAAEWAIAI7P6ReUnb6bqigQ06Iqgzrm\nK16ReYELdE6Gp5lv24pw4fEa8vW7u5M8Rp4FndfpcnwjODyw9BRN1xzmJJ6sbNgZ\nusB12KyN6Q7MPAmRcoleKzndfcTpoAGR1chuVeDuMT6VcSjHdCbkNO8nhbXnhjPY\nDncx6uXrm8t+ak8IcbJ6nbxFvDPlqySGK7GvbsNIFhSdIy5FkvFg8kVYXssnD0mn\nhfGWCyKBfbkIAIR/w2Rdb6xXm6E2zjMFZp6BDRNhq0hv3R8/hj7GJdw6I+WwYq39\ntUIgPyYtYJpB8NK5DuTqfK7bIEiz3tXkXJ84iyx8Wscpldq60GYA0r0c2QBb7io=\n=yv18\n-----END PGP SIGNATURE-----\n", 28 | "payload": "tree 3306d7ce0d2ac3324755dbe410d3fa3da841fe7f\nparent 56ada1084d6b5cc71821f65cf41f013fe6c280f3\nparent 883724426505ec03e29a1e4fd11079797a30bd57\nauthor Louis 1666745279 +0800\ncommitter GitHub 1666745279 +0800\n\nMerge pull request #126 from sewenthy/sewen/121-ignore-code-comments-from-defined-list-of-users\n\nIgnore code comments from defined list of users" 29 | } 30 | }, 31 | "url": "https://api.github.com/repos/ahrefs/monorobot/commits/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 32 | "html_url": "https://github.com/ahrefs/monorobot/commit/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d", 33 | "comments_url": "https://api.github.com/repos/ahrefs/monorobot/commits/6ec6b47d1f333d30d5c3889e05dcaad078f0e77d/comments", 34 | "author": { 35 | "login": "Khady", 36 | "id": 974142, 37 | "node_id": "MDQ6VXNlcjk3NDE0Mg==", 38 | "avatar_url": "https://avatars.githubusercontent.com/u/974142?v=4", 39 | "gravatar_id": "", 40 | "url": "https://api.github.com/users/Khady", 41 | "html_url": "https://github.com/Khady", 42 | "followers_url": "https://api.github.com/users/Khady/followers", 43 | "following_url": "https://api.github.com/users/Khady/following{/other_user}", 44 | "gists_url": "https://api.github.com/users/Khady/gists{/gist_id}", 45 | "starred_url": "https://api.github.com/users/Khady/starred{/owner}{/repo}", 46 | "subscriptions_url": "https://api.github.com/users/Khady/subscriptions", 47 | "organizations_url": "https://api.github.com/users/Khady/orgs", 48 | "repos_url": "https://api.github.com/users/Khady/repos", 49 | "events_url": "https://api.github.com/users/Khady/events{/privacy}", 50 | "received_events_url": "https://api.github.com/users/Khady/received_events", 51 | "type": "User", 52 | "site_admin": false 53 | }, 54 | "committer": { 55 | "login": "web-flow", 56 | "id": 19864447, 57 | "node_id": "MDQ6VXNlcjE5ODY0NDQ3", 58 | "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", 59 | "gravatar_id": "", 60 | "url": "https://api.github.com/users/web-flow", 61 | "html_url": "https://github.com/web-flow", 62 | "followers_url": "https://api.github.com/users/web-flow/followers", 63 | "following_url": "https://api.github.com/users/web-flow/following{/other_user}", 64 | "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", 65 | "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", 66 | "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", 67 | "organizations_url": "https://api.github.com/users/web-flow/orgs", 68 | "repos_url": "https://api.github.com/users/web-flow/repos", 69 | "events_url": "https://api.github.com/users/web-flow/events{/privacy}", 70 | "received_events_url": "https://api.github.com/users/web-flow/received_events", 71 | "type": "User", 72 | "site_admin": false 73 | }, 74 | "parents": [ 75 | { 76 | "sha": "56ada1084d6b5cc71821f65cf41f013fe6c280f3", 77 | "url": "https://api.github.com/repos/ahrefs/monorobot/commits/56ada1084d6b5cc71821f65cf41f013fe6c280f3", 78 | "html_url": "https://github.com/ahrefs/monorobot/commit/56ada1084d6b5cc71821f65cf41f013fe6c280f3" 79 | }, 80 | { 81 | "sha": "883724426505ec03e29a1e4fd11079797a30bd57", 82 | "url": "https://api.github.com/repos/ahrefs/monorobot/commits/883724426505ec03e29a1e4fd11079797a30bd57", 83 | "html_url": "https://github.com/ahrefs/monorobot/commit/883724426505ec03e29a1e4fd11079797a30bd57" 84 | } 85 | ] 86 | }, 87 | "_links": { 88 | "self": "https://api.github.com/repos/ahrefs/monorobot/branches/master", 89 | "html": "https://github.com/ahrefs/monorobot/tree/master" 90 | }, 91 | "protected": false, 92 | "protection": { 93 | "enabled": false, 94 | "required_status_checks": { 95 | "enforcement_level": "off", 96 | "contexts": [ 97 | 98 | ], 99 | "checks": [ 100 | 101 | ] 102 | } 103 | }, 104 | "protection_url": "https://api.github.com/repos/ahrefs/monorobot/branches/master/protection" 105 | } 106 | -------------------------------------------------------------------------------- /test/github-api-cache/xinyuluo_monorepo_commit_1edcdf5e45a8b7ed01c247b981d8f42d426a7794: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 3 | "node_id": "00000000000000000000", 4 | "commit": { 5 | "author": { 6 | "name": "xinyuluo", 7 | "email": "mail@example.org", 8 | "date": "2020-05-21T03:04:22Z" 9 | }, 10 | "committer": { 11 | "name": "GitHub", 12 | "email": "noreply@github.com", 13 | "date": "2020-05-21T03:04:22Z" 14 | }, 15 | "message": "Add files via upload", 16 | "tree": { 17 | "sha": "b0e3f737ccd133dec1b880518f8fee587a67f92d", 18 | "url": "https://api.github.com/repos/xinyuluo/monorepo/git/trees/b0e3f737ccd133dec1b880518f8fee587a67f92d" 19 | }, 20 | "url": "https://api.github.com/repos/xinyuluo/monorepo/git/commits/1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 21 | "comment_count": 2, 22 | "verification": { 23 | "verified": true, 24 | "reason": "valid", 25 | "signature": "", 26 | "payload": "tree b0e3f737ccd133dec1b880518f8fee587a67f92d\nparent f8384148db4f6bc2d10daf54764fe67b8f4223f2\nauthor xinyuluo 1590030262 +0800\ncommitter GitHub 1590030262 +0800\n\nAdd files via upload" 27 | } 28 | }, 29 | "url": "https://api.github.com/repos/xinyuluo/monorepo/commits/1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 30 | "html_url": "https://github.com/xinyuluo/monorepo/commit/1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 31 | "comments_url": "https://api.github.com/repos/xinyuluo/monorepo/commits/1edcdf5e45a8b7ed01c247b981d8f42d426a7794/comments", 32 | "author": { 33 | "login": "xinyuluo", 34 | "id": 0, 35 | "node_id": "00000000000000000000", 36 | "avatar_url": "https://avatars0.githubusercontent.com/u/000000?v=4", 37 | "gravatar_id": "", 38 | "url": "https://api.github.com/users/xinyuluo", 39 | "html_url": "https://github.com/xinyuluo", 40 | "followers_url": "https://api.github.com/users/xinyuluo/followers", 41 | "following_url": "https://api.github.com/users/xinyuluo/following{/other_user}", 42 | "gists_url": "https://api.github.com/users/xinyuluo/gists{/gist_id}", 43 | "starred_url": "https://api.github.com/users/xinyuluo/starred{/owner}{/repo}", 44 | "subscriptions_url": "https://api.github.com/users/xinyuluo/subscriptions", 45 | "organizations_url": "https://api.github.com/users/xinyuluo/orgs", 46 | "repos_url": "https://api.github.com/users/xinyuluo/repos", 47 | "events_url": "https://api.github.com/users/xinyuluo/events{/privacy}", 48 | "received_events_url": "https://api.github.com/users/xinyuluo/received_events", 49 | "type": "User", 50 | "site_admin": false 51 | }, 52 | "committer": { 53 | "login": "web-flow", 54 | "id": 0, 55 | "node_id": "00000000000000000000", 56 | "avatar_url": "https://avatars3.githubusercontent.com/u/000000?v=4", 57 | "gravatar_id": "", 58 | "url": "https://api.github.com/users/web-flow", 59 | "html_url": "https://github.com/web-flow", 60 | "followers_url": "https://api.github.com/users/web-flow/followers", 61 | "following_url": "https://api.github.com/users/web-flow/following{/other_user}", 62 | "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", 63 | "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", 64 | "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", 65 | "organizations_url": "https://api.github.com/users/web-flow/orgs", 66 | "repos_url": "https://api.github.com/users/web-flow/repos", 67 | "events_url": "https://api.github.com/users/web-flow/events{/privacy}", 68 | "received_events_url": "https://api.github.com/users/web-flow/received_events", 69 | "type": "User", 70 | "site_admin": false 71 | }, 72 | "parents": [ 73 | { 74 | "sha": "f8384148db4f6bc2d10daf54764fe67b8f4223f2", 75 | "url": "https://api.github.com/repos/xinyuluo/monorepo/commits/f8384148db4f6bc2d10daf54764fe67b8f4223f2", 76 | "html_url": "https://github.com/xinyuluo/monorepo/commit/f8384148db4f6bc2d10daf54764fe67b8f4223f2" 77 | } 78 | ], 79 | "stats": { 80 | "total": 243, 81 | "additions": 242, 82 | "deletions": 1 83 | }, 84 | "files": [ 85 | { 86 | "sha": "792f7fb2fb90f6c7d3403ff6fd139227f70b4642", 87 | "filename": "Makefile", 88 | "status": "added", 89 | "additions": 21, 90 | "deletions": 0, 91 | "changes": 21, 92 | "blob_url": "https://github.com/xinyuluo/monorepo/blob/1edcdf5e45a8b7ed01c247b981d8f42d426a7794/Makefile", 93 | "raw_url": "https://github.com/xinyuluo/monorepo/raw/1edcdf5e45a8b7ed01c247b981d8f42d426a7794/Makefile", 94 | "contents_url": "https://api.github.com/repos/xinyuluo/monorepo/contents/Makefile?ref=1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 95 | "patch": "" 96 | }, 97 | { 98 | "sha": "1a100653600611bce8fd16e6d107b3d287f5c900", 99 | "filename": "README.md", 100 | "status": "modified", 101 | "additions": 35, 102 | "deletions": 1, 103 | "changes": 36, 104 | "blob_url": "https://github.com/xinyuluo/monorepo/blob/1edcdf5e45a8b7ed01c247b981d8f42d426a7794/README.md", 105 | "raw_url": "https://github.com/xinyuluo/monorepo/raw/1edcdf5e45a8b7ed01c247b981d8f42d426a7794/README.md", 106 | "contents_url": "https://api.github.com/repos/xinyuluo/monorepo/contents/README.md?ref=1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 107 | "patch": "" 108 | }, 109 | { 110 | "sha": "bf0bb3dde5bd449ffdc0d9227160b4aa95f74047", 111 | "filename": "backend/a1/main.ml", 112 | "status": "added", 113 | "additions": 186, 114 | "deletions": 0, 115 | "changes": 186, 116 | "blob_url": "https://github.com/xinyuluo/monorepo/blob/1edcdf5e45a8b7ed01c247b981d8f42d426a7794/main.ml", 117 | "raw_url": "https://github.com/xinyuluo/monorepo/raw/1edcdf5e45a8b7ed01c247b981d8f42d426a7794/main.ml", 118 | "contents_url": "https://api.github.com/repos/xinyuluo/monorepo/contents/main.ml?ref=1edcdf5e45a8b7ed01c247b981d8f42d426a7794", 119 | "patch": "" 120 | } 121 | ] 122 | } 123 | -------------------------------------------------------------------------------- /test/buildkite-api-cache/organizations_org_pipelines_pipeline2_builds_181733: -------------------------------------------------------------------------------- 1 | { 2 | "id": "018fd828-7bce-4c40-a75e-df46ea0bae8d", 3 | "graphql_id": "QnVpbGQtLS0wMThmZDgyOC03YmNlLTRjNDAtYTc1ZS1kZjQ2ZWEwYmFlOGQ=", 4 | "url": "https://api.buildkite.com/v2/organizations/ahrefs/pipelines/pipeline2/builds/181733", 5 | "web_url": "https://buildkite.com/org/pipeline2/builds/181733", 6 | "number": 181733, 7 | "state": "passed", 8 | "cancel_reason": null, 9 | "blocked": false, 10 | "blocked_state": "", 11 | "message": "c1 message", 12 | "commit": "51d7c2d0fc8f182f8ffad40ef79471789e6f5578", 13 | "branch": "other-branch", 14 | "tag": null, 15 | "env": {}, 16 | "source": "webhook", 17 | "author": { 18 | "name": "author", 19 | "email": "author@ahrefs.com", 20 | "date": "2024-05-31T03:54:00Z" 21 | }, 22 | "creator": { 23 | "id": "5d606b51-82ad-41aa-acf3-d0bf150ed353", 24 | "graphql_id": "VXNlci0tLTVkNjA2YjUxLTgyYWQtNDFhYS1hY2YzLWQwYmYxNTBlZDM1Mw==", 25 | "name": "author", 26 | "email": "author@ahrefs.com", 27 | "avatar_url": "https://www.gravatar.com/avatar/f9e385f84bc0fa77bfb0f5893a5c18d5", 28 | "created_at": "2016-08-29T05:29:30.389Z" 29 | }, 30 | "created_at": "2024-06-02T08:54:42.920Z", 31 | "scheduled_at": "2024-06-02T08:54:42.867Z", 32 | "started_at": "2024-06-02T08:54:47.128Z", 33 | "finished_at": "2024-06-02T09:19:17.829Z", 34 | "meta_data": { 35 | "buildkite:git:commit": "commit 51d7c2d0fc8f182f8ffad40ef79471789e6f5578\nabbrev-commit 51d7c2d\nAuthor: Author Name \n\n Commit message" 36 | }, 37 | "pull_request": null, 38 | "rebuilt_from": null, 39 | "pipeline": { 40 | "id": "fa4ddfa6-bed6-4323-ae7b-74ff7164c147", 41 | "graphql_id": "UGlwZWxpbmUtLS1mYTRkZGZhNi1iZWQ2LTQzMjMtYWU3Yi03NGZmNzE2NGMxNDc=", 42 | "url": "https://api.buildkite.com/v2/organizations/ahrefs/pipelines/pipeline2", 43 | "web_url": "https://buildkite.com/org/pipeline2", 44 | "name": "pipeline2", 45 | "description": "", 46 | "slug": "pipeline2", 47 | "repository": "git@git.org.com.com:ahrefs/pipeline2.git", 48 | "cluster_id": null, 49 | "pipeline_template_uuid": null, 50 | "default_branch": "develop", 51 | "skip_queued_branch_builds": true, 52 | "skip_queued_branch_builds_filter": "", 53 | "cancel_running_branch_builds": false, 54 | "cancel_running_branch_builds_filter": "", 55 | "allow_rebuilds": true, 56 | "provider": { 57 | "id": "github_enterprise", 58 | "settings": { 59 | "build_branches": true, 60 | "build_merge_group_checks_requested": false, 61 | "build_pull_request_base_branch_changed": false, 62 | "build_pull_request_forks": false, 63 | "build_pull_request_labels_changed": false, 64 | "build_pull_request_ready_for_review": false, 65 | "build_pull_requests": true, 66 | "build_tags": true, 67 | "cancel_deleted_branch_builds": true, 68 | "filter_enabled": false, 69 | "prefix_pull_request_fork_branch_names": true, 70 | "publish_blocked_as_pending": false, 71 | "publish_commit_status_per_step": true, 72 | "publish_commit_status": true, 73 | "pull_request_branch_filter_enabled": false, 74 | "separate_pull_request_statuses": false, 75 | "skip_builds_for_existing_commits": false, 76 | "skip_pull_request_builds_for_existing_commits": true, 77 | "trigger_mode": "code", 78 | "use_step_key_as_commit_status": false, 79 | "repository": "ahrefs/pipeline2", 80 | "pull_request_branch_filter_configuration": "", 81 | "filter_condition": "" 82 | }, 83 | "webhook_url": "" 84 | }, 85 | "builds_url": "https://api.buildkite.com/v2/organizations/ahrefs/pipelines/pipeline2/builds", 86 | "badge_url": "https://badge.buildkite.com/93c1e7b73b922bbb48eeb8cdab4f2efdb6a00ce1aa06441fd6.svg", 87 | "created_by": { 88 | "id": "d5617cb0-e22d-46ab-ba1b-fba1f2a32c6f", 89 | "graphql_id": "VXNlci0tLWQ1NjE3Y2IwLWUyMmQtNDZhYi1iYTFiLWZiYTFmMmEzMmM2Zg==", 90 | "name": "user", 91 | "email": "user@ahrefs.com", 92 | "avatar_url": "https://www.gravatar.com/avatar/de1d8b95df4bc5d94c40583280a72876", 93 | "created_at": "2014-11-25T10:09:34.691Z" 94 | }, 95 | "created_at": "2018-04-03T21:13:37.212Z", 96 | "archived_at": null, 97 | "env": {}, 98 | "scheduled_builds_count": 0, 99 | "running_builds_count": 4, 100 | "scheduled_jobs_count": 1, 101 | "running_jobs_count": 9, 102 | "waiting_jobs_count": 2, 103 | "visibility": "private", 104 | "tags": null, 105 | "emoji": null, 106 | "color": null, 107 | "steps": [ 108 | { 109 | "type": "script", 110 | "name": "setup", 111 | "command": "", 112 | "artifact_paths": "", 113 | "branch_configuration": "", 114 | "env": {}, 115 | "timeout_in_minutes": null, 116 | "agent_query_rules": [ 117 | "bookworm=true" 118 | ], 119 | "concurrency": null, 120 | "parallelism": null 121 | } 122 | ], 123 | "cluster_url": null 124 | }, 125 | "jobs": [ 126 | { 127 | "id": "018fd828-7be6-4e25-b4b8-0f4eeff74f17", 128 | "graphql_id": "Sm9iLS0tMDE4ZmQ4MjgtN2JlNi00ZTI1LWI0YjgtMGY0ZWVmZjc0ZjE3", 129 | "type": "script", 130 | "name": "setup", 131 | "step_key": null, 132 | "step": { 133 | "id": "018fd828-7bce-4d7f-b32c-224089a56687", 134 | "signature": null 135 | }, 136 | "priority": { 137 | "number": 0 138 | }, 139 | "agent_query_rules": [], 140 | "state": "passed", 141 | "build_url": "https://api.buildkite.com/v2/organizations/ahrefs/pipelines/monorepo/builds/181733", 142 | "web_url": "https://buildkite.com/org/monorepo/builds/181733#018fd828-7be6-4e25-b4b8-0f4eeff74f17", 143 | "log_url": "https://api.buildkite.com/v2/organizations/ahrefs/pipelines/monorepo/builds/181733/jobs/018fd828-7be6-4e25-b4b8-0f4eeff74f17/log", 144 | "raw_log_url": "https://api.buildkite.com/v2/organizations/ahrefs/pipelines/monorepo/builds/181733/jobs/018fd828-7be6-4e25-b4b8-0f4eeff74f17/log.txt", 145 | "artifacts_url": "https://api.buildkite.com/v2/organizations/ahrefs/pipelines/monorepo/builds/181733/jobs/018fd828-7be6-4e25-b4b8-0f4eeff74f17/artifacts", 146 | "command": "", 147 | "soft_failed": false, 148 | "exit_status": 0, 149 | "artifact_paths": "", 150 | "created_at": "2024-06-02T08:54:42.894Z", 151 | "scheduled_at": "2024-06-02T08:54:42.894Z", 152 | "runnable_at": "2024-06-02T08:54:42.961Z", 153 | "started_at": "2024-06-02T08:54:47.128Z", 154 | "finished_at": "2024-06-02T08:54:56.649Z", 155 | "expired_at": null, 156 | "retried": false, 157 | "retried_in_job_id": null, 158 | "retries_count": null, 159 | "retry_source": null, 160 | "retry_type": null, 161 | "parallel_group_index": null, 162 | "parallel_group_total": null, 163 | "matrix": null, 164 | "agent": null, 165 | "cluster_id": null, 166 | "cluster_url": null, 167 | "cluster_queue_id": null, 168 | "cluster_queue_url": null 169 | } 170 | ], 171 | "cluster_id": null, 172 | "cluster_url": null 173 | } 174 | --------------------------------------------------------------------------------