mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-03 10:35:35 +00:00
fix(ci): repair draft PR command dispatch (#9271)
* fix(ci): repair draft PR command dispatch Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): dispatch command workflows by branch ref Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): admit PR authors and writers for CI commands Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): preserve dispatch guard dependency semantics Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): configure slash command permissions individually Signed-off-by: WenyXu <wenymedia@gmail.com> * refactor(ci): run command workflows at dispatch branch head Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): rerun fork PR checks and report command failures Signed-off-by: WenyXu <wenymedia@gmail.com> --------- Signed-off-by: WenyXu <wenymedia@gmail.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Admit `/ci` comments and dispatch a pinned draft-PR CI workflow."""
|
||||
"""Admit `/ci` comments and dispatch a draft-PR CI workflow."""
|
||||
import json, os, re, sys, urllib.request
|
||||
|
||||
COMMAND = re.compile(r'^/ci(?:\s+(.+))?$')
|
||||
@@ -29,7 +29,7 @@ OPTIONS = {
|
||||
'fuzz chaos': ('integration.yml', 'chaos', 'chaos fuzz'),
|
||||
'fuzz all': ('integration.yml', 'all', 'all fuzz'),
|
||||
}
|
||||
HELP = '''Available draft-PR CI commands:\n\n- `/ci` or `/ci rust` — Rust CI\n- `/ci integration` — integration CI without fuzz\n- `/ci checks` or `/ci docs`\n- `/ci fuzz standalone|distributed|chaos`\n- `/ci fuzz all` — all fuzz suites (admin only)\n\nCommands require repository admin permission and a same-repository open draft PR. CI is pinned to the current head SHA; comment again after a push.'''
|
||||
HELP = '''Available draft-PR CI commands:\n\n- `/ci` — standard CI\n- `/ci rust` — Rust CI\n- `/ci integration` — integration CI without fuzz\n- `/ci checks` or `/ci docs`\n- `/ci fuzz standalone|distributed|chaos`\n- `/ci fuzz all` — all fuzz suites (admin only)\n\nCommands require the PR author or repository write/maintain/admin permission and an open draft PR. Fork PRs require repository write permission and rerun existing pull-request CI; fuzz commands are same-repository only. `/ci fuzz all` requires admin permission. Same-repository CI runs the branch head at dispatch time. Fork CI reruns the original pull-request revision (within GitHub rerun limits); sync your branch with the updated CI configuration first.'''
|
||||
|
||||
def api(path):
|
||||
req=urllib.request.Request(os.environ['GITHUB_API_URL']+path, headers={'Authorization':'Bearer '+os.environ['GITHUB_TOKEN'],'Accept':'application/vnd.github+json'})
|
||||
@@ -46,7 +46,7 @@ def reject(number, text): out(skip='true',pr_number=number,reply='CI command ign
|
||||
def main():
|
||||
if os.environ.get('DISPATCH_SENDER') != 'github-actions[bot]': return reject('', 'invalid dispatch sender.')
|
||||
comment=api('/repos/'+os.environ['GITHUB_REPOSITORY']+'/issues/comments/'+os.environ['COMMENT_ID'])
|
||||
body=comment.get('body','').splitlines()[0].strip(); m=COMMAND.fullmatch(body)
|
||||
body=(comment.get('body') or '').partition('\n')[0].strip(); m=COMMAND.fullmatch(body)
|
||||
number=str(comment.get('issue_url','').rstrip('/').split('/')[-1])
|
||||
if not m: return reject(number,'comment is not a `/ci` command.')
|
||||
arg=(m.group(1) or '').strip().lower()
|
||||
@@ -54,13 +54,38 @@ def main():
|
||||
if arg=='help': out(skip='true',pr_number=number,reply=HELP); return 0
|
||||
pr=api('/repos/'+os.environ['GITHUB_REPOSITORY']+'/pulls/'+number)
|
||||
if pr.get('state')!='open' or not pr.get('draft'): return reject(number,'PR must be open and draft.')
|
||||
if pr.get('head',{}).get('repo',{}).get('full_name') != os.environ['GITHUB_REPOSITORY']: return reject(number,'fork PRs are not admitted.')
|
||||
head=pr.get('head',{}).get('sha','')
|
||||
if head != os.environ.get('DISPATCH_HEAD_SHA') or not re.fullmatch('[0-9a-f]{40}',head): return reject(number,'PR head changed; comment again.')
|
||||
fork=pr.get('head',{}).get('repo',{}).get('full_name') != os.environ['GITHUB_REPOSITORY']
|
||||
if fork and arg.startswith('fuzz '): return reject(number,'fuzz commands require a same-repository PR.')
|
||||
head=pr.get('head',{})
|
||||
head_sha=head.get('sha','')
|
||||
head_ref=head.get('ref','')
|
||||
if head_sha != os.environ.get('DISPATCH_HEAD_SHA') or not re.fullmatch('[0-9a-f]{40}',head_sha) or not head_ref: return reject(number,'PR head changed; comment again.')
|
||||
actor=comment.get('user',{}).get('login','')
|
||||
permission=api('/repos/'+os.environ['GITHUB_REPOSITORY']+'/collaborators/'+actor+'/permission').get('user',{}).get('permission')
|
||||
if permission!='admin': return reject(number,'repository admin permission is required.')
|
||||
if not actor: return reject(number,'comment author is missing.')
|
||||
is_author=actor==pr.get('user',{}).get('login')
|
||||
if fork or arg=='fuzz all' or not is_author:
|
||||
permission=api('/repos/'+os.environ['GITHUB_REPOSITORY']+'/collaborators/'+actor+'/permission').get('permission')
|
||||
if arg=='fuzz all':
|
||||
if permission!='admin': return reject(number,'repository admin permission is required for `/ci fuzz all`.')
|
||||
elif permission not in ('write','maintain','admin'):
|
||||
return reject(number,'PR author or repository write permission is required.')
|
||||
workflow, profile, label=OPTIONS[arg]
|
||||
out(skip='false',pr_number=number,head_sha=head,workflow=workflow,fuzz_profile=profile,reply=f'Dispatched {label} for `{head}`.')
|
||||
if fork:
|
||||
runs=api('/repos/'+os.environ['GITHUB_REPOSITORY']+'/actions/runs?event=pull_request&head_sha='+head_sha+'&per_page=100')['workflow_runs']
|
||||
selected={}
|
||||
for run in runs:
|
||||
name=run.get('path','').split('@',1)[0].removeprefix('.github/workflows/')
|
||||
if name not in workflow.split(',') or name in selected: continue
|
||||
if run.get('head_sha') != head_sha or run.get('head_repository',{}).get('full_name') != head['repo']['full_name'] or run.get('head_branch') != head_ref: continue
|
||||
selected[name]=run
|
||||
if not selected: return reject(number,'no pull-request CI runs found for this head; push a new commit first.')
|
||||
if arg and workflow not in selected: return reject(number,'selected CI has no pull-request run for this head.')
|
||||
if any(run['status'] != 'completed' for run in selected.values()): return reject(number,'pull-request CI is still pending; approve or wait for it, then comment again.')
|
||||
for run in selected.values():
|
||||
original=run if run.get('run_attempt',1)==1 else api('/repos/'+os.environ['GITHUB_REPOSITORY']+'/actions/runs/'+str(run['id'])+'/attempts/1')
|
||||
if original.get('conclusion') != 'skipped': return reject(number,'only originally skipped draft CI runs can be requested; push a new commit while draft.')
|
||||
out(skip='false',pr_number=number,run_ids=','.join(str(run['id']) for run in selected.values()),reply=f'Requested rerun of {", ".join(selected)} for `{head_sha}`.')
|
||||
return 0
|
||||
out(skip='false',pr_number=number,head_sha=head_sha,head_ref=head_ref,workflow=workflow,fuzz_profile=profile,reply=f'Dispatched {label} for branch `{head_ref}`.')
|
||||
return 0
|
||||
if __name__ == '__main__': sys.exit(main())
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
# limitations under the License.
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
@@ -27,6 +30,9 @@ ci = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ci)
|
||||
|
||||
|
||||
OPTIONS_TO_TEST = ["/ci", "/ci rust", "/ci fuzz chaos", "/ci fuzz all"]
|
||||
|
||||
|
||||
class CiSlashTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.output = tempfile.NamedTemporaryFile(delete=False)
|
||||
@@ -54,23 +60,145 @@ class CiSlashTest(unittest.TestCase):
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn("Available draft-PR CI commands", output)
|
||||
|
||||
def test_dispatches_full_suite_at_verified_head(self):
|
||||
def test_dispatches_full_suite_with_top_level_admin_permission(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "admin"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
{"user": {"permission": "admin"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
{"permission": "admin", "user": {"login": "admin"}},
|
||||
])
|
||||
self.assertIn("skip=false", output)
|
||||
self.assertIn("head_ref=fix/draft-ci", output)
|
||||
self.assertIn("workflow=rust.yml,integration.yml,checks.yml,docs.yml", output)
|
||||
|
||||
def test_rejects_non_draft_before_permission_lookup(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci fuzz chaos", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "admin"}},
|
||||
{"state": "open", "draft": False, "head": {"sha": "a" * 40, "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
{"state": "open", "draft": False, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
])
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn("PR must be open and draft", output)
|
||||
|
||||
def test_rejects_missing_head_ref_before_permission_lookup(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "admin"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
])
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn("PR head changed; comment again.", output)
|
||||
|
||||
def test_permission_matrix(self):
|
||||
for command in OPTIONS_TO_TEST:
|
||||
for author in (True, False):
|
||||
for permission in ("admin", "maintain", "write", "triage", "read", "none", None):
|
||||
with self.subTest(command=command, author=author, permission=permission):
|
||||
Path(self.output.name).write_text("")
|
||||
responses = [
|
||||
{"body": command, "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "actor"}},
|
||||
{"state": "open", "draft": True, "user": {"login": "actor" if author else "other"}, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
]
|
||||
if command == "/ci fuzz all" or not author:
|
||||
responses.append({"permission": permission})
|
||||
output = self.run_main(responses)
|
||||
allowed = permission == "admin" if command == "/ci fuzz all" else author or permission in ("admin", "maintain", "write")
|
||||
self.assertIn("skip=false" if allowed else "skip=true", output)
|
||||
|
||||
def test_reply_uses_issue_comment_endpoint(self):
|
||||
workflow = SCRIPT.parents[1] / "workflows" / "ci-slash.yml"
|
||||
step = workflow.read_text().split(" - name: Reply with command result\n", 1)[1]
|
||||
command = textwrap.dedent(step.split(" run: |\n", 1)[1])
|
||||
reply = "Denied: `example`\nSecond line with $variables and 'quotes'"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
gh = Path(directory) / "gh"
|
||||
gh.write_text("#!/usr/bin/env python3\nimport json, sys\nprint(json.dumps(sys.argv[1:]))\n")
|
||||
gh.chmod(0o755)
|
||||
env = {**os.environ, "PATH": directory + os.pathsep + os.environ["PATH"], "GITHUB_REPOSITORY": "GreptimeTeam/greptimedb", "PR_NUMBER": "42", "REPLY": reply, "ADMIT_OUTCOME": "success", "DISPATCH_OUTCOME": "success"}
|
||||
result = subprocess.run(["bash", "-eu", "-c", command], env=env, check=True, capture_output=True, text=True)
|
||||
self.assertEqual(json.loads(result.stdout), ["api", "--method", "POST", "/repos/GreptimeTeam/greptimedb/issues/42/comments", "-f", "body=" + reply])
|
||||
|
||||
def test_failure_reply_and_partial_dispatch(self):
|
||||
workflow = (SCRIPT.parents[1] / "workflows" / "ci-slash.yml").read_text()
|
||||
dispatch = textwrap.dedent(workflow.split(" - name: Dispatch selected CI workflow\n", 1)[1].split(" run: |\n", 1)[1].split(" - name: Reply", 1)[0])
|
||||
reply = textwrap.dedent(workflow.split(" - name: Reply with command result\n", 1)[1].split(" run: |\n", 1)[1])
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
gh = Path(directory) / "gh"
|
||||
log = Path(directory) / "calls"
|
||||
gh.write_text("#!/usr/bin/env python3\nimport json, os, sys\nwith open(os.environ['CALLS'], 'a') as f: f.write(json.dumps(sys.argv[1:]) + '\\n')\nsys.exit(1 if '/runs/8/rerun' in sys.argv[-1] else 0)\n")
|
||||
gh.chmod(0o755)
|
||||
env = {**os.environ, "PATH": directory + os.pathsep + os.environ["PATH"], "CALLS": str(log), "GITHUB_REPOSITORY": "GreptimeTeam/greptimedb", "RUN_IDS": "7,8,9", "PR_NUMBER": "42", "REPLY": "Success", "ADMIT_OUTCOME": "success", "DISPATCH_OUTCOME": "failure", "GITHUB_SERVER_URL": "https://github.com", "GITHUB_RUN_ID": "123"}
|
||||
result = subprocess.run(["bash", "-eu", "-c", dispatch], env=env, capture_output=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
subprocess.run(["bash", "-eu", "-c", reply], env=env, check=True, capture_output=True)
|
||||
calls = [json.loads(line) for line in log.read_text().splitlines()]
|
||||
self.assertEqual([call[-1] for call in calls[:2]], ["/repos/GreptimeTeam/greptimedb/actions/runs/7/rerun", "/repos/GreptimeTeam/greptimedb/actions/runs/8/rerun"])
|
||||
self.assertIn("CI trigger failed; some workflows may already have been requested", calls[2][-1])
|
||||
self.assertIn("/actions/runs/123", calls[2][-1])
|
||||
self.assertIn("!cancelled()", workflow.split(" - name: Reply with command result", 1)[1])
|
||||
|
||||
def test_fork_rerun_gates_cover_standard_jobs(self):
|
||||
import re
|
||||
for name, count in [("rust", 7), ("integration", 5), ("checks", 5), ("docs", 3)]:
|
||||
workflow = (SCRIPT.parents[1] / "workflows" / (name + ".yml")).read_text()
|
||||
gates = re.findall(r"^ if:.*github.run_attempt > 1.*$", workflow, re.MULTILINE)
|
||||
self.assertEqual(len(gates), count, name)
|
||||
self.assertTrue(all("always()" not in gate for gate in gates))
|
||||
|
||||
def test_command_permissions(self):
|
||||
workflow = SCRIPT.parents[1] / "workflows" / "slash-command-dispatch.yml"
|
||||
config = workflow.read_text().split("config: >-", 1)[1].split(" - name:", 1)[0]
|
||||
self.assertEqual(json.loads(config), [
|
||||
{"command": "query-regression", "permission": "admin", "issue_type": "pull-request"},
|
||||
{"command": "ci", "permission": "none", "issue_type": "pull-request"},
|
||||
])
|
||||
|
||||
def test_fork_requires_writer_even_for_author(self):
|
||||
for permission in ("read", "write", "maintain", "admin"):
|
||||
with self.subTest(permission=permission):
|
||||
Path(self.output.name).write_text("")
|
||||
responses = [
|
||||
{"body": "/ci", "issue_url": "https://api.github.test/issues/42", "user": {"login": "actor"}},
|
||||
{"state": "open", "draft": True, "user": {"login": "actor"}, "head": {"sha": "a" * 40, "ref": "fork-branch", "repo": {"full_name": "actor/greptimedb"}}},
|
||||
{"permission": permission},
|
||||
]
|
||||
if permission != "read":
|
||||
responses.append({"workflow_runs": [
|
||||
{"id": 7, "path": ".github/workflows/rust.yml", "head_sha": "a" * 40, "head_branch": "fork-branch", "head_repository": {"full_name": "actor/greptimedb"}, "status": "completed", "conclusion": "skipped"},
|
||||
{"id": 8, "path": ".github/workflows/checks.yml", "head_sha": "a" * 40, "head_branch": "fork-branch", "head_repository": {"full_name": "someone/greptimedb"}, "status": "completed", "conclusion": "skipped"},
|
||||
]})
|
||||
output = self.run_main(responses)
|
||||
if permission == "read":
|
||||
self.assertIn("skip=true", output)
|
||||
else:
|
||||
self.assertIn("run_ids=7\n", output)
|
||||
self.assertNotIn("head_ref=", output)
|
||||
|
||||
def test_fork_rejects_non_draft_original_run(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci rust", "issue_url": "https://api.github.test/issues/42", "user": {"login": "writer"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "ref": "fork-branch", "repo": {"full_name": "actor/greptimedb"}}},
|
||||
{"permission": "write"},
|
||||
{"workflow_runs": [{"id": 7, "path": ".github/workflows/rust.yml@main", "head_sha": "a" * 40, "head_branch": "fork-branch", "head_repository": {"full_name": "actor/greptimedb"}, "status": "completed", "conclusion": "success", "run_attempt": 2}]},
|
||||
{"conclusion": "success"},
|
||||
])
|
||||
self.assertIn("only originally skipped draft CI runs", output)
|
||||
self.assertIn("skip=true", output)
|
||||
|
||||
def test_author_cannot_bypass_pr_guards(self):
|
||||
for change, reason in [
|
||||
({"state": "closed"}, "PR must be open and draft"),
|
||||
({"draft": False}, "PR must be open and draft"),
|
||||
({"head": {"sha": "b" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}}, "PR head changed"),
|
||||
]:
|
||||
with self.subTest(change=change):
|
||||
Path(self.output.name).write_text("")
|
||||
pr = {"state": "open", "draft": True, "user": {"login": "actor"}, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}}
|
||||
pr.update(change)
|
||||
output = self.run_main([
|
||||
{"body": "/ci rust", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "actor"}},
|
||||
pr,
|
||||
])
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn(reason, output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -5,6 +5,7 @@ name: Cargo.lock Diff Check
|
||||
# posts/updates a PR comment when the threshold is exceeded.
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "Cargo.lock"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ name: Check Git Dependencies on Main Branch
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'Cargo.toml'
|
||||
|
||||
@@ -32,7 +32,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
check-typos-and-docs:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Check typos and docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|| (echo "'config/config.md' is not up-to-date, please run 'make config-docs'." && exit 1)
|
||||
|
||||
license-header-check:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Check License Header
|
||||
steps:
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
config: licenserc-enterprise.toml
|
||||
|
||||
github-script-tests:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: GitHub Script Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
python3 tests/perf/test_agent_observability_summary.py
|
||||
|
||||
check:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Check
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
run: cargo check --locked --workspace --all-targets --all-features
|
||||
|
||||
toml:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Toml Check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
@@ -30,23 +30,46 @@ jobs:
|
||||
DISPATCH_SENDER: ${{ github.event.sender.login }}
|
||||
DISPATCH_HEAD_SHA: ${{ github.event.client_payload.pull_request.head.sha }}
|
||||
run: python3 .github/scripts/ci-slash.py
|
||||
- name: Reply with command result
|
||||
if: ${{ steps.admit.outputs.reply != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPLY: ${{ steps.admit.outputs.reply }}
|
||||
PR_NUMBER: ${{ steps.admit.outputs.pr_number }}
|
||||
run: printf '%s\n' "${REPLY}" | gh pr comment "${PR_NUMBER}" --body-file -
|
||||
- name: Dispatch selected CI workflow
|
||||
id: dispatch
|
||||
if: ${{ steps.admit.outputs.skip == 'false' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_IDS: ${{ steps.admit.outputs.run_ids }}
|
||||
WORKFLOWS: ${{ steps.admit.outputs.workflow }}
|
||||
HEAD_SHA: ${{ steps.admit.outputs.head_sha }}
|
||||
HEAD_REF: ${{ steps.admit.outputs.head_ref }}
|
||||
FUZZ_PROFILE: ${{ steps.admit.outputs.fuzz_profile }}
|
||||
run: |
|
||||
if [[ -n "${RUN_IDS}" ]]; then
|
||||
IFS=, read -ra runs <<<"${RUN_IDS}"
|
||||
for run in "${runs[@]}"; do
|
||||
gh api --method POST "/repos/${GITHUB_REPOSITORY}/actions/runs/${run}/rerun"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
IFS=, read -ra workflows <<<"${WORKFLOWS}"
|
||||
for workflow in "${workflows[@]}"; do
|
||||
inputs=(-F 'inputs[ci_command]=true')
|
||||
if [[ "${workflow}" == 'integration.yml' ]]; then
|
||||
inputs+=(-f "inputs[fuzz_profile]=${FUZZ_PROFILE}")
|
||||
fi
|
||||
gh api --method POST "/repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/dispatches" \
|
||||
-f ref="${HEAD_SHA}" -F 'inputs[ci_command]=true' -f "inputs[fuzz_profile]=${FUZZ_PROFILE}"
|
||||
-f ref="${HEAD_REF}" "${inputs[@]}"
|
||||
done
|
||||
- name: Reply with command result
|
||||
if: ${{ !cancelled() && github.event.sender.login == 'github-actions[bot]' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPLY: ${{ steps.admit.outputs.reply }}
|
||||
PR_NUMBER: ${{ steps.admit.outputs.pr_number || github.event.client_payload.github.payload.issue.number }}
|
||||
ADMIT_OUTCOME: ${{ steps.admit.outcome }}
|
||||
DISPATCH_OUTCOME: ${{ steps.dispatch.outcome }}
|
||||
# Use the issue-comment endpoint with the existing issues: write permission.
|
||||
run: |
|
||||
[[ "${PR_NUMBER}" =~ ^[0-9]+$ ]] || exit 1
|
||||
if [[ "${ADMIT_OUTCOME}" == failure || "${DISPATCH_OUTCOME}" == failure ]]; then
|
||||
REPLY="CI trigger failed; some workflows may already have been requested. See ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID} for details."
|
||||
fi
|
||||
[[ -n "${REPLY}" ]] || exit 1
|
||||
gh api --method POST "/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
|
||||
-f "body=${REPLY}"
|
||||
|
||||
@@ -2,6 +2,7 @@ name: Check Dependencies
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches:
|
||||
- main
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ name: Docs CI
|
||||
|
||||
jobs:
|
||||
typos:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1 }}
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- uses: crate-ci/typos@v1.50.2
|
||||
|
||||
license-header-check:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1 }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Check License Header
|
||||
steps:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- uses: korandoru/hawkeye@v5
|
||||
|
||||
required-checks:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1 }}
|
||||
name: ${{ matrix.check }}
|
||||
runs-on: ubuntu-slim
|
||||
strategy:
|
||||
|
||||
@@ -2,6 +2,7 @@ name: Check Grafana Panels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
|
||||
@@ -43,7 +43,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Build GreptimeDB binaries
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
version: current
|
||||
|
||||
sqlness:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Sqlness Test (${{ matrix.mode.name }})
|
||||
needs: build
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
export-import-v2-e2e:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Export/Import V2 E2E Test
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
run-multi-lang-tests:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Run Multi-language SDK Tests
|
||||
needs: build
|
||||
uses: ./.github/workflows/run-multi-lang-tests.yml
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
artifact-name: bins
|
||||
|
||||
compat-updater-check:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) && (github.event_name == 'merge_group' || github.event_name == 'pull_request') }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) && (github.event_name == 'merge_group' || github.event_name == 'pull_request') }}
|
||||
name: Check compat version updater
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -32,7 +32,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
run: make fmt-check
|
||||
|
||||
clippy:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: make clippy
|
||||
|
||||
check-udeps:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
run: make check-udeps
|
||||
|
||||
check-riscv64:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Check riscv64 build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
run: cargo check --locked --workspace --all-targets --features servers/dashboard --target riscv64gc-unknown-linux-gnu
|
||||
|
||||
check-windows:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Check Windows build
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 60
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
run: cargo nextest run --locked --workspace -F dashboard --no-fail-fast
|
||||
|
||||
conflict-check:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
name: Check for conflict
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
uses: olivernybroe/action-conflict-finder@v4.0
|
||||
|
||||
test:
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && github.event_name != 'merge_group' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
if: ${{ github.repository == 'GreptimeTeam/greptimedb' && github.event_name != 'merge_group' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false || github.run_attempt > 1) }}
|
||||
runs-on: ubuntu-22.04-arm
|
||||
timeout-minutes: 60
|
||||
needs: [conflict-check, clippy, fmt, check-udeps]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: Slash Command Dispatch
|
||||
|
||||
# ChatOps front door: parse `/command` on PR comments, check admin
|
||||
# permission, and repository_dispatch to a per-command handler. Handlers
|
||||
# ChatOps front door: parse `/command` on PR comments, apply command-specific
|
||||
# permissions, and repository_dispatch to a per-command handler. Handlers
|
||||
# own allowlists, SHA admission, and the actual work. Same-repo dispatch
|
||||
# uses github.token with contents: write; GitHub starts the handler run
|
||||
# for GITHUB_TOKEN-created repository_dispatch events. Do not pass the
|
||||
# long-lived GH_PERSONAL_ACCESS_TOKEN into this third-party action.
|
||||
#
|
||||
# To add a command: list it under `commands` and add a workflow with
|
||||
# To add a command: add it to `config` and add a workflow with
|
||||
# `on.repository_dispatch.types: ["<command>-command"]`.
|
||||
|
||||
on:
|
||||
@@ -30,8 +30,17 @@ jobs:
|
||||
uses: peter-evans/slash-command-dispatch@9bdcd7914ec1b75590b790b844aa3b8eee7c683a # v5.0.2
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
permission: admin
|
||||
issue-type: pull-request
|
||||
commands: |
|
||||
query-regression
|
||||
ci
|
||||
# CI admission re-fetches the comment and checks author/member permissions.
|
||||
config: >-
|
||||
[
|
||||
{"command": "query-regression", "permission": "admin", "issue_type": "pull-request"},
|
||||
{"command": "ci", "permission": "none", "issue_type": "pull-request"}
|
||||
]
|
||||
- name: Report CI dispatch failure
|
||||
if: ${{ failure() && (github.event.comment.body == '/ci' || startsWith(github.event.comment.body, '/ci ')) }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
gh api --method POST "/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
|
||||
-f "body=CI command dispatch failed. See ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID} for details."
|
||||
|
||||
Reference in New Issue
Block a user