diff --git a/.github/workflows/auto-mark-released.yml b/.github/workflows/auto-mark-released.yml new file mode 100644 index 00000000..9e75695e --- /dev/null +++ b/.github/workflows/auto-mark-released.yml @@ -0,0 +1,117 @@ +name: Mark released issues on close + +on: + issues: + types: [closed] + +permissions: + contents: read + issues: write + +jobs: + mark-released: + # Run only if closed as completed AND has at least one "status:" label + if: ${{ github.event.issue.state_reason == 'completed' && contains(toJSON(github.event.issue.labels), 'status:') }} + runs-on: ubuntu-latest + steps: + - name: Set "released" label when closed completed and was ready for release + uses: actions/github-script@v7 + with: + script: | + // Helpers + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue = context.payload.issue; + if (!issue) { + core.info('No issue payload found, skipping.'); + return; + } + + // Only proceed if closed as "completed" (not "not_planned") + const isCompleted = issue.state_reason === 'completed'; + if (!isCompleted) { + core.info('Issue state_reason is not "completed"; skipping.'); + return; + } + + const getName = (l) => typeof l === 'string' ? l : l.name; + const labels = (issue.labels || []).map(getName); + + // Status detection (robust to case/spacing) + const isStatusLabel = (name) => /^status:\s*/i.test(String(name || '')); + const hasAnyStatus = labels.some(isStatusLabel); + if (!hasAnyStatus) { + core.info('Issue has no "status:*" label; skipping.'); + return; + } + + const READY_REGEX = /^status:\s*ready\s*for\s*release$/i; + const hasReady = labels.some((n) => READY_REGEX.test(String(n || ''))); + if (!hasReady) { + core.info('Issue is not "status: ready for release"; nothing to do.'); + return; + } + + const releasedLabel = 'status: released'; + + // Ensure "released" label exists + try { + await github.rest.issues.getLabel({ owner, repo, name: releasedLabel }); + } catch (e) { + if (e.status === 404) { + core.info(`Label "${releasedLabel}" not found. Creating it.`); + await github.rest.issues.createLabel({ + owner, + repo, + name: releasedLabel, + color: '184738', // deep green + description: 'Feature released.' + }); + } else { + throw e; + } + } + + // Remove all status:* labels, add "status: released" + const filtered = labels.filter((name) => !isStatusLabel(name)); + const newLabels = [...new Set([...filtered, releasedLabel])]; + + await github.rest.issues.update({ + owner, + repo, + issue_number: issue.number, + labels: newLabels + }); + + core.info(`Issue #${issue.number} relabeled as "${releasedLabel}".`); + + // === NEW: add release comment with milestone if present === + const milestone = issue.milestone; + if (!milestone || !milestone.title) { + core.info('Issue has no milestone; no release comment added.'); + return; + } + + const commentBody = `Released in version ${milestone.title}.`; + + // Avoid duplicate comment if the action reruns + const existingComments = await github.paginate( + github.rest.issues.listComments, + { owner, repo, issue_number: issue.number, per_page: 100 } + ); + + const alreadyCommented = existingComments.some(c => + typeof c.body === 'string' && c.body.trim() === commentBody + ); + + if (alreadyCommented) { + core.info('Release comment already present; skipping comment creation.'); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body: commentBody + }); + core.info('Release comment added.'); + } diff --git a/.github/workflows/issues-validate-integration-field.yml b/.github/workflows/issues-validate-integration-field.yml new file mode 100644 index 00000000..70682fa9 --- /dev/null +++ b/.github/workflows/issues-validate-integration-field.yml @@ -0,0 +1,75 @@ +name: Issues Validate Integration Field + +on: + issues: + types: [opened, edited] + +permissions: + issues: write + +jobs: + check-integration: + runs-on: ubuntu-latest + steps: + - name: Validate dropdown selection (only for bug report form) + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + const bodyText = issue.body || ""; + + // ---- Guard: only act if this looks like a Bug Report form ---- + if (!bodyText.includes("### The problem")) { + core.info("Skipped: This issue does not contain '### The problem', so it is not a Bug Report."); + return; + } + core.info("Detected a Bug Report issue. Proceeding with validation..."); + + // Extract selection under "Which integration are you using?" + const header = "### Which integration are you using?"; + const nextHeaderRegex = /\n### /; + let selection = ""; + + const startIdx = bodyText.indexOf(header); + if (startIdx !== -1) { + const afterHeader = bodyText.slice(startIdx + header.length).replace(/^\r?\n+/, ""); + const nextHeaderIdx = afterHeader.search(nextHeaderRegex); + selection = (nextHeaderIdx === -1 ? afterHeader : afterHeader.slice(0, nextHeaderIdx)).trim(); + } + + core.info(`Integration field extracted: "${selection}"`); + + const isInvalid = !selection || selection.startsWith("-- Select an option"); + if (isInvalid) { + core.info("Integration field is invalid or not selected."); + } else { + core.info("Integration field is valid. Nothing to do."); + } + + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = issue.number; + + // Avoid duplicate comments + const marker = "please specify **Which integration are you using?**"; + const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number }); + const alreadyCommented = comments.some(c => c.body && c.body.includes(marker)); + + if (isInvalid) { + if (!alreadyCommented) { + core.info("No existing reminder found. Posting a new comment..."); + await github.rest.issues.createComment({ + owner, repo, issue_number, + body: [ + `👋 Hello @${issue.user.login}!`, + `To complete your bug report, please specify which integration you are using.`, + "", + "Right now the field `**Which integration are you using?**` is set to `-- Select an option --`.", + "", + "Click **Edit** at the top right of the issue, choose a valid option from the dropdown, and save.", + ].join("\n") + }); + } else { + core.info("Reminder comment already exists. Skipping new comment."); + } + } diff --git a/.github/workflows/prerelease_comment_issue.yml b/.github/workflows/prerelease_comment_issue.yml index 037ca050..d82226b3 100644 --- a/.github/workflows/prerelease_comment_issue.yml +++ b/.github/workflows/prerelease_comment_issue.yml @@ -49,7 +49,6 @@ jobs: const milestoneTitle = parsed.milestoneTitle; // es. "3.7.0" const prereleaseTagDisplay = tagName.startsWith("v") ? tagName : `v${tagName}`; const commentBody = `This feature is available for testing in the pre-release version [${prereleaseTagDisplay}](https://github.com/micz/ThunderAI/releases).`; - core.info(`(MAIN) Looking for milestone '${milestoneTitle}' in ${owner}/${repo}`); // Find milestone (open or closed)