diff --git a/.github/workflows/octo-issue-notify.yml b/.github/workflows/octo-issue-notify.yml deleted file mode 100644 index 7b7f37d..0000000 --- a/.github/workflows/octo-issue-notify.yml +++ /dev/null @@ -1,164 +0,0 @@ -# Reusable workflow: notify Octo IM when an issue is opened, and forward the -# event to the triage agent (optional). -# -# Refactored (Wave 2) to the two-stage shape: this workflow does the MAPPING -# (sanitize + format the IM message, build the webhook JSON), and delegates -# TRANSPORT to the shared `octo-notify` composite action (POST + retry/backoff, -# IM allowlist). See docs/workflow-architecture.md §4a + Plane 6. -# -# Event policy: only `opened` is forwarded (unified IM + webhook trigger). The -# guard below enforces this centrally even if a caller still subscribes to -# `reopened`/others. (This narrows the old [opened, reopened] behavior — intended.) -# -# Triage webhook (optional): pass the TRIAGE_WEBHOOK_URL secret to forward the -# event to the triage agent, which classifies the issue (labels + a triage -# comment). The URL is a capability secret — never hardcode it; pass from a -# secret. The agent fetches issue details itself via the GitHub API using -# repo + number, so the body is NOT egressed through Actions. -# -# This is the canonical reusable for issue notification + triage. (It replaced -# the legacy octo-issue-feed.yml, whose back-compat shim was removed in v1.1.0 -# once all callers migrated to octo-issue-notify.yml@v1.) -name: Octo Issue Notify (reusable) - -on: - workflow_call: - inputs: - repo_name: - type: string - required: true - issue_number: - type: number - required: true - issue_title: - type: string - required: true - issue_url: - type: string - required: true - issue_author: - type: string - required: true - event_action: - type: string - required: true - api_base_url: - type: string - required: false - default: 'https://im.deepminer.com.cn/api' - description: 'Octo IM API base URL. Only the production endpoint is allowlisted.' - feed_group_id: - type: string - required: false - default: '151a45970e1546afa9e947ac36a5c4e5' - description: 'Octo IM group ID for the central issue feed channel.' - enable_im_notify: - type: boolean - required: false - default: true - description: >- - When false, skip the Octo IM push and only fire the triage webhook. - Default true preserves existing behavior for all current callers; opt - in by passing `enable_im_notify: false` when a repo has already moved - to an external triage agent and no longer wants duplicate IM pings. - secrets: - OCTO_BOT_TOKEN: - required: true - TRIAGE_WEBHOOK_URL: - required: false - description: >- - Triage agent webhook URL (capability secret). When provided, the issue - event is forwarded to the agent for classification. Omit to disable. - -permissions: {} - -jobs: - notify: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Build IM message and webhook payload - id: build - env: - REPO_NAME: ${{ inputs.repo_name }} - ISSUE_NUMBER: ${{ inputs.issue_number }} - ISSUE_TITLE: ${{ inputs.issue_title }} - ISSUE_URL: ${{ inputs.issue_url }} - ISSUE_AUTHOR: ${{ inputs.issue_author }} - EVENT_ACTION: ${{ inputs.event_action }} - run: | - python3 - <<'PYEOF' - import os, json, re, sys - - OUT = os.environ['GITHUB_OUTPUT'] - DELIM = '__OCTO_NOTIFY_EOF_9f3a__' - - def emit(key, value): - with open(OUT, 'a') as f: - f.write(f'{key}<<{DELIM}\n{value}\n{DELIM}\n') - - def require_env(name): - val = os.environ.get(name, '').strip() - if not val: - print(f'ERROR: required environment variable {name} is missing or empty') - sys.exit(2) - return val - - def sanitize_text(s, max_len=300): - """Strip control characters to prevent IM message injection.""" - s = str(s or '') - s = re.sub(r'[\x00-\x1f\x7f]', ' ', s) - return s[:max_len] - - def require_repo_name(name): - """Validate repo name to prevent path traversal / injection.""" - val = require_env(name) - if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,99}', val) or val in {'.', '..'}: - print(f'ERROR: {name} contains invalid characters: {val!r}') - sys.exit(2) - return val - - action = require_env('EVENT_ACTION') - - # Unified event policy: only `opened` triggers IM + triage. - if action != 'opened': - print(f'Skipping event_action={action!r} — only opened is forwarded.') - emit('skip', 'true') - sys.exit(0) - - repo = require_repo_name('REPO_NAME') - num = require_env('ISSUE_NUMBER') - title = sanitize_text(require_env('ISSUE_TITLE'), max_len=300) - url = require_env('ISSUE_URL') - author = sanitize_text(require_env('ISSUE_AUTHOR'), max_len=80) - - # IM message (mapping stays here; transport is octo-notify) - im_message = f"🆕 [{repo}] Issue #{num} · {title}\n👤 {author}\n🔗 {url}" - - # Webhook payload for the triage agent. Minimal identifying fields — - # the agent fetches full issue details via the GitHub API itself, so - # the issue body is not egressed through Actions. - webhook_payload = json.dumps({ - 'repo': repo, - 'number': int(num), - 'title': title, - 'url': url, - 'author': author, - 'action': action, - }) - - emit('skip', 'false') - emit('im_message', im_message) - emit('webhook_payload', webhook_payload) - PYEOF - - - name: Send (IM feed + optional triage webhook) - if: steps.build.outputs.skip != 'true' - uses: Mininglamp-OSS/.github/.github/actions/octo-notify@v1 - with: - im-message: ${{ inputs.enable_im_notify && steps.build.outputs.im_message || '' }} - im-token: ${{ secrets.OCTO_BOT_TOKEN }} - im-group-id: ${{ inputs.feed_group_id }} - im-api-base: ${{ inputs.api_base_url }} - webhook-url: ${{ secrets.TRIAGE_WEBHOOK_URL }} - webhook-payload: ${{ steps.build.outputs.webhook_payload }} diff --git a/.github/workflows/octo-pr-result-notify.yml b/.github/workflows/octo-pr-result-notify.yml deleted file mode 100644 index d866f12..0000000 --- a/.github/workflows/octo-pr-result-notify.yml +++ /dev/null @@ -1,178 +0,0 @@ -name: Octo PR Result Notify (reusable) - -# Refactored (Wave 2): MAPPING (validate + sanitize + format message) stays -# here; TRANSPORT delegated to the shared octo-notify composite action. -# See docs/workflow-architecture.md Plane 6. - -on: - workflow_call: - inputs: - repo_name: - type: string - required: true - pr_number: - type: number - required: true - pr_title: - type: string - required: true - pr_url: - type: string - required: true - pr_author: - type: string - required: true - event_kind: - type: string - required: true - reviewer: - type: string - required: false - default: '' - pr_additions: - type: number - required: false - default: 0 - pr_deletions: - type: number - required: false - default: 0 - pr_changed_files: - type: number - required: false - default: 0 - feed_group_id: - type: string - required: false - default: '1c303c142e9840f2a9b46c10b0972e8d' - api_base_url: - type: string - required: false - default: 'https://im.deepminer.com.cn/api' - secrets: - OCTO_BOT_TOKEN: - required: true - -permissions: {} - -jobs: - notify: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Build result message - id: build - env: - REPO_NAME: ${{ inputs.repo_name }} - PR_NUMBER: ${{ inputs.pr_number }} - PR_TITLE: ${{ inputs.pr_title }} - PR_URL: ${{ inputs.pr_url }} - PR_AUTHOR: ${{ inputs.pr_author }} - EVENT_KIND: ${{ inputs.event_kind }} - REVIEWER: ${{ inputs.reviewer }} - PR_ADDITIONS: ${{ inputs.pr_additions }} - PR_DELETIONS: ${{ inputs.pr_deletions }} - PR_CHANGED_FILES: ${{ inputs.pr_changed_files }} - FEED_GROUP_ID: ${{ inputs.feed_group_id }} - run: | - python3 - <<'PYEOF' - import os, re, sys - - OUT = os.environ['GITHUB_OUTPUT'] - DELIM = '__OCTO_NOTIFY_EOF_9f3a__' - - def emit(key, value): - with open(OUT, 'a') as f: - f.write(f'{key}<<{DELIM}\n{value}\n{DELIM}\n') - - ALLOWED_KINDS = { - "pr_merged", "pr_closed", - "review_approved", "review_changes_requested", - } - - def require_env(name): - value = os.environ.get(name, "").strip() - if not value: - print(f"::error::Missing required env var: {name}") - sys.exit(1) - return value - - def get_env(name, default=""): - return os.environ.get(name, default).strip() - - def require_group_id(name): - val = os.environ.get(name, '').strip() - if not re.fullmatch(r'[0-9a-f]{32}', val or ''): - print(f'ERROR: {name} must be a 32-char lowercase hex group id') - sys.exit(2) - return val - - def require_repo_name(name): - val = os.environ.get(name, '').strip() - if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,99}', val or '') or val in {'.', '..'}: - print(f'ERROR: {name} contains invalid characters: {val!r}') - sys.exit(2) - return val - - def sanitize_text(s, max_len=300): - s = str(s or '') - s = re.sub(r'[\x00-\x1f\x7f]', ' ', s) - return s[:max_len] - - repo_name = require_repo_name('REPO_NAME') - pr_number = require_env("PR_NUMBER") - pr_title = sanitize_text(require_env("PR_TITLE")) - pr_url = require_env("PR_URL") - pr_author = sanitize_text(require_env('PR_AUTHOR'), max_len=80) - event_kind = require_env("EVENT_KIND") - reviewer = sanitize_text(get_env('REVIEWER'), max_len=80) - reviewer_display = reviewer if reviewer else "(unknown)" - pr_additions = int(get_env("PR_ADDITIONS", "0") or "0") - pr_deletions = int(get_env("PR_DELETIONS", "0") or "0") - pr_changed_files = int(get_env("PR_CHANGED_FILES", "0") or "0") - # Validate group id format (transport allowlists the API base itself). - require_group_id("FEED_GROUP_ID") - - if event_kind not in ALLOWED_KINDS: - print(f"::warning::Unrecognized event_kind '{event_kind}', skipping") - emit('skip', 'true') - sys.exit(0) - - if event_kind == "pr_merged": - stats_part = f' · +{pr_additions} -{pr_deletions} · {pr_changed_files} files' if (pr_additions or pr_deletions or pr_changed_files) else '' - message = ( - f"\U0001f7e2 [{repo_name}] PR #{pr_number} merged · {pr_title}\n" - f"\U0001f464 {pr_author}{stats_part}\n" - f"\U0001f517 {pr_url}" - ) - elif event_kind == "pr_closed": - message = ( - f"\U0001f534 [{repo_name}] PR #{pr_number} closed · {pr_title}\n" - f"\U0001f464 {pr_author}\n" - f"\U0001f517 {pr_url}" - ) - elif event_kind == "review_approved": - message = ( - f"✅ [{repo_name}] PR #{pr_number} approved · {pr_title}\n" - f"\U0001f464 reviewer: {reviewer_display}\n" - f"\U0001f517 {pr_url}" - ) - elif event_kind == "review_changes_requested": - message = ( - f"\U0001f501 [{repo_name}] PR #{pr_number} changes requested · {pr_title}\n" - f"\U0001f464 reviewer: {reviewer_display}\n" - f"\U0001f517 {pr_url}" - ) - - emit('skip', 'false') - emit('im_message', message) - PYEOF - - - name: Send to Octo IM - if: steps.build.outputs.skip != 'true' - uses: Mininglamp-OSS/.github/.github/actions/octo-notify@v1 - with: - im-message: ${{ steps.build.outputs.im_message }} - im-token: ${{ secrets.OCTO_BOT_TOKEN }} - im-group-id: ${{ inputs.feed_group_id }} - im-api-base: ${{ inputs.api_base_url }} diff --git a/.github/workflows/octo-pr-review-feed.yml b/.github/workflows/octo-pr-review-feed.yml deleted file mode 100644 index 7a1a483..0000000 --- a/.github/workflows/octo-pr-review-feed.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: Octo PR Review Feed (reusable) - -# Refactored (Wave 2): MAPPING stays here; TRANSPORT delegated to octo-notify. -# See docs/workflow-architecture.md Plane 6. - -on: - workflow_call: - inputs: - repo_name: - type: string - required: true - pr_number: - type: number - required: true - pr_title: - type: string - required: true - pr_url: - type: string - required: true - pr_author: - type: string - required: true - event_action: - type: string - required: true - project_group_id: - type: string - required: true - api_base_url: - type: string - required: false - default: 'https://im.deepminer.com.cn/api' - enable_im_notify: - type: boolean - required: false - default: true - description: >- - When false, skip the Octo IM push and only fire the PR triage webhook. - Default true preserves existing behavior for all current callers; opt - in by passing `enable_im_notify: false` when a repo has already moved - to an external PR triage agent and no longer wants duplicate IM pings. - secrets: - OCTO_BOT_TOKEN: - required: true - PR_TRIAGE_WEBHOOK_URL: - required: false - description: >- - PR review triage agent webhook URL (capability secret). When provided, - the PR ready_for_review / review_requested event is forwarded to the - agent for fan-out review dispatch. Omit to disable. - -permissions: {} - -jobs: - notify: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Build review message - id: build - env: - REPO_NAME: ${{ inputs.repo_name }} - PR_NUMBER: ${{ inputs.pr_number }} - PR_TITLE: ${{ inputs.pr_title }} - PR_URL: ${{ inputs.pr_url }} - PR_AUTHOR: ${{ inputs.pr_author }} - EVENT_ACTION: ${{ inputs.event_action }} - PROJECT_GROUP_ID: ${{ inputs.project_group_id }} - run: | - python3 - <<'PYEOF' - import os, re, sys - - OUT = os.environ['GITHUB_OUTPUT'] - DELIM = '__OCTO_NOTIFY_EOF_9f3a__' - - def emit(key, value): - with open(OUT, 'a') as f: - f.write(f'{key}<<{DELIM}\n{value}\n{DELIM}\n') - - ALLOWED_ACTIONS = {"ready_for_review", "review_requested"} - - def require_env(name): - value = os.environ.get(name, "").strip() - if not value: - print(f"::error::Missing required env var: {name}") - sys.exit(1) - return value - - def require_group_id(name): - val = os.environ.get(name, '').strip() - if not re.fullmatch(r'[0-9a-f]{32}', val or ''): - print(f'ERROR: {name} must be a 32-char lowercase hex group id') - sys.exit(2) - return val - - def require_repo_name(name): - val = os.environ.get(name, '').strip() - if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,99}', val or '') or val in {'.', '..'}: - print(f'ERROR: {name} contains invalid characters: {val!r}') - sys.exit(2) - return val - - def sanitize_text(s, max_len=300): - s = str(s or '') - s = re.sub(r'[\x00-\x1f\x7f]', ' ', s) - return s[:max_len] - - repo_name = require_repo_name('REPO_NAME') - pr_number = require_env("PR_NUMBER") - pr_title = sanitize_text(require_env("PR_TITLE")) - pr_url = require_env("PR_URL") - pr_author = sanitize_text(require_env('PR_AUTHOR'), max_len=80) - event_action = require_env("EVENT_ACTION") - require_group_id("PROJECT_GROUP_ID") - - if event_action not in ALLOWED_ACTIONS: - print(f"::warning::Unrecognized event_action '{event_action}', skipping") - emit('skip', 'true') - sys.exit(0) - - emoji = {"ready_for_review": "✅", "review_requested": "\U0001f440"}[event_action] - - message = ( - f"{emoji} [{repo_name}] PR #{pr_number} · {pr_title}\n" - f"\U0001f464 {pr_author}\n" - f"\U0001f517 {pr_url}" - ) - - import json - webhook_payload = json.dumps({ - 'repo': repo_name, - 'number': int(pr_number), - 'title': pr_title, - 'url': pr_url, - 'author': pr_author, - 'action': event_action, - }) - - emit('skip', 'false') - emit('im_message', message) - emit('webhook_payload', webhook_payload) - PYEOF - - - name: Send (IM feed + optional triage webhook) - if: steps.build.outputs.skip != 'true' - uses: Mininglamp-OSS/.github/.github/actions/octo-notify@v1 - with: - im-message: ${{ inputs.enable_im_notify && steps.build.outputs.im_message || '' }} - im-token: ${{ secrets.OCTO_BOT_TOKEN }} - im-group-id: ${{ inputs.project_group_id }} - im-api-base: ${{ inputs.api_base_url }} - webhook-url: ${{ secrets.PR_TRIAGE_WEBHOOK_URL }} - webhook-payload: ${{ steps.build.outputs.webhook_payload }}