diff --git a/.github/workflows/octo-ci-status.yml b/.github/workflows/octo-ci-status.yml index 90c6d60..f9ac3a0 100644 --- a/.github/workflows/octo-ci-status.yml +++ b/.github/workflows/octo-ci-status.yml @@ -30,12 +30,23 @@ on: permissions: actions: read + contents: read jobs: notify: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - name: Checkout reusable-workflow scripts + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: Mininglamp-OSS/.github + ref: ${{ github.workflow_sha }} + path: _shared + sparse-checkout: scripts + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Check state change and notify Octo env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -46,119 +57,4 @@ jobs: RUN_ID: ${{ inputs.run_id }} RUN_URL: ${{ inputs.run_url }} PROJECT_GROUP_ID: ${{ inputs.project_group_id }} - run: | - python3 << 'PYEOF' - import os, json, urllib.request, urllib.error, sys - - conclusion = os.environ['CONCLUSION'] - # Cancelled runs are not real failures; skip silently - if conclusion == 'cancelled': - print('Cancelled run, skipping.') - sys.exit(0) - - repo = os.environ['REPO_NAME'] - wf_name = os.environ['WORKFLOW_NAME'] - run_url = os.environ['RUN_URL'] - proj_gid = os.environ['PROJECT_GROUP_ID'] - gh_token = os.environ['GITHUB_TOKEN'] - bot_token = os.environ['OCTO_BOT_TOKEN'] - - # Fetch recent completed runs on main branch - api_url = ( - f'https://api.github.com/repos/Mininglamp-OSS/{repo}/actions/runs' - f'?branch=main&status=completed&per_page=10' - ) - req = urllib.request.Request(api_url, headers={ - 'Authorization': f'Bearer {gh_token}', - 'Accept': 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - }) - try: - with urllib.request.urlopen(req, timeout=15) as r: - runs = json.load(r)['workflow_runs'] - except (urllib.error.HTTPError, urllib.error.URLError) as e: - print(f'ERROR: failed to fetch workflow runs: {e}') - sys.exit(1) - - # Filter to the same workflow by name - same_wf = [r for r in runs if r['name'] == wf_name] - - if not same_wf: - print('No matching runs found, skipping.') - sys.exit(0) - - # Use run_id to precisely identify the current run (avoids race conditions - # when two runs complete close together). - run_id = int(os.environ.get('RUN_ID', 0)) - matched = [r for r in same_wf if r['id'] == run_id] - if not matched: - print('WARN: run_id %s not found in recent runs window, falling back to same_wf[0]' % run_id) - current = matched[0] if matched else same_wf[0] - # Only consider runs created before current to avoid picking a later - # concurrent run as "previous" (which would flip alert/recovery semantics). - older = [r for r in same_wf - if r['id'] != current['id'] - and r['created_at'] < current['created_at']] - previous = older[0] if older else None - - curr_conclusion = current['conclusion'] - prev_conclusion = previous['conclusion'] if previous else None - - print(f'State: {prev_conclusion} → {curr_conclusion}') - - # Only act on state changes - if curr_conclusion == prev_conclusion: - print('No state change, silent.') - sys.exit(0) - - # Guard: first-ever run has no previous history — skip silently - if prev_conclusion is None: - print('First run detected (no previous history), skipping notification.') - sys.exit(0) - - # Determine message - if curr_conclusion == 'failure': - msg = ( - f'❌ [{repo}] main CI 挂了\n\n' - f'工作流:{wf_name}\n' - f'🔗 {run_url}' - ) - elif curr_conclusion == 'success' and prev_conclusion == 'failure': - msg = ( - f'✅ [{repo}] main CI 已恢复\n\n' - f'工作流:{wf_name}\n' - f'🔗 {run_url}' - ) - else: - print(f'Unhandled transition {prev_conclusion!r} → {curr_conclusion!r}, skipping.') - sys.exit(0) - - # Send to Octo IM - send_url = 'https://im.deepminer.com.cn/api/v1/bot/sendMessage' - headers = { - 'Authorization': f'Bearer {bot_token}', - 'Content-Type': 'application/json', - } - - failed = [] - - def send(group_id, message): - body = json.dumps({ - 'channel_id': group_id, - 'channel_type': 2, - 'payload': {'type': 1, 'content': message}, - }).encode() - req = urllib.request.Request(send_url, data=body, headers=headers, method='POST') - try: - with urllib.request.urlopen(req, timeout=15) as r: - print(f' → {group_id[:8]}... HTTP {r.status}') - except (urllib.error.HTTPError, urllib.error.URLError) as e: - print(f'ERROR: failed to send message to {group_id[:8]}...: {e}') - failed.append(group_id) - - # Push to ci-status group and the repo's project group - send('4ade985d984e432eb7fbdd0ad4f8118a', msg) - send(proj_gid, msg) - if failed: - sys.exit(1) - PYEOF + run: python3 _shared/scripts/octo_ci_status_notify.py diff --git a/.github/workflows/octo-issue-feed.yml b/.github/workflows/octo-issue-feed.yml index 32d5c08..45ebe4d 100644 --- a/.github/workflows/octo-issue-feed.yml +++ b/.github/workflows/octo-issue-feed.yml @@ -33,13 +33,24 @@ on: OCTO_BOT_TOKEN: required: true -permissions: {} +permissions: + contents: read jobs: notify: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - name: Checkout reusable-workflow scripts + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: Mininglamp-OSS/.github + ref: ${{ github.workflow_sha }} + path: _shared + sparse-checkout: scripts + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Send to Octo env: OCTO_BOT_TOKEN: ${{ secrets.OCTO_BOT_TOKEN }} @@ -51,47 +62,4 @@ jobs: ISSUE_LABELS: ${{ inputs.issue_labels }} EVENT_ACTION: ${{ inputs.event_action }} PROJECT_GROUP_ID: ${{ inputs.project_group_id }} - run: | - python3 << 'PYEOF' - import os, json, sys, urllib.request, urllib.error - - action = os.environ['EVENT_ACTION'] - emoji = {'opened': '🆕', 'closed': '✅', 'reopened': '🔄', 'labeled': '🏷️'}.get(action, 'ℹ️') - - try: - labels = json.loads(os.environ['ISSUE_LABELS']) - labels_part = ' · 🏷️ ' + ', '.join(labels) if labels else '' - except Exception: - labels_part = '' - - repo = os.environ['REPO_NAME'] - num = os.environ['ISSUE_NUMBER'] - title = os.environ['ISSUE_TITLE'] - url = os.environ['ISSUE_URL'] - author = os.environ['ISSUE_AUTHOR'] - - feed_msg = f"{emoji} [{repo}] Issue #{num} · {title}\n👤 {author}{labels_part}\n🔗 {url}" - proj_msg = f"{emoji} Issue #{num} · {title}\n👤 {author}{labels_part}\n🔗 {url}" - - api = 'https://im.deepminer.com.cn/api/v1/bot/sendMessage' - token = os.environ['OCTO_BOT_TOKEN'] - headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} - - failed = [] - - def send(group_id, msg): - body = json.dumps({'channel_id': group_id, 'channel_type': 2, - 'payload': {'type': 1, 'content': msg}}).encode() - req = urllib.request.Request(api, data=body, headers=headers, method='POST') - try: - with urllib.request.urlopen(req, timeout=15) as r: - print(f'→ {group_id[:8]}... {r.status}') - except (urllib.error.HTTPError, urllib.error.URLError) as e: - print(f'ERROR: failed to send message to {group_id[:8]}...: {e}') - failed.append(group_id) - - send('151a45970e1546afa9e947ac36a5c4e5', feed_msg) - send(os.environ['PROJECT_GROUP_ID'], proj_msg) - if failed: - sys.exit(1) - PYEOF + run: python3 _shared/scripts/octo_issue_feed_notify.py diff --git a/.github/workflows/octo-pr-feed.yml b/.github/workflows/octo-pr-feed.yml index d6cd00a..c7b09f8 100644 --- a/.github/workflows/octo-pr-feed.yml +++ b/.github/workflows/octo-pr-feed.yml @@ -42,13 +42,24 @@ on: OCTO_BOT_TOKEN: required: true -permissions: {} +permissions: + contents: read jobs: notify: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - name: Checkout reusable-workflow scripts + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: Mininglamp-OSS/.github + ref: ${{ github.workflow_sha }} + path: _shared + sparse-checkout: scripts + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Send to Octo env: OCTO_BOT_TOKEN: ${{ secrets.OCTO_BOT_TOKEN }} @@ -63,52 +74,4 @@ jobs: PR_CHANGED_FILES: ${{ inputs.pr_changed_files }} EVENT_ACTION: ${{ inputs.event_action }} PROJECT_GROUP_ID: ${{ inputs.project_group_id }} - run: | - python3 << 'PYEOF' - import os, json, sys, urllib.request, urllib.error - - action = os.environ['EVENT_ACTION'] - merged = os.environ['PR_MERGED'].lower() == 'true' - - if action == 'closed': - emoji = '🟢' if merged else '🔴' - else: - emoji = {'opened': '🔵', 'reopened': '🔄', - 'review_requested': '👀', 'ready_for_review': '✅'}.get(action, 'ℹ️') - - adds = int(os.environ.get('PR_ADDITIONS', 0) or 0) - dels = int(os.environ.get('PR_DELETIONS', 0) or 0) - files = int(os.environ.get('PR_CHANGED_FILES', 0) or 0) - stats_part = f' · +{adds} -{dels} · {files} files' if (adds or dels or files) else '' - - repo = os.environ['REPO_NAME'] - num = os.environ['PR_NUMBER'] - title = os.environ['PR_TITLE'] - url = os.environ['PR_URL'] - author = os.environ['PR_AUTHOR'] - - feed_msg = f"{emoji} [{repo}] PR #{num} · {title}\n👤 {author}{stats_part}\n🔗 {url}" - proj_msg = f"{emoji} PR #{num} · {title}\n👤 {author}{stats_part}\n🔗 {url}" - - api = 'https://im.deepminer.com.cn/api/v1/bot/sendMessage' - token = os.environ['OCTO_BOT_TOKEN'] - headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} - - failed = [] - - def send(group_id, msg): - body = json.dumps({'channel_id': group_id, 'channel_type': 2, - 'payload': {'type': 1, 'content': msg}}).encode() - req = urllib.request.Request(api, data=body, headers=headers, method='POST') - try: - with urllib.request.urlopen(req, timeout=15) as r: - print(f'→ {group_id[:8]}... {r.status}') - except (urllib.error.HTTPError, urllib.error.URLError) as e: - print(f'ERROR: failed to send message to {group_id[:8]}...: {e}') - failed.append(group_id) - - send('1c303c142e9840f2a9b46c10b0972e8d', feed_msg) - send(os.environ['PROJECT_GROUP_ID'], proj_msg) - if failed: - sys.exit(1) - PYEOF + run: python3 _shared/scripts/octo_pr_feed_notify.py diff --git a/scripts/octo_ci_status_notify.py b/scripts/octo_ci_status_notify.py new file mode 100644 index 0000000..63f2fb4 --- /dev/null +++ b/scripts/octo_ci_status_notify.py @@ -0,0 +1,113 @@ +import os, json, urllib.request, urllib.error, sys + +conclusion = os.environ['CONCLUSION'] +# Cancelled runs are not real failures; skip silently +if conclusion == 'cancelled': + print('Cancelled run, skipping.') + sys.exit(0) + +repo = os.environ['REPO_NAME'] +wf_name = os.environ['WORKFLOW_NAME'] +run_url = os.environ['RUN_URL'] +proj_gid = os.environ['PROJECT_GROUP_ID'] +gh_token = os.environ['GITHUB_TOKEN'] +bot_token = os.environ['OCTO_BOT_TOKEN'] + +# Fetch recent completed runs on main branch +api_url = ( + f'https://api.github.com/repos/Mininglamp-OSS/{repo}/actions/runs' + f'?branch=main&status=completed&per_page=10' +) +req = urllib.request.Request(api_url, headers={ + 'Authorization': f'Bearer {gh_token}', + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', +}) +try: + with urllib.request.urlopen(req, timeout=15) as r: + runs = json.load(r)['workflow_runs'] +except (urllib.error.HTTPError, urllib.error.URLError) as e: + print(f'ERROR: failed to fetch workflow runs: {e}') + sys.exit(1) + +# Filter to the same workflow by name +same_wf = [r for r in runs if r['name'] == wf_name] + +if not same_wf: + print('No matching runs found, skipping.') + sys.exit(0) + +# Use run_id to precisely identify the current run (avoids race conditions +# when two runs complete close together). +run_id = int(os.environ.get('RUN_ID', 0)) +matched = [r for r in same_wf if r['id'] == run_id] +if not matched: + print('WARN: run_id %s not found in recent runs window, falling back to same_wf[0]' % run_id) +current = matched[0] if matched else same_wf[0] +# Only consider runs created before current to avoid picking a later +# concurrent run as "previous" (which would flip alert/recovery semantics). +older = [r for r in same_wf + if r['id'] != current['id'] + and r['created_at'] < current['created_at']] +previous = older[0] if older else None + +curr_conclusion = current['conclusion'] +prev_conclusion = previous['conclusion'] if previous else None + +print(f'State: {prev_conclusion} → {curr_conclusion}') + +# Only act on state changes +if curr_conclusion == prev_conclusion: + print('No state change, silent.') + sys.exit(0) + +# Guard: first-ever run has no previous history — skip silently +if prev_conclusion is None: + print('First run detected (no previous history), skipping notification.') + sys.exit(0) + +# Determine message +if curr_conclusion == 'failure': + msg = ( + f'❌ [{repo}] main CI 挂了\n\n' + f'工作流:{wf_name}\n' + f'🔗 {run_url}' + ) +elif curr_conclusion == 'success' and prev_conclusion == 'failure': + msg = ( + f'✅ [{repo}] main CI 已恢复\n\n' + f'工作流:{wf_name}\n' + f'🔗 {run_url}' + ) +else: + print(f'Unhandled transition {prev_conclusion!r} → {curr_conclusion!r}, skipping.') + sys.exit(0) + +# Send to Octo IM +send_url = 'https://im.deepminer.com.cn/api/v1/bot/sendMessage' +headers = { + 'Authorization': f'Bearer {bot_token}', + 'Content-Type': 'application/json', +} + +failed = [] + +def send(group_id, message): + body = json.dumps({ + 'channel_id': group_id, + 'channel_type': 2, + 'payload': {'type': 1, 'content': message}, + }).encode() + req = urllib.request.Request(send_url, data=body, headers=headers, method='POST') + try: + with urllib.request.urlopen(req, timeout=15) as r: + print(f' → {group_id[:8]}... HTTP {r.status}') + except (urllib.error.HTTPError, urllib.error.URLError) as e: + print(f'ERROR: failed to send message to {group_id[:8]}...: {e}') + failed.append(group_id) + +# Push to ci-status group and the repo's project group +send('4ade985d984e432eb7fbdd0ad4f8118a', msg) +send(proj_gid, msg) +if failed: + sys.exit(1) diff --git a/scripts/octo_issue_feed_notify.py b/scripts/octo_issue_feed_notify.py new file mode 100644 index 0000000..1ae3807 --- /dev/null +++ b/scripts/octo_issue_feed_notify.py @@ -0,0 +1,41 @@ +import os, json, sys, urllib.request, urllib.error + +action = os.environ['EVENT_ACTION'] +emoji = {'opened': '🆕', 'closed': '✅', 'reopened': '🔄', 'labeled': '🏷️'}.get(action, 'ℹ️') + +try: + labels = json.loads(os.environ['ISSUE_LABELS']) + labels_part = ' · 🏷️ ' + ', '.join(labels) if labels else '' +except Exception: + labels_part = '' + +repo = os.environ['REPO_NAME'] +num = os.environ['ISSUE_NUMBER'] +title = os.environ['ISSUE_TITLE'] +url = os.environ['ISSUE_URL'] +author = os.environ['ISSUE_AUTHOR'] + +feed_msg = f"{emoji} [{repo}] Issue #{num} · {title}\n👤 {author}{labels_part}\n🔗 {url}" +proj_msg = f"{emoji} Issue #{num} · {title}\n👤 {author}{labels_part}\n🔗 {url}" + +api = 'https://im.deepminer.com.cn/api/v1/bot/sendMessage' +token = os.environ['OCTO_BOT_TOKEN'] +headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} + +failed = [] + +def send(group_id, msg): + body = json.dumps({'channel_id': group_id, 'channel_type': 2, + 'payload': {'type': 1, 'content': msg}}).encode() + req = urllib.request.Request(api, data=body, headers=headers, method='POST') + try: + with urllib.request.urlopen(req, timeout=15) as r: + print(f'→ {group_id[:8]}... {r.status}') + except (urllib.error.HTTPError, urllib.error.URLError) as e: + print(f'ERROR: failed to send message to {group_id[:8]}...: {e}') + failed.append(group_id) + +send('151a45970e1546afa9e947ac36a5c4e5', feed_msg) +send(os.environ['PROJECT_GROUP_ID'], proj_msg) +if failed: + sys.exit(1) diff --git a/scripts/octo_pr_feed_notify.py b/scripts/octo_pr_feed_notify.py new file mode 100644 index 0000000..7a52519 --- /dev/null +++ b/scripts/octo_pr_feed_notify.py @@ -0,0 +1,46 @@ +import os, json, sys, urllib.request, urllib.error + +action = os.environ['EVENT_ACTION'] +merged = os.environ['PR_MERGED'].lower() == 'true' + +if action == 'closed': + emoji = '🟢' if merged else '🔴' +else: + emoji = {'opened': '🔵', 'reopened': '🔄', + 'review_requested': '👀', 'ready_for_review': '✅'}.get(action, 'ℹ️') + +adds = int(os.environ.get('PR_ADDITIONS', 0) or 0) +dels = int(os.environ.get('PR_DELETIONS', 0) or 0) +files = int(os.environ.get('PR_CHANGED_FILES', 0) or 0) +stats_part = f' · +{adds} -{dels} · {files} files' if (adds or dels or files) else '' + +repo = os.environ['REPO_NAME'] +num = os.environ['PR_NUMBER'] +title = os.environ['PR_TITLE'] +url = os.environ['PR_URL'] +author = os.environ['PR_AUTHOR'] + +feed_msg = f"{emoji} [{repo}] PR #{num} · {title}\n👤 {author}{stats_part}\n🔗 {url}" +proj_msg = f"{emoji} PR #{num} · {title}\n👤 {author}{stats_part}\n🔗 {url}" + +api = 'https://im.deepminer.com.cn/api/v1/bot/sendMessage' +token = os.environ['OCTO_BOT_TOKEN'] +headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} + +failed = [] + +def send(group_id, msg): + body = json.dumps({'channel_id': group_id, 'channel_type': 2, + 'payload': {'type': 1, 'content': msg}}).encode() + req = urllib.request.Request(api, data=body, headers=headers, method='POST') + try: + with urllib.request.urlopen(req, timeout=15) as r: + print(f'→ {group_id[:8]}... {r.status}') + except (urllib.error.HTTPError, urllib.error.URLError) as e: + print(f'ERROR: failed to send message to {group_id[:8]}...: {e}') + failed.append(group_id) + +send('1c303c142e9840f2a9b46c10b0972e8d', feed_msg) +send(os.environ['PROJECT_GROUP_ID'], proj_msg) +if failed: + sys.exit(1)