项目文件夹

文件
Ali Khokhar d37b34b02f Improve issue version triage and remove title prefixes (#1217)
## Problem

Bug-report validation rejected useful prose even when the FCC version
was unambiguous, and older installations received no update guidance.
Issue forms also duplicated their existing labels with forced title
prefixes.

## Changes

| Before | After |
| --- | --- |
| The FCC version field accepted only a bare version, copied command
output, or `None`. | The field accepts exactly one standalone
`number.number.number` value anywhere in the text, while preserving
exact `None` and rejecting ambiguous input. |
| Valid versions were not compared with the currently installable code.
| The workflow reads the live issue and project version from the default
branch, then compares numeric components safely. |
| Older reports received no update guidance. | One bot-owned comment
asks the reporter to update; edits update or remove that comment without
labels or issue closure. |
| Bug and feature forms forced `[Bug]` and `[Feature]` title prefixes. |
Existing `bug` and `enhancement` labels own classification without
changing the reporter's title. |
| Contract coverage inspected only workflow source fragments. | Contract
coverage executes the JavaScript lifecycle and protects label-only issue
classification. |

<!-- greptile_comment -->

<details open><summary><h3>Greptile Summary</h3></summary>

This PR improves issue classification and FCC version triage. The main
changes are:

- Removes forced title prefixes from bug and feature forms.
- Accepts one unambiguous numeric FCC version within descriptive text.
- Compares reported versions with the default branch project version.
- Reconciles invalid-version and update-guidance comments after edits.
- Adds executable workflow lifecycle and issue-form contract tests.
</details>

<h3>Confidence Score: 5/5</h3>

This looks safe to merge.

Corrected version fields now remove stale invalid-version comments.
Common valid TOML quote and comment formats are handled.

No blocking issues remain in the changed code.

<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>

**What T-Rex did**
- I reviewed the pre-change contract-validation baseline for forms,
which showed 12 failures and 18 passes under origin/main.
- I executed the post-change contract-validation test run with the
environment set for the project and the pytest suite targeting the
contract tests, and it completed with exit code 0 and 30 passes.

<a
href="https://app.greptile.com/trex/runs/15163869/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a>

<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>

<details open><summary><h3>Important Files Changed</h3></summary>

| Filename | Overview |
|----------|----------|
| .github/workflows/validate-bug-report-version.yml | Adds live issue
reconciliation, project-version parsing, numeric comparison, and managed
invalid and outdated comments. |
| tests/contracts/test_issue_form_version_validation.py | Adds
executable coverage for version extraction, TOML parsing, numeric
comparison, and comment reconciliation. |
| .github/ISSUE_TEMPLATE/bug-report.yml | Removes the title prefix and
allows one numeric version within descriptive text. |
| .github/ISSUE_TEMPLATE/feature-request.yml | Removes the title prefix
while retaining enhancement classification. |
| tests/contracts/test_issue_forms.py | Checks that issue forms use
labels instead of title prefixes. |

</details>

<sub>Reviews (3): Last reviewed commit: ["Reconcile bug version triage
state"](https://github.com/alishahryar1/free-claude-code/commit/8efe7aaf95bd2a96719b1d0811afb83b06c5d79c)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=45775911)</sub>

<!-- /greptile_comment -->
2026-07-20 15:46:41 -07:00

176 行
7.2 KiB
YAML

name: Validate bug report version
on:
issues:
types: [opened, edited]
concurrency:
group: bug-report-version-${{ github.event.issue.number }}
cancel-in-progress: false
permissions:
contents: read
issues: write
jobs:
validate:
if: contains(github.event.issue.labels.*.name, 'bug')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validate FCC version
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const repo = context.repo;
const issue_number = context.payload.issue.number;
const { data: issue } = await github.rest.issues.get({
...repo,
issue_number,
});
const fieldPattern = "^### FCC version\\r?\\n+\\s*([^\\r\\n]+)";
const versionPattern = "(?:^|[^0-9A-Za-z_.])v?([0-9]+\\.[0-9]+\\.[0-9]+)(?![0-9A-Za-z_+-]|\\.[0-9A-Za-z_])";
const projectVersionPattern = "^\\s*version\\s*=\\s*([\"'])([0-9]+\\.[0-9]+\\.[0-9]+)\\1\\s*(?:#.*)?$";
const match = (issue.body || "").match(new RegExp(fieldPattern, "m"));
const fieldValue = match?.[1]?.trim() || "";
const versionMatches = [...fieldValue.matchAll(new RegExp(versionPattern, "g"))];
const reportedVersion = versionMatches.length === 1 ? versionMatches[0][1] : undefined;
const valid = reportedVersion !== undefined || fieldValue === "None";
const labelName = "needs-fcc-version";
const invalidMarker = "<!-- fcc-version-validator -->";
const outdatedMarker = "<!-- fcc-version-outdated -->";
const labels = new Set(issue.labels.map((label) => label.name));
const comments = await github.paginate(
github.rest.issues.listComments,
{ ...repo, issue_number, per_page: 100 },
);
const managedComment = (marker) => comments.find(
(comment) => comment.user?.login === "github-actions[bot]"
&& comment.body?.includes(marker),
);
const invalidComment = managedComment(invalidMarker);
const outdatedComment = managedComment(outdatedMarker);
const deleteManagedComment = async (comment) => {
if (comment === undefined) return;
await github.rest.issues.deleteComment({
...repo,
comment_id: comment.id,
});
};
const isOlderVersion = (reported, latest) => {
const reportedParts = reported.split(".").map((part) => BigInt(part));
const latestParts = latest.split(".").map((part) => BigInt(part));
for (let index = 0; index < reportedParts.length; index += 1) {
if (reportedParts[index] < latestParts[index]) return true;
if (reportedParts[index] > latestParts[index]) return false;
}
return false;
};
const projectVersionFromToml = (pyproject) => {
let inProjectSection = false;
for (const line of pyproject.split(/\r?\n/)) {
const stripped = line.trim();
if (stripped === "[project]") {
inProjectSection = true;
continue;
}
if (inProjectSection && stripped.startsWith("[")) return undefined;
if (!inProjectSection) continue;
const projectVersion = line.match(new RegExp(projectVersionPattern));
if (projectVersion !== null) return projectVersion[2];
}
return undefined;
};
if (!valid) {
await deleteManagedComment(outdatedComment);
if (!labels.has(labelName)) {
try {
await github.rest.issues.getLabel({ ...repo, name: labelName });
} catch (error) {
if (error.status !== 404) throw error;
try {
await github.rest.issues.createLabel({
...repo,
name: labelName,
color: "d4c5f9",
description: "FCC version must contain number.number.number or be None",
});
} catch (createError) {
if (createError.status !== 422) throw createError;
}
}
await github.rest.issues.addLabels({
...repo,
issue_number,
labels: [labelName],
});
}
if (invalidComment === undefined) {
await github.rest.issues.createComment({
...repo,
issue_number,
body: `${invalidMarker}\nPlease edit **FCC version** so it contains one \`number.number.number\` value, such as \`1.22.333\`, or is exactly \`None\`. Run \`fcc-server --version\` to find the installed version.`,
});
}
return;
}
if (labels.has(labelName)) {
await github.rest.issues.removeLabel({
...repo,
issue_number,
name: labelName,
});
}
await deleteManagedComment(invalidComment);
if (reportedVersion === undefined) {
await deleteManagedComment(outdatedComment);
return;
}
const defaultBranch = context.payload.repository.default_branch;
const { data: versionFile } = await github.rest.repos.getContent({
...repo,
path: "pyproject.toml",
ref: defaultBranch,
});
if (
Array.isArray(versionFile)
|| versionFile.type !== "file"
|| versionFile.encoding !== "base64"
|| !versionFile.content
) {
throw new Error(`Could not read pyproject.toml from ${defaultBranch}`);
}
const pyproject = Buffer.from(versionFile.content, "base64").toString("utf8");
const latestVersion = projectVersionFromToml(pyproject);
if (latestVersion === undefined) {
throw new Error(`Could not read the project version from ${defaultBranch}`);
}
if (!isOlderVersion(reportedVersion, latestVersion)) {
await deleteManagedComment(outdatedComment);
return;
}
const body = `${outdatedMarker}\nThis report uses FCC \`${reportedVersion}\`, while the current version on \`${defaultBranch}\` is \`${latestVersion}\`. Please [update FCC](${context.payload.repository.html_url}#install), restart FCC, and check whether the issue still occurs. The update may already contain the fix.`;
if (outdatedComment === undefined) {
await github.rest.issues.createComment({
...repo,
issue_number,
body,
});
} else if (outdatedComment.body !== body) {
await github.rest.issues.updateComment({
...repo,
comment_id: outdatedComment.id,
body,
});
}