Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .github/workflows/issue-welcome.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
jobs:
welcome:
runs-on: ubuntu-latest
timeout-minutes: 3
concurrency:
group: welcome-${{ github.event.issue.number }}
cancel-in-progress: false
permissions:
issues: write
steps:
Expand All @@ -15,13 +19,28 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issue = context.payload.issue;

if (!issue) {
core.setFailed('This workflow must be called from an issues event. context.payload.issue is undefined.');
return;
}
// Skip bot-opened issues (Dependabot, etc.)
if (issue.user.type === 'Bot') {
console.log('Skipping bot-opened issue');
return;
}

const marker = '<!-- octo-issue-welcome:v1 -->';
const existingComments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
if (existingComments.some(c => c.body && c.body.includes(marker))) {
console.log('Welcome comment already posted, skipping (idempotent).');
return;
}

// Language detection: Chinese characters in title or body → reply in Chinese
const text = (issue.title || '') + ' ' + (issue.body || '');
const isChinese = /[\u4e00-\u9fff]/.test(text);
Expand Down Expand Up @@ -113,5 +132,5 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body
body: marker + '\n' + body
});
52 changes: 40 additions & 12 deletions .github/workflows/octo-ci-status.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ on:
default: 0
workflow_id:
type: number
required: false
default: 0
description: 'Numeric workflow ID (strongly recommended). Without it, run history is fetched globally for the repo and may include other workflows. Pass ${{ github.workflow_run.workflow_id }} from caller.'
required: true
description: 'Numeric workflow ID. Pass ${{ github.event.workflow_run.workflow_id }} from caller.'
run_url:
type: string
required: true
Expand All @@ -33,7 +32,7 @@ on:
type: string
required: false
default: 'https://im.deepminer.com.cn/api'
description: 'Octo IM API base URL (without trailing slash)'
description: 'Octo IM API base URL. Only the production endpoint is allowlisted; any other value will cause the workflow to fail.'
secrets:
OCTO_BOT_TOKEN:
required: true
Expand All @@ -60,7 +59,7 @@ jobs:
API_BASE_URL: ${{ inputs.api_base_url }}
run: |
python3 - << 'PYEOF'
import os, json, time, urllib.request, urllib.error, sys
import os, json, re, time, urllib.request, urllib.error, sys


def require_env(name):
Expand All @@ -69,6 +68,24 @@ jobs:
print(f'ERROR: required environment variable {name} is missing or empty')
sys.exit(2)
return val


def require_group_id(name):
"""Validate format only (32-char hex); this is not an authorization check."""
val = require_env(name)
if not re.fullmatch(r'[0-9a-f]{32}', val):
print(f'ERROR: {name} must be a 32-char lowercase hex group id')
sys.exit(2)
return val


def require_repo_name(name):
"""Validate repo name to prevent path traversal in GitHub API URLs."""
val = require_env(name)
if not re.fullmatch(r'[A-Za-z0-9._-]{1,100}', val):
print(f'ERROR: {name} contains invalid characters: {val!r}')
sys.exit(2)
return val


conclusion = require_env('CONCLUSION')
Expand All @@ -77,10 +94,10 @@ jobs:
print('Cancelled run, skipping.')
sys.exit(0)

repo = require_env('REPO_NAME')
repo = require_repo_name('REPO_NAME')
wf_name = require_env('WORKFLOW_NAME')
run_url = require_env('RUN_URL')
proj_gid = require_env('PROJECT_GROUP_ID')
proj_gid = require_group_id('PROJECT_GROUP_ID')
gh_token = require_env('GITHUB_TOKEN')
bot_token = require_env('OCTO_BOT_TOKEN')

Expand Down Expand Up @@ -139,10 +156,14 @@ jobs:
curr_created = current['created_at']

# Previous: any run older than current (by created_at), same workflow name
older = [r for r in all_runs
if r['id'] != current['id']
and r['name'] == wf_name # defensive; redundant when workflow_id is set (already scoped)
and r['created_at'] < curr_created]
older = sorted(
[r for r in all_runs
if r['id'] != current['id']
and r['name'] == wf_name # defensive; redundant when workflow_id is set (already scoped)
and r['created_at'] < curr_created],
key=lambda r: r['created_at'],
reverse=True,
)
previous = older[0] if older else None

curr_conclusion = current['conclusion']
Expand Down Expand Up @@ -186,7 +207,14 @@ jobs:
sys.exit(0)

# Send to Octo IM
send_url = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + '/v1/bot/sendMessage'
ALLOWED_API_BASES = {
'https://im.deepminer.com.cn/api',
}
_api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/')
if _api_base.lower() not in {b.lower() for b in ALLOWED_API_BASES}:
print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}')
sys.exit(2)
send_url = _api_base + '/v1/bot/sendMessage'
headers = {
'Authorization': f'Bearer {bot_token}',
'Content-Type': 'application/json',
Expand Down
50 changes: 42 additions & 8 deletions .github/workflows/octo-issue-feed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ on:
type: string
required: false
default: 'https://im.deepminer.com.cn/api'
description: 'Octo IM API base URL (without trailing slash)'
description: 'Octo IM API base URL. Only the production endpoint is allowlisted; any other value will cause the workflow to fail.'
secrets:
OCTO_BOT_TOKEN:
required: true
Expand All @@ -59,7 +59,7 @@ jobs:
API_BASE_URL: ${{ inputs.api_base_url }}
run: |
python3 - << 'PYEOF'
import os, json, sys, time, urllib.request, urllib.error
import os, json, re, sys, time, urllib.request, urllib.error


def require_env(name):
Expand All @@ -70,25 +70,59 @@ jobs:
return val


def sanitize_text(s, max_len=300):
"""Strip control characters (CR, LF, tabs, etc.) to prevent IM message injection."""
s = str(s or '')
# Strip all C0 control characters (U+0000-U+001F) and DEL (U+007F)
s = re.sub(r'[\x00-\x1f\x7f]', ' ', s)
return s[:max_len]


def require_group_id(name):
"""Validate format only (32-char hex); this is not an authorization check."""
val = require_env(name)
if not re.fullmatch(r'[0-9a-f]{32}', val):
print(f'ERROR: {name} must be a 32-char lowercase hex group id')
sys.exit(2)
return val


def require_repo_name(name):
"""Validate repo name to prevent path traversal in GitHub API URLs."""
val = require_env(name)
if not re.fullmatch(r'[A-Za-z0-9._-]{1,100}', val):
print(f'ERROR: {name} contains invalid characters: {val!r}')
sys.exit(2)
return val


action = require_env('EVENT_ACTION')
emoji = {'opened': '🆕', 'closed': '✅', 'reopened': '🔄', 'labeled': '🏷️'}.get(action, 'ℹ️')

try:
labels = json.loads(os.environ.get('ISSUE_LABELS', '[]'))
labels = [sanitize_text(l, max_len=64) for l in json.loads(os.environ.get('ISSUE_LABELS', '[]'))]
labels_part = ' · 🏷️ ' + ', '.join(labels) if labels else ''
except Exception:
labels_part = ''

repo = require_env('REPO_NAME')
repo = require_repo_name('REPO_NAME')
num = require_env('ISSUE_NUMBER')
title = require_env('ISSUE_TITLE')
title = sanitize_text(require_env('ISSUE_TITLE'), max_len=300)
url = require_env('ISSUE_URL')
author = require_env('ISSUE_AUTHOR')
author = sanitize_text(require_env('ISSUE_AUTHOR'), max_len=80)
proj_gid = require_group_id('PROJECT_GROUP_ID')

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 = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + '/v1/bot/sendMessage'
ALLOWED_API_BASES = {
'https://im.deepminer.com.cn/api',
}
_api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/')
if _api_base.lower() not in {b.lower() for b in ALLOWED_API_BASES}:
print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}')
sys.exit(2)
api = _api_base + '/v1/bot/sendMessage'
token = require_env('OCTO_BOT_TOKEN')
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}

Expand Down Expand Up @@ -133,12 +167,12 @@ jobs:
print(f'ERROR: failed to send message to {group_id[:8]}...: {last_err}')
failed.append(group_id)

proj_gid = require_env('PROJECT_GROUP_ID')
send('151a45970e1546afa9e947ac36a5c4e5', feed_msg)
send(proj_gid, proj_msg)

# Trigger IssueTriage bot on newly opened issues
if action == 'opened':
# repo is already validated by require_repo_name; safe to interpolate into URL
triage_msg = (
'@[27pmzxx8nad78c9d01e_bot:Octo 助理-IssueTriage] [TRIAGE] '
f'https://github.com/Mininglamp-OSS/{repo}/issues/{num}'
Expand Down
49 changes: 41 additions & 8 deletions .github/workflows/octo-pr-feed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ on:
type: string
required: false
default: 'https://im.deepminer.com.cn/api'
description: 'Octo IM API base URL (without trailing slash)'
description: 'Octo IM API base URL. Only the production endpoint is allowlisted; any other value will cause the workflow to fail.'
feed_group_id:
type: string
required: true
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
API_BASE_URL: ${{ inputs.api_base_url }}
run: |
python3 - << 'PYEOF'
import os, json, sys, time, urllib.request, urllib.error
import os, json, re, sys, time, urllib.request, urllib.error


def require_env(name):
Expand All @@ -87,6 +87,32 @@ jobs:
return val


def sanitize_text(s, max_len=300):
"""Strip control characters (CR, LF, tabs, etc.) to prevent IM message injection."""
s = str(s or '')
# Strip all C0 control characters (U+0000-U+001F) and DEL (U+007F)
s = re.sub(r'[\x00-\x1f\x7f]', ' ', s)
return s[:max_len]


def require_group_id(name):
"""Validate format only (32-char hex); this is not an authorization check."""
val = require_env(name)
if not re.fullmatch(r'[0-9a-f]{32}', val):
print(f'ERROR: {name} must be a 32-char lowercase hex group id')
sys.exit(2)
return val


def require_repo_name(name):
"""Validate repo name to prevent path traversal in GitHub API URLs."""
val = require_env(name)
if not re.fullmatch(r'[A-Za-z0-9._-]{1,100}', val):
print(f'ERROR: {name} contains invalid characters: {val!r}')
sys.exit(2)
return val


action = require_env('EVENT_ACTION')
merged = require_env('PR_MERGED').lower() == 'true'

Expand All @@ -101,16 +127,23 @@ jobs:
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 = require_env('REPO_NAME')
repo = require_repo_name('REPO_NAME')
num = require_env('PR_NUMBER')
title = require_env('PR_TITLE')
title = sanitize_text(require_env('PR_TITLE'), max_len=300)
url = require_env('PR_URL')
author = require_env('PR_AUTHOR')
author = sanitize_text(require_env('PR_AUTHOR'), max_len=80)

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 = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + '/v1/bot/sendMessage'
ALLOWED_API_BASES = {
'https://im.deepminer.com.cn/api',
}
_api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/')
if _api_base.lower() not in {b.lower() for b in ALLOWED_API_BASES}:
print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}')
sys.exit(2)
api = _api_base + '/v1/bot/sendMessage'
token = require_env('OCTO_BOT_TOKEN')
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}

Expand Down Expand Up @@ -155,8 +188,8 @@ jobs:
print(f'ERROR: failed to send message to {group_id[:8]}...: {last_err}')
failed.append(group_id)

feed_gid = require_env('FEED_GROUP_ID')
proj_gid = require_env('PROJECT_GROUP_ID')
feed_gid = require_group_id('FEED_GROUP_ID')
proj_gid = require_group_id('PROJECT_GROUP_ID')
send(feed_gid, feed_msg)
send(proj_gid, proj_msg)
if failed:
Expand Down