From 74feeb71cc971eae6c2abc13c7ca9b56775a49f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=BA=E6=BE=88?= Date: Fri, 3 Apr 2026 17:07:01 +0800 Subject: [PATCH 1/3] feat(ci): add changelog generation workflow and PR lint gate --- .github/pull_request_template.md | 14 ++- .github/workflows/changelog.yml | 158 +++++++++++++++++++++++++ .github/workflows/prelint.yml | 194 +++++++++++++++++++++++++++++++ cliff.toml | 73 ++++++++++++ 4 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/changelog.yml create mode 100644 .github/workflows/prelint.yml create mode 100644 cliff.toml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 36b8d95..e86d3a5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,9 +4,19 @@ ## Related Issue - + + +closes # ## Type of Change diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..a0ff464 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,158 @@ +############################################################################### +# CHANGELOG semi-automatic generation +# +# Spec: +# Uses git-cliff to generate changelogs. A single shared cliff.toml is +# maintained at the repository root. The --include-path flag scopes commits +# to each sub-project. Generated output is reviewed by maintainers before +# merging into a release PR. +# +# Usage: +# Trigger manually via workflow_dispatch. Select a scope, the previous tag, +# and optionally the new release tag to label the section header. +############################################################################### + +name: πŸ“ Generate CHANGELOG + +on: + workflow_dispatch: + inputs: + scope: + description: 'Component scope' + required: true + type: choice + options: + - cosh + - agent-sec-core + - agentsight + - os-skills + previous_tag: + description: 'Previous tag (e.g. cosh/v2.0.0). Leave empty to regenerate full changelog.' + required: false + type: string + release_tag: + description: 'New release tag (e.g. cosh/v2.1.0). Used to label the changelog section.' + required: false + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + generate: + name: πŸ“ Generate CHANGELOG + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install git-cliff + run: | + # Pin to a specific version for reproducible builds. + # Update this value intentionally when upgrading git-cliff. + VERSION="2.4.0" + echo "Installing git-cliff v${VERSION}..." + curl -sSfL "https://github.com/orhun/git-cliff/releases/download/v${VERSION}/git-cliff-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \ + | tar -xz + sudo mv "git-cliff-${VERSION}/git-cliff" /usr/local/bin/ + git-cliff --version + + - name: Determine paths and options + id: config + run: | + SCOPE="${{ inputs.scope }}" + PREV_TAG="${{ inputs.previous_tag }}" + REL_TAG="${{ inputs.release_tag }}" + + case "$SCOPE" in + cosh) + INCLUDE_PATH="src/copilot-shell/**" + OUTPUT_PATH="src/copilot-shell/CHANGELOG.md" + ;; + agent-sec-core) + INCLUDE_PATH="src/agent-sec-core/**" + OUTPUT_PATH="src/agent-sec-core/CHANGELOG.md" + ;; + agentsight) + INCLUDE_PATH="src/agentsight/**" + OUTPUT_PATH="src/agentsight/CHANGELOG.md" + ;; + os-skills) + INCLUDE_PATH="src/os-skills/**" + OUTPUT_PATH="src/os-skills/CHANGELOG.md" + ;; + esac + + echo "include_path=$INCLUDE_PATH" >> $GITHUB_OUTPUT + echo "output_path=$OUTPUT_PATH" >> $GITHUB_OUTPUT + echo "scope=$SCOPE" >> $GITHUB_OUTPUT + + if [ -n "$PREV_TAG" ]; then + echo "range=${PREV_TAG}..HEAD" >> $GITHUB_OUTPUT + # Incremental update: prepend the new section to the existing file + echo "write_mode=prepend" >> $GITHUB_OUTPUT + else + echo "range=" >> $GITHUB_OUTPUT + # Full history regeneration: overwrite the file + echo "write_mode=overwrite" >> $GITHUB_OUTPUT + fi + + if [ -n "$REL_TAG" ]; then + echo "tag_flag=--tag $REL_TAG" >> $GITHUB_OUTPUT + else + echo "tag_flag=" >> $GITHUB_OUTPUT + fi + + - name: Generate CHANGELOG + run: | + RANGE="${{ steps.config.outputs.range }}" + INCLUDE="${{ steps.config.outputs.include_path }}" + OUTPUT="${{ steps.config.outputs.output_path }}" + WRITE_MODE="${{ steps.config.outputs.write_mode }}" + TAG_FLAG="${{ steps.config.outputs.tag_flag }}" + + echo "πŸ“ Generating CHANGELOG..." + echo " Scope: ${{ steps.config.outputs.scope }}" + echo " Range: ${RANGE:-'(full history)'}" + echo " Include: $INCLUDE" + echo " Output: $OUTPUT" + echo " Write mode: $WRITE_MODE" + + if [ "$WRITE_MODE" = "prepend" ]; then + git cliff $RANGE $TAG_FLAG \ + --include-path "$INCLUDE" \ + --prepend "$OUTPUT" + else + git cliff $TAG_FLAG \ + --include-path "$INCLUDE" \ + -o "$OUTPUT" + fi + + echo "### Generated CHANGELOG" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + head -50 "$OUTPUT" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: Create PR + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "chore(${{ steps.config.outputs.scope }}): update CHANGELOG" + title: "chore(${{ steps.config.outputs.scope }}): update CHANGELOG" + body: | + ## πŸ“ CHANGELOG Auto-generated + + **Scope**: `${{ steps.config.outputs.scope }}` + **Range**: `${{ steps.config.outputs.range || '(full history)' }}` + **Release tag**: `${{ inputs.release_tag || '(none β€” section labeled Unreleased)' }}` + **Include path**: `${{ steps.config.outputs.include_path }}` + + --- + + > πŸ€– Generated by CHANGELOG Generator workflow. + > Please review the CHANGELOG content before merging. + branch: chore/changelog-${{ steps.config.outputs.scope }} + delete-branch: true + labels: docs diff --git a/.github/workflows/prelint.yml b/.github/workflows/prelint.yml new file mode 100644 index 0000000..cadaf5d --- /dev/null +++ b/.github/workflows/prelint.yml @@ -0,0 +1,194 @@ +############################################################################### +# PR Lint gate β€” Commit Message + PR Title + Branch Name + Issue Link +# +# Jobs: +# commit-lint β€” requires checkout; runs independently +# pr-checks β€” all github-script checks consolidated into one runner +# (PR title, branch name, linked issue) +############################################################################### + +name: πŸ” PR Lint + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [main, 'feature/**', 'release/**'] + +permissions: + contents: read + pull-requests: read + +jobs: + # ========================================================================= + # Job 1: Commit Message Lint + # ========================================================================= + commit-lint: + name: πŸ“ Commit Message Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Lint commit messages + uses: wagoid/commitlint-github-action@v6 + with: + configFile: .github/commitlint.config.json + + # ========================================================================= + # Job 2: PR Checks (title + branch name + linked issue + merge hint) + # All steps share one runner β€” no checkout needed. + # ========================================================================= + pr-checks: + name: πŸ” PR Checks + runs-on: ubuntu-latest + steps: + # ----------------------------------------------------------------------- + # Step 1: PR Title Validation + # ----------------------------------------------------------------------- + - name: πŸ“‹ Validate PR title + uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title; + console.log(`PR Title: "${title}"`); + + // Conventional Commits pattern: type(scope): description + // Optional ! for breaking changes + const pattern = /^(feat|fix|refactor|perf|docs|chore|test|ci|build|style|revert)(\(.+\))!?: .+/; + + if (!pattern.test(title)) { + core.setFailed( + `❌ PR title does not follow Conventional Commits format.\n\n` + + `Expected: type(scope): description\n` + + `Examples: feat(cosh): add json output\n` + + ` fix(agent-sec-core): handle sandbox escape\n\n` + + `Valid types: feat, fix, refactor, perf, docs, chore, test, ci, build, style, revert\n` + + `Valid scopes: cosh, agent-sec-core, os-skills, agentsight, deps, ci, docs, chore\n\n` + + `Current title: "${title}"` + ); + return; + } + + // Validate scope + const scopeMatch = title.match(/^\w+\(([^)]+)\)/); + if (scopeMatch) { + const scope = scopeMatch[1]; + const validScopes = [ + 'cosh', 'agent-sec-core', 'os-skills', 'agentsight', + 'deps', 'ci', 'docs', 'chore' + ]; + if (!validScopes.includes(scope)) { + core.setFailed( + `❌ Scope "${scope}" in PR title is not in the allowed list.\n\n` + + `Valid scopes: ${validScopes.join(', ')}\n\n` + + `Current title: "${title}"` + ); + return; + } + } + + console.log('βœ… PR title format is valid'); + + # ----------------------------------------------------------------------- + # Step 2: Branch Name Validation + # ----------------------------------------------------------------------- + - name: 🌿 Validate branch name + uses: actions/github-script@v7 + with: + script: | + const branch = context.payload.pull_request.head.ref; + const baseBranch = context.payload.pull_request.base.ref; + console.log(`Head branch: "${branch}"`); + console.log(`Base branch: "${baseBranch}"`); + + // Whitelist: these branches are always allowed + const whitelist = [ + /^main$/, + /^dependabot\/.+/, + /^renovate\/.+/, + /^revert-.+/, + /^chore\/changelog-.+/, // created by changelog.yml workflow + ]; + + for (const pattern of whitelist) { + if (pattern.test(branch)) { + console.log(`βœ… Branch "${branch}" is whitelisted`); + return; + } + } + + // Valid scopes β€” sub-projects and project-wide categories + const validScopes = ['cosh', 'agent-sec-core', 'os-skills', 'agentsight', 'ci', 'docs', 'deps']; + const scopePattern = validScopes.join('|'); + + // Valid branch patterns per development spec + const validPatterns = [ + // feature// + new RegExp(`^feature/(${scopePattern})/[a-z0-9][a-z0-9._-]*$`), + // feature/// (collaborative track) + new RegExp(`^feature/(${scopePattern})/[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$`), + // fix// + new RegExp(`^fix/(${scopePattern})/[a-z0-9][a-z0-9._-]*$`), + // release//vX.Y + new RegExp(`^release/(${scopePattern})/v\\d+\\.\\d+$`), + // hotfix// + new RegExp(`^hotfix/(${scopePattern})/[a-z0-9][a-z0-9._-]*$`), + ]; + + const isValid = validPatterns.some(p => p.test(branch)); + + if (!isValid) { + core.setFailed( + `❌ Branch "${branch}" does not follow the naming convention.\n\n` + + `Valid formats:\n` + + ` feature// e.g. feature/cosh/json-output\n` + + ` feature/// e.g. feature/cosh/auth-refactor/oauth\n` + + ` fix// e.g. fix/agent-sec-core/sandbox-escape\n` + + ` release//vX.Y e.g. release/cosh/v2.1\n` + + ` hotfix// e.g. hotfix/os-skills/broken-load\n\n` + + `Valid scopes: ${validScopes.join(', ')}\n` + + `Branch names must use lowercase letters, digits, hyphens, and dots only.` + ); + return; + } + + console.log('βœ… Branch name is valid'); + + # ----------------------------------------------------------------------- + # Step 3: Linked Issue Check + # ----------------------------------------------------------------------- + - name: πŸ”— Check linked issue + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ''; + + // Accept standard GitHub closing keywords linking to an issue number + const closingKeywords = /\b(closes|fixes|resolves|close|fix|resolve)\s+#\d+/i; + + // Accept explicit exemption: no-issue: + const noIssueExemption = /no-issue\s*:/i; + + if (closingKeywords.test(body)) { + const match = body.match(/(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i); + console.log(`βœ… PR is linked to issue #${match[1]}`); + return; + } + + if (noIssueExemption.test(body)) { + const exemptMatch = body.match(/no-issue\s*:\s*(.+)/i); + console.log(`βœ… Issue link exempted: ${exemptMatch?.[1]?.trim() || '(no reason given)'}`); + return; + } + + core.setFailed( + `❌ This PR does not reference a linked issue.\n\n` + + `Every PR must include one of the following in the PR description:\n\n` + + ` closes #\n` + + ` fixes #\n` + + ` resolves #\n\n` + + `If there is genuinely no applicable issue, add:\n` + + ` no-issue: \n\n` + + `Please create an issue first at https://github.com/alibaba/anolisa/issues/new` + ); diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..b79ce19 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,73 @@ +# ============================================================================= +# git-cliff shared configuration β€” used by all ANOLISA sub-projects +# ============================================================================= +# +# Usage (copilot-shell example): +# git cliff cosh/v2.0.0..HEAD \ +# --include-path "src/copilot-shell/**" \ +# --prepend src/copilot-shell/CHANGELOG.md +# +# Commit type β†’ CHANGELOG section mapping: +# feat β†’ Added +# fix β†’ Fixed +# refactor, perf β†’ Changed +# feat! / BREAKING β†’ Breaking Changes +# docs, chore, test, ci β†’ excluded +# ============================================================================= + +[changelog] +header = """# Changelog\n +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n +""" +body = """ +{%- macro remote_url() -%} + https://github.com/alibaba/anolisa +{%- endmacro -%} + +{% if version -%} +## [{{ version | split(pat="/") | last | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} +## [Unreleased] +{% endif -%} + +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} +{% for commit in commits %} +- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | upper_first }}\ +{% endfor %} +{% endfor -%} +""" +footer = "" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false + +commit_parsers = [ + # Breaking changes must come before feat/fix to take priority + { message = "^feat!|^fix!|^refactor!", group = "Breaking Changes" }, + { body = "BREAKING CHANGE", group = "Breaking Changes" }, + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^refactor", group = "Changed" }, + { message = "^perf", group = "Changed" }, + { message = "^revert", group = "Reverted" }, + # Excluded from CHANGELOG + { message = "^doc", skip = true }, + { message = "^docs", skip = true }, + { message = "^chore", skip = true }, + { message = "^test", skip = true }, + { message = "^ci", skip = true }, + { message = "^build", skip = true }, + { message = "^style", skip = true }, +] + +protect_breaking_commits = true +filter_commits = true +tag_pattern = ".+/v[0-9].*" +sort_commits = "newest" \ No newline at end of file From 0ae928d9352cecb89c1cc633f56b34ca2bd96d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=BA=E6=BE=88?= Date: Sat, 4 Apr 2026 09:55:40 +0800 Subject: [PATCH 2/3] feat(ci): add and harden GitHub Actions CI governance workflows --- .github/CODEOWNERS | 24 ++++ .github/commitlint.config.json | 32 ++++- .github/maintainers.json | 46 +++++++ .github/workflows/ci.yaml | 16 +-- .github/workflows/issue-automation.yml | 69 +++++++++++ .github/workflows/issue-triage.yml | 160 +++++++++++++++++++++++++ 6 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/maintainers.json create mode 100644 .github/workflows/issue-automation.yml create mode 100644 .github/workflows/issue-triage.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a126dd0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,24 @@ +# ============================================================================= +# CODEOWNERS β€” automatic review request on pull requests +# +# GitHub requests a review from the listed owners whenever a PR touches +# the matched path. Combined with Branch Protection "Require review from +# Code Owners", this enforces sub-project ownership. +# +# Syntax: <@github-user> ... +# Patterns are evaluated top-to-bottom; the LAST matching rule wins. +# ============================================================================= + +# Global fallback β€” catches anything not matched below +* @casparant + +# Sub-project owners +/src/copilot-shell/ @kongche-jbw @samchu-zsl +/src/agent-sec-core/ @1570005763 +/src/os-skills/ @Ziqi002 +/src/agentsight/ @chengshuyi + +# CI / project-wide config β€” always require lead review +/.github/ @kongche-jbw +/cliff.toml @samchu-zsl +/Makefile @samchu-zsl diff --git a/.github/commitlint.config.json b/.github/commitlint.config.json index 55d4159..64e68ec 100644 --- a/.github/commitlint.config.json +++ b/.github/commitlint.config.json @@ -1,8 +1,30 @@ { - "extends": ["@commitlint/config-conventional"], + "extends": [ + "@commitlint/config-conventional" + ], "rules": { - "scope-enum": [2, "always", ["cosh", "agent-sec-core", "os-skills", "agentsight", "deps", "ci", "docs"]], - "scope-empty": [2, "never"], - "header-max-length": [2, "always", 120] + "scope-enum": [ + 2, + "always", + [ + "cosh", + "agent-sec-core", + "os-skills", + "agentsight", + "deps", + "ci", + "docs", + "chore" + ] + ], + "scope-empty": [ + 2, + "never" + ], + "header-max-length": [ + 2, + "always", + 120 + ] } -} +} \ No newline at end of file diff --git a/.github/maintainers.json b/.github/maintainers.json new file mode 100644 index 0000000..bb1ea8b --- /dev/null +++ b/.github/maintainers.json @@ -0,0 +1,46 @@ +{ + "scopes": [ + { + "label": "component:cosh", + "maintainers": [ + { + "github": "kongche-jbw" + }, + { + "github": "samchu-zsl" + } + ] + }, + { + "label": "component:sec-core", + "maintainers": [ + { + "github": "1570005763" + } + ] + }, + { + "label": "component:skill", + "maintainers": [ + { + "github": "Ziqi002" + } + ] + }, + { + "label": "component:sight", + "maintainers": [ + { + "github": "chengshuyi" + } + ] + } + ], + "default": { + "maintainers": [ + { + "github": "kongche-jbw" + } + ] + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 19bb600..155a29f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, 'release/**'] pull_request: - branches: [main] + branches: [main, 'release/**'] workflow_dispatch: inputs: run_copilot_shell: @@ -32,7 +32,7 @@ jobs: # ========================================================================= detect-changes: name: Detect Changes - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: copilot_shell: ${{ steps.changes.outputs.copilot_shell }} agent_sec_core: ${{ steps.changes.outputs.agent_sec_core }} @@ -101,7 +101,7 @@ jobs: name: Build copilot-shell needs: detect-changes if: needs.detect-changes.outputs.copilot_shell == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -145,7 +145,7 @@ jobs: name: Test copilot-shell/cli needs: [detect-changes, build-copilot-shell] if: needs.detect-changes.outputs.copilot_shell == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -175,7 +175,7 @@ jobs: name: Test copilot-shell/core needs: [detect-changes, build-copilot-shell] if: needs.detect-changes.outputs.copilot_shell == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -208,7 +208,7 @@ jobs: name: Test agent-sec-core needs: detect-changes if: needs.detect-changes.outputs.agent_sec_core == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -249,7 +249,7 @@ jobs: name: Test agentsight needs: detect-changes if: needs.detect-changes.outputs.agentsight == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/issue-automation.yml b/.github/workflows/issue-automation.yml new file mode 100644 index 0000000..985d929 --- /dev/null +++ b/.github/workflows/issue-automation.yml @@ -0,0 +1,69 @@ +############################################################################### +# Issue Automation β€” Parse Issue Form and apply component label +# +# Triggered when a new issue is opened. +# Reads the "Component" dropdown from the Issue Form body and maps it to a +# component:xxx label. The label addition then triggers issue-triage.yml, +# which handles assignee routing and notifications. +############################################################################### + +name: πŸ€– Issue Automation + +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + auto-label: + name: 🏷️ Auto Label + runs-on: ubuntu-22.04 + + steps: + # ----------------------------------------------------------------------- + # Parse Issue Form body β†’ map Component dropdown value to a label + # ----------------------------------------------------------------------- + - name: πŸ” Parse component and apply label + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.issue.body || ''; + + // Skip if a component label is already present (e.g. applied via API or Issue Form) + const existingLabels = context.payload.issue.labels.map(l => l.name); + const alreadyLabeled = existingLabels.some(l => l.startsWith('component:')); + if (alreadyLabeled) { + console.log(`Issue already has a component label: ${existingLabels.filter(l => l.startsWith('component:')).join(', ')} β€” skipping.`); + return; + } + + // Issue Form renders dropdown fields as: "### FieldName\n\nValue" + const componentMatch = body.match(/###\s*Component\s*\n\n(.+)/i); + const component = componentMatch ? componentMatch[1].trim() : ''; + + console.log(`Parsed component: "${component}"`); + + // Map dropdown display value β†’ label name + const labelMap = { + 'copilot-shell (cosh)': 'component:cosh', + 'agent-sec-core': 'component:sec-core', + 'os-skills': 'component:skill', + 'agentsight': 'component:sight', + }; + + const label = labelMap[component]; + if (!label) { + console.log('No matching component label found β€” skipping.'); + return; + } + + console.log(`Applying label: ${label}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [label], + }); + // Label addition will trigger issue-triage.yml for assign + notify diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 0000000..f765658 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,160 @@ +############################################################################### +# Issue Triage β€” Assign maintainers and send notifications +# +# Triggered when a component:xxx label is added to an issue. +# Works for both auto-labeling (via issue-automation.yml) and manual labeling. +# +# Maintainer routing is configured in .github/maintainers.json. +# No checkout or external tools required β€” config is fetched via GitHub API. +############################################################################### + +name: πŸ”€ Issue Triage + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + assign-and-notify: + name: πŸ“Œ Assign & Notify Maintainers + runs-on: ubuntu-22.04 + if: startsWith(github.event.label.name, 'component:') + + steps: + # ----------------------------------------------------------------------- + # Fetch maintainers.json via API, resolve maintainers, assign + comment + # No checkout or yq needed. + # ----------------------------------------------------------------------- + - name: πŸ” Resolve, assign and comment + id: triage + uses: actions/github-script@v7 + with: + script: | + const label = context.payload.label.name; + const scopeName = label.replace('component:', ''); + + // Fetch maintainers.json from the repository via API + const { data } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/maintainers.json', + }); + const config = JSON.parse(Buffer.from(data.content, 'base64').toString()); + + // Resolve maintainers for this label, fall back to default + const scope = config.scopes.find(s => s.label === label); + const entries = scope ? scope.maintainers : config.default.maintainers; + const maintainers = entries.map(e => e.github); + + if (maintainers.length === 0) { + console.log('No maintainers configured β€” skipping.'); + return; + } + + console.log(`Assigning: ${maintainers.join(', ')}`); + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + assignees: maintainers, + }); + + const mentionList = maintainers.map(m => `@${m}`).join(' '); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + `## πŸ”€ Issue Triage`, + ``, + `This issue has been automatically assigned to **${mentionList}** ` + + `as the maintainer(s) of \`${scopeName}\`.`, + ``, + `> Maintainers, please review and triage this issue. ` + + `Set a priority label and update the status as needed. Thanks! πŸ™`, + ].join('\n'), + }); + + core.setOutput('maintainers', maintainers.join(',')); + + # ----------------------------------------------------------------------- + # Email notification (skipped if MAIL_TO / MAIL_SERVER secrets are absent) + # ----------------------------------------------------------------------- + - name: πŸ“§ Check email configuration + if: steps.triage.outputs.maintainers != '' + id: mail-check + env: + MAIL_TO: ${{ secrets.MAIL_TO }} + MAIL_SERVER: ${{ secrets.MAIL_SERVER }} + run: | + if [ -n "$MAIL_TO" ] && [ -n "$MAIL_SERVER" ]; then + echo "mail_configured=true" >> "$GITHUB_OUTPUT" + else + echo "Email secrets not configured β€” skipping" + fi + + - name: πŸ“§ Send email notification + if: steps.mail-check.outputs.mail_configured == 'true' + uses: dawidd6/action-send-mail@v3 + with: + server_address: ${{ secrets.MAIL_SERVER }} + server_port: ${{ secrets.MAIL_PORT }} + secure: true + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + subject: "[ANOLISA] New Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }}" + to: ${{ secrets.MAIL_TO }} + from: "ANOLISA Bot <${{ secrets.MAIL_USERNAME }}>" + body: | + # New Issue Notification + + **Repository**: ${{ github.repository }} + **Issue**: #${{ github.event.issue.number }} + **Title**: ${{ github.event.issue.title }} + **Label**: ${{ github.event.label.name }} + **Assigned to**: ${{ steps.triage.outputs.maintainers }} + **Opened by**: ${{ github.event.issue.user.login }} + + **Link**: ${{ github.event.issue.html_url }} + + > This email was sent automatically by the ANOLISA Issue Triage Bot. + convert_markdown: true + + # ----------------------------------------------------------------------- + # DingTalk Webhook notification (skipped if secrets are absent) + # ----------------------------------------------------------------------- + - name: πŸ”” Send DingTalk notification + if: steps.triage.outputs.maintainers != '' + env: + DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }} + DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} + run: | + if [ -z "$DINGTALK_WEBHOOK" ] || [ -z "$DINGTALK_SECRET" ]; then + echo "DingTalk webhook not configured β€” skipping" + exit 0 + fi + + TIMESTAMP=$(date +%s%3N) + STRING_TO_SIGN="${TIMESTAMP}\n${DINGTALK_SECRET}" + SIGN=$(echo -ne "$STRING_TO_SIGN" \ + | openssl dgst -sha256 -hmac "$DINGTALK_SECRET" -binary \ + | base64) + ENCODED_SIGN=$(python3 -c "import urllib.parse; print(urllib.parse.quote_plus('$SIGN'))") + WEBHOOK_URL="${DINGTALK_WEBHOOK}×tamp=${TIMESTAMP}&sign=${ENCODED_SIGN}" + + curl -s -X POST "$WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d @- < πŸ€– ANOLISA Issue Triage Bot" + } + } + EOF + echo "DingTalk notification sent" From 4fc72fd8e53278b09dfea538073f6810cb30e89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=BA=E6=BE=88?= Date: Sat, 4 Apr 2026 20:33:51 +0800 Subject: [PATCH 3/3] chore(ci): add PR merged notification workflow --- .github/workflows/changelog.yml | 158 -------------------------------- .github/workflows/pr-merged.yml | 69 ++++++++++++++ cliff.toml | 73 --------------- 3 files changed, 69 insertions(+), 231 deletions(-) delete mode 100644 .github/workflows/changelog.yml create mode 100644 .github/workflows/pr-merged.yml delete mode 100644 cliff.toml diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml deleted file mode 100644 index a0ff464..0000000 --- a/.github/workflows/changelog.yml +++ /dev/null @@ -1,158 +0,0 @@ -############################################################################### -# CHANGELOG semi-automatic generation -# -# Spec: -# Uses git-cliff to generate changelogs. A single shared cliff.toml is -# maintained at the repository root. The --include-path flag scopes commits -# to each sub-project. Generated output is reviewed by maintainers before -# merging into a release PR. -# -# Usage: -# Trigger manually via workflow_dispatch. Select a scope, the previous tag, -# and optionally the new release tag to label the section header. -############################################################################### - -name: πŸ“ Generate CHANGELOG - -on: - workflow_dispatch: - inputs: - scope: - description: 'Component scope' - required: true - type: choice - options: - - cosh - - agent-sec-core - - agentsight - - os-skills - previous_tag: - description: 'Previous tag (e.g. cosh/v2.0.0). Leave empty to regenerate full changelog.' - required: false - type: string - release_tag: - description: 'New release tag (e.g. cosh/v2.1.0). Used to label the changelog section.' - required: false - type: string - -permissions: - contents: write - pull-requests: write - -jobs: - generate: - name: πŸ“ Generate CHANGELOG - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install git-cliff - run: | - # Pin to a specific version for reproducible builds. - # Update this value intentionally when upgrading git-cliff. - VERSION="2.4.0" - echo "Installing git-cliff v${VERSION}..." - curl -sSfL "https://github.com/orhun/git-cliff/releases/download/v${VERSION}/git-cliff-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \ - | tar -xz - sudo mv "git-cliff-${VERSION}/git-cliff" /usr/local/bin/ - git-cliff --version - - - name: Determine paths and options - id: config - run: | - SCOPE="${{ inputs.scope }}" - PREV_TAG="${{ inputs.previous_tag }}" - REL_TAG="${{ inputs.release_tag }}" - - case "$SCOPE" in - cosh) - INCLUDE_PATH="src/copilot-shell/**" - OUTPUT_PATH="src/copilot-shell/CHANGELOG.md" - ;; - agent-sec-core) - INCLUDE_PATH="src/agent-sec-core/**" - OUTPUT_PATH="src/agent-sec-core/CHANGELOG.md" - ;; - agentsight) - INCLUDE_PATH="src/agentsight/**" - OUTPUT_PATH="src/agentsight/CHANGELOG.md" - ;; - os-skills) - INCLUDE_PATH="src/os-skills/**" - OUTPUT_PATH="src/os-skills/CHANGELOG.md" - ;; - esac - - echo "include_path=$INCLUDE_PATH" >> $GITHUB_OUTPUT - echo "output_path=$OUTPUT_PATH" >> $GITHUB_OUTPUT - echo "scope=$SCOPE" >> $GITHUB_OUTPUT - - if [ -n "$PREV_TAG" ]; then - echo "range=${PREV_TAG}..HEAD" >> $GITHUB_OUTPUT - # Incremental update: prepend the new section to the existing file - echo "write_mode=prepend" >> $GITHUB_OUTPUT - else - echo "range=" >> $GITHUB_OUTPUT - # Full history regeneration: overwrite the file - echo "write_mode=overwrite" >> $GITHUB_OUTPUT - fi - - if [ -n "$REL_TAG" ]; then - echo "tag_flag=--tag $REL_TAG" >> $GITHUB_OUTPUT - else - echo "tag_flag=" >> $GITHUB_OUTPUT - fi - - - name: Generate CHANGELOG - run: | - RANGE="${{ steps.config.outputs.range }}" - INCLUDE="${{ steps.config.outputs.include_path }}" - OUTPUT="${{ steps.config.outputs.output_path }}" - WRITE_MODE="${{ steps.config.outputs.write_mode }}" - TAG_FLAG="${{ steps.config.outputs.tag_flag }}" - - echo "πŸ“ Generating CHANGELOG..." - echo " Scope: ${{ steps.config.outputs.scope }}" - echo " Range: ${RANGE:-'(full history)'}" - echo " Include: $INCLUDE" - echo " Output: $OUTPUT" - echo " Write mode: $WRITE_MODE" - - if [ "$WRITE_MODE" = "prepend" ]; then - git cliff $RANGE $TAG_FLAG \ - --include-path "$INCLUDE" \ - --prepend "$OUTPUT" - else - git cliff $TAG_FLAG \ - --include-path "$INCLUDE" \ - -o "$OUTPUT" - fi - - echo "### Generated CHANGELOG" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - head -50 "$OUTPUT" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: Create PR - uses: peter-evans/create-pull-request@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "chore(${{ steps.config.outputs.scope }}): update CHANGELOG" - title: "chore(${{ steps.config.outputs.scope }}): update CHANGELOG" - body: | - ## πŸ“ CHANGELOG Auto-generated - - **Scope**: `${{ steps.config.outputs.scope }}` - **Range**: `${{ steps.config.outputs.range || '(full history)' }}` - **Release tag**: `${{ inputs.release_tag || '(none β€” section labeled Unreleased)' }}` - **Include path**: `${{ steps.config.outputs.include_path }}` - - --- - - > πŸ€– Generated by CHANGELOG Generator workflow. - > Please review the CHANGELOG content before merging. - branch: chore/changelog-${{ steps.config.outputs.scope }} - delete-branch: true - labels: docs diff --git a/.github/workflows/pr-merged.yml b/.github/workflows/pr-merged.yml new file mode 100644 index 0000000..03fb363 --- /dev/null +++ b/.github/workflows/pr-merged.yml @@ -0,0 +1,69 @@ +# PR Merged Notification +# ====================== +# Sends a DingTalk notification when a pull request is merged into main. +# Requires secrets: DINGTALK_WEBHOOK, DINGTALK_SECRET + +name: PR Merged Notification + +on: + pull_request: + types: [closed] + branches: [main] + +permissions: + contents: read + +jobs: + notify: + name: DingTalk Notification + if: github.event.pull_request.merged == true + runs-on: ubuntu-22.04 + env: + DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }} + DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} + + steps: + - name: πŸ“£ Send DingTalk message + if: env.DINGTALK_WEBHOOK != '' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + python3 << 'PYEOF' + import hmac, hashlib, base64, urllib.parse, time, os, subprocess, json + + ts = str(round(time.time() * 1000)) + secret = os.environ.get('DINGTALK_SECRET', '') + string_to_sign = ts + '\n' + secret + sig = hmac.new(secret.encode(), string_to_sign.encode(), hashlib.sha256).digest() + sign = urllib.parse.quote_plus(base64.b64encode(sig).decode()) + + webhook = os.environ.get('DINGTALK_WEBHOOK', '') + pr_number = os.environ.get('PR_NUMBER', '') + pr_title = os.environ.get('PR_TITLE', '') + pr_url = os.environ.get('PR_URL', '') + pr_author = os.environ.get('PR_AUTHOR', '') + pr_branch = os.environ.get('PR_BRANCH', '') + + text = ( + f"## βœ… ANOLISA PR Merged\n\n" + f"**PR**: [#{pr_number} {pr_title}]({pr_url})\n\n" + f"**Branch**: `{pr_branch}` β†’ `main`\n\n" + f"**Author**: {pr_author}\n\n" + f"> πŸ€– ANOLISA CI Bot" + ) + + payload = json.dumps({ + "msgtype": "markdown", + "markdown": {"title": f"PR #{pr_number} Merged", "text": text} + }) + + url = f"{webhook}×tamp={ts}&sign={sign}" + subprocess.run( + ["curl", "-s", "-X", "POST", url, "-H", "Content-Type: application/json", "-d", payload], + check=True + ) + PYEOF diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index b79ce19..0000000 --- a/cliff.toml +++ /dev/null @@ -1,73 +0,0 @@ -# ============================================================================= -# git-cliff shared configuration β€” used by all ANOLISA sub-projects -# ============================================================================= -# -# Usage (copilot-shell example): -# git cliff cosh/v2.0.0..HEAD \ -# --include-path "src/copilot-shell/**" \ -# --prepend src/copilot-shell/CHANGELOG.md -# -# Commit type β†’ CHANGELOG section mapping: -# feat β†’ Added -# fix β†’ Fixed -# refactor, perf β†’ Changed -# feat! / BREAKING β†’ Breaking Changes -# docs, chore, test, ci β†’ excluded -# ============================================================================= - -[changelog] -header = """# Changelog\n -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n -""" -body = """ -{%- macro remote_url() -%} - https://github.com/alibaba/anolisa -{%- endmacro -%} - -{% if version -%} -## [{{ version | split(pat="/") | last | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else -%} -## [Unreleased] -{% endif -%} - -{% for group, commits in commits | group_by(attribute="group") %} -### {{ group | upper_first }} -{% for commit in commits %} -- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | upper_first }}\ -{% endfor %} -{% endfor -%} -""" -footer = "" -trim = true - -[git] -conventional_commits = true -filter_unconventional = true -split_commits = false - -commit_parsers = [ - # Breaking changes must come before feat/fix to take priority - { message = "^feat!|^fix!|^refactor!", group = "Breaking Changes" }, - { body = "BREAKING CHANGE", group = "Breaking Changes" }, - { message = "^feat", group = "Added" }, - { message = "^fix", group = "Fixed" }, - { message = "^refactor", group = "Changed" }, - { message = "^perf", group = "Changed" }, - { message = "^revert", group = "Reverted" }, - # Excluded from CHANGELOG - { message = "^doc", skip = true }, - { message = "^docs", skip = true }, - { message = "^chore", skip = true }, - { message = "^test", skip = true }, - { message = "^ci", skip = true }, - { message = "^build", skip = true }, - { message = "^style", skip = true }, -] - -protect_breaking_commits = true -filter_commits = true -tag_pattern = ".+/v[0-9].*" -sort_commits = "newest" \ No newline at end of file