From dce43a29e165f6105a90d19fbfa8541893da15d3 Mon Sep 17 00:00:00 2001 From: MarkoAleksandric Date: Thu, 30 Apr 2026 16:59:04 +0200 Subject: [PATCH] add triage Co-authored-by: Copilot --- .../ISSUE_TEMPLATE/extensibility-request.yaml | 2 + .../argus-extensibility-triage-feedback.yml | 156 ++++++++++++++++++ .../argus-extensibility-triage-scheduled.yml | 151 +++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 .github/workflows/argus-extensibility-triage-feedback.yml create mode 100644 .github/workflows/argus-extensibility-triage-scheduled.yml diff --git a/.github/ISSUE_TEMPLATE/extensibility-request.yaml b/.github/ISSUE_TEMPLATE/extensibility-request.yaml index e72f9251c6..03c2de7bfa 100644 --- a/.github/ISSUE_TEMPLATE/extensibility-request.yaml +++ b/.github/ISSUE_TEMPLATE/extensibility-request.yaml @@ -13,6 +13,8 @@ body: πŸ“– [How to structure the requests](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/create-extensibility-request) πŸ“– [Types of events by value](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/types-of-events-for-extensibility) πŸ“– [Min. requirements for IsHandled events](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-use-ishandled-min-req) + + ⚠️Note: Requests submitted through this form are analyzed by AI to support faster triage and more consistent routing. - type: textarea id: explaining-the-need diff --git a/.github/workflows/argus-extensibility-triage-feedback.yml b/.github/workflows/argus-extensibility-triage-feedback.yml new file mode 100644 index 0000000000..354164927a --- /dev/null +++ b/.github/workflows/argus-extensibility-triage-feedback.yml @@ -0,0 +1,156 @@ +name: Argus β€” Extensibility Request Triage Feedback + +on: + issue_comment: + types: [created] + +permissions: + issues: write + contents: read + id-token: write + +jobs: + handle-feedback: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Checkout BCAppsTriage source + uses: actions/checkout@v5 + with: + repository: ${{ github.repository_owner }}/BCAppsTriage + token: ${{ secrets.EXT_TRIAGE_SOURCE_TOKEN }} + path: _triage-src + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies + working-directory: _triage-src/internal/Argus_Triage_Extensibility_Requests + run: npm ci + + # ── Step 1: Detect /not-accurate command ──────────────────────────────── + - name: Detect /not-accurate command + id: detect + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + COMMENT_BODY=$(gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }} --jq '.body') + + if ! echo "$COMMENT_BODY" | grep -qiE '^\s*/not-accurate'; then + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Skip bots + ACTOR="${{ github.actor }}" + if [[ "$ACTOR" == "github-actions[bot]" || "$ACTOR" == *"[bot]"* ]]; then + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "skip=false" >> $GITHUB_OUTPUT + + # ── Step 2: Validate author & collect context ──────────────────────────── + - name: Validate author and collect context + if: steps.detect.outputs.skip == 'false' + id: context + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ISSUE_NUMBER=${{ github.event.issue.number }} + ACTOR="${{ github.actor }}" + + # Fetch issue metadata + ISSUE_DATA=$(gh api repos/${{ github.repository }}/issues/$ISSUE_NUMBER) + ISSUE_AUTHOR=$(echo "$ISSUE_DATA" | jq -r '.user.login') + ISSUE_URL=$(echo "$ISSUE_DATA" | jq -r '.html_url') + ASSIGNEES=$(echo "$ISSUE_DATA" | jq -r '[.assignees[].login] | join(" ")') + + # Validate: only issue author or an assignee may submit /not-accurate + AUTHORIZED=false + if [[ "$ACTOR" == "$ISSUE_AUTHOR" ]]; then + AUTHORIZED=true + else + for ASSIGNEE in $ASSIGNEES; do + if [[ "$ACTOR" == "$ASSIGNEE" ]]; then + AUTHORIZED=true + break + fi + done + fi + + if [[ "$AUTHORIZED" != "true" ]]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "Skipping: $ACTOR is not the issue author or an assignee." + exit 0 + fi + + # Idempotency: check if feedback was already recorded + ALREADY_RECORDED=$(gh api repos/${{ github.repository }}/issues/$ISSUE_NUMBER/comments \ + --paginate --jq '[.[] | select(.body | contains(""))] | length') + if [[ "$ALREADY_RECORDED" -gt 0 ]]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "Skipping: feedback already recorded for this issue." + exit 0 + fi + + # Find latest agent comment (posted by github-actions[bot]) + AGENT_COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/$ISSUE_NUMBER/comments \ + --paginate --jq '[.[] | select(.user.login == "github-actions[bot]")] | last | .id // 0') + + # Extract optional feedback text: inline text after /not-accurate AND any subsequent lines + COMMENT_BODY=$(gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }} --jq '.body') + INLINE_TEXT=$(echo "$COMMENT_BODY" | head -n 1 | sed -E 's|^\s*/not-accurate\s*||i') + REST_TEXT=$(echo "$COMMENT_BODY" | tail -n +2) + FEEDBACK_TEXT=$(printf '%s\n%s' "$INLINE_TEXT" "$REST_TEXT" | sed '/^[[:space:]]*$/d' | head -c 2000) + + echo "skip=false" >> $GITHUB_OUTPUT + echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT + echo "issue_author=$ISSUE_AUTHOR" >> $GITHUB_OUTPUT + echo "issue_url=$ISSUE_URL" >> $GITHUB_OUTPUT + echo "feedback_author=$ACTOR" >> $GITHUB_OUTPUT + echo "feedback_comment_id=${{ github.event.comment.id }}" >> $GITHUB_OUTPUT + echo "agent_comment_id=$AGENT_COMMENT_ID" >> $GITHUB_OUTPUT + + # Write multi-line feedback text to a file to avoid output encoding issues + echo "$FEEDBACK_TEXT" > /tmp/feedback_text.txt + + # ── Step 3: Log telemetry ──────────────────────────────────────────────── + - name: Azure login (OIDC) + if: steps.detect.outputs.skip == 'false' && steps.context.outputs.skip == 'false' + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Log feedback telemetry + if: steps.detect.outputs.skip == 'false' && steps.context.outputs.skip == 'false' + working-directory: _triage-src/internal/Argus_Triage_Extensibility_Requests + env: + ISSUE_NUMBER: ${{ steps.context.outputs.issue_number }} + REPOSITORY: ${{ github.repository }} + ISSUE_URL: ${{ steps.context.outputs.issue_url }} + ISSUE_AUTHOR: ${{ steps.context.outputs.issue_author }} + FEEDBACK_AUTHOR: ${{ steps.context.outputs.feedback_author }} + FEEDBACK_COMMENT_ID: ${{ steps.context.outputs.feedback_comment_id }} + AGENT_COMMENT_ID: ${{ steps.context.outputs.agent_comment_id }} + FEEDBACK_TEXT_FILE: /tmp/feedback_text.txt + run: npm run feedback + + # ── Step 4: Post acknowledgement ──────────────────────────────────────── + - name: Post acknowledgement comment + if: steps.detect.outputs.skip == 'false' && steps.context.outputs.skip == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + printf '%s\n' \ + '' \ + 'βœ… **Feedback recorded**' \ + "Thanks β€” your \`/not-accurate\` feedback has been logged and will be reviewed." \ + > /tmp/ack_body.txt + gh issue comment "${{ steps.context.outputs.issue_number }}" \ + --repo "${{ github.repository }}" \ + --body-file /tmp/ack_body.txt diff --git a/.github/workflows/argus-extensibility-triage-scheduled.yml b/.github/workflows/argus-extensibility-triage-scheduled.yml new file mode 100644 index 0000000000..9ef5667d7e --- /dev/null +++ b/.github/workflows/argus-extensibility-triage-scheduled.yml @@ -0,0 +1,151 @@ +name: Argus β€” Extensibility Request Triage (Scheduled) + +on: + schedule: + - cron: '0 1/6 * * *' # 4Γ— daily: 01:00, 07:00, 13:00, 19:00 UTC + workflow_dispatch: + +permissions: + issues: write + contents: read + id-token: write + +# ── Job 1: discover eligible issues ───────────────────────────────────────── +jobs: + discover: + runs-on: ubuntu-latest + outputs: + issues: ${{ steps.find-issues.outputs.issues }} + steps: + - name: Find eligible issues + id: find-issues + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REPO="${{ github.repository }}" + NOW=$(date -u '+%s') + + # ISO 8601 UTC strings β€” sort lexicographically == chronologically + CUTOFF_5MIN=$(date -u -d "@$((NOW - 300))" '+%Y-%m-%dT%H:%M:%SZ') # now βˆ’ 5 min + CUTOFF_7H=$(date -u -d "@$((NOW - 25200))" '+%Y-%m-%dT%H:%M:%SZ') # now βˆ’ 7 h + + ELIGIBLE=() + + while IFS= read -r ISSUE; do + NUMBER=$( echo "$ISSUE" | jq -r '.number') + TYPE=$( echo "$ISSUE" | jq -r '.type.name // ""') + LABEL_COUNT=$(echo "$ISSUE" | jq '.labels | length') + LABEL_NAME=$( echo "$ISSUE" | jq -r '.labels[0].name // ""') + CREATED_AT=$( echo "$ISSUE" | jq -r '.created_at') + UPDATED_AT=$( echo "$ISSUE" | jq -r '.updated_at') + + # Must be a Task + [ "$TYPE" != "Task" ] && continue + + if [ "$LABEL_COUNT" -eq 0 ]; then + # ── No labels: created at least 5 min before this run ───────── + [[ "$CREATED_AT" < "$CUTOFF_5MIN" ]] || continue + + # Skip if last comment contains /not-accurate + LAST_COMMENT=$(gh api "repos/$REPO/issues/$NUMBER/comments" \ + --paginate --jq '.[]' 2>/dev/null | tail -1 || true) + LAST_BODY=$(echo "$LAST_COMMENT" | jq -r '.body // ""' 2>/dev/null || true) + if echo "$LAST_BODY" | grep -qiE '^\s*/not-accurate'; then + continue + fi + + ELIGIBLE+=("$NUMBER") + + elif [ "$LABEL_COUNT" -eq 1 ] && [ "$LABEL_NAME" = "missing-info" ]; then + # ── Only missing-info: updated inside the 6-hour window ─────── + # Window: (now βˆ’ 7h) ≀ updated_at < (now βˆ’ 5min) + [[ ! "$UPDATED_AT" < "$CUTOFF_7H" && "$UPDATED_AT" < "$CUTOFF_5MIN" ]] || continue + + # Fetch last comment once β€” used for both checks below + LAST_COMMENT=$(gh api "repos/$REPO/issues/$NUMBER/comments" \ + --paginate --jq '.[]' 2>/dev/null | tail -1 || true) + + # Skip if last comment contains /not-accurate + LAST_BODY=$(echo "$LAST_COMMENT" | jq -r '.body // ""' 2>/dev/null || true) + if echo "$LAST_BODY" | grep -qiE '^\s*/not-accurate'; then + continue + fi + + # Last comment must not be from a bot (no comments = eligible) + LAST_AUTHOR=$(echo "$LAST_COMMENT" | jq -r '.user.login // ""' 2>/dev/null || true) + if [[ -z "$LAST_AUTHOR" || "$LAST_AUTHOR" != *"[bot]"* ]]; then + ELIGIBLE+=("$NUMBER") + fi + fi + + done < <(gh api "repos/$REPO/issues" \ + --paginate -X GET -f state=open -f per_page=100 --jq '.[]') + + if [ "${#ELIGIBLE[@]}" -eq 0 ]; then + echo "No eligible issues found." + echo "issues=[]" >> "$GITHUB_OUTPUT" + else + ISSUES_JSON=$(printf '%s\n' "${ELIGIBLE[@]}" | jq -R 'tonumber' | jq -sc '.') + echo "Found ${#ELIGIBLE[@]} eligible issue(s): ${ELIGIBLE[*]}" + echo "issues=$ISSUES_JSON" >> "$GITHUB_OUTPUT" + fi + +# ── Job 2: process each issue on its own runner ────────────────────────────── + process: + needs: discover + if: needs.discover.outputs.issues != '' && needs.discover.outputs.issues != '[]' + runs-on: ubuntu-latest + strategy: + matrix: + issue: ${{ fromJson(needs.discover.outputs.issues) }} + fail-fast: false # one failing issue must not block the others + steps: + - uses: actions/checkout@v5 + + - name: Checkout BCAppsTriage source + uses: actions/checkout@v5 + with: + repository: ${{ github.repository_owner }}/BCAppsTriage + token: ${{ secrets.EXT_TRIAGE_SOURCE_TOKEN }} + path: _triage-src + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies + working-directory: _triage-src/internal/Argus_Triage_Extensibility_Requests + run: npm ci + + - name: Install tsx globally + run: npm install -g tsx + + - name: Make process-issue script executable + run: chmod +x _triage-src/internal/Argus_Triage_Extensibility_Requests/scripts/process-issue.sh + + - name: Azure login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Process issue #${{ matrix.issue }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ secrets.EXT_TRIAGE_COPILOT_TOKEN }} + REPO_READ_TOKEN: ${{ secrets.EXT_TRIAGE_CODEBASE_TOKEN }} + run: | + bash _triage-src/internal/Argus_Triage_Extensibility_Requests/scripts/process-issue.sh \ + "${{ matrix.issue }}" \ + "${{ github.repository }}" \ + "$(pwd)/_triage-src" + + - name: Upload result artifact + if: always() + uses: actions/upload-artifact@v5 + with: + name: argus-result-${{ github.run_id }}-issue-${{ matrix.issue }} + path: _triage-src/internal/Argus_Triage_Extensibility_Requests/data/argus_result.json + retention-days: 30 + if-no-files-found: ignore