Add orbit/fleetd version detection and support both singular/plural version fields in bug tagging workflow (#41268)

Updates the auto-tag-unreleased-bugs workflow to improve version
detection and handling.

## Changes Made

- **Orbit/Fleetd Version Detection**: Added support for detecting Orbit
and Fleetd versions in addition to Fleet server versions. The workflow
now checks `**Orbit version**:` and `**Fleetd version**:` fields (case
insensitive) and validates them against orbit-v* tags.

- **Optimized API Calls**: The workflow now only fetches the data it
needs:
  - Fetches releases only when checking Fleet server versions
  - Fetches tags only when checking Orbit/Fleetd versions
  - This reduces unnecessary GitHub API calls and improves performance

- **Singular/Plural Field Support**: Updated regex patterns to match
both "version" and "versions" in issue templates (e.g., `**Fleet
version**:` and `**Fleet versions**:`). This handles variations in issue
template formatting where either singular or plural forms may be used.

- **Pagination Support**: Both `listReleases` and `listTags` API calls
now use `github.paginate()` to fetch all results instead of just the
first 100. This ensures older Orbit/Fleetd versions or Fleet versions
won't be misclassified as unreleased when they exist beyond the first
page of results.

- **Fixed 4.x Handling**: Corrected the logic for handling "4.x" version
strings (which represent all 4.x versions). The check now occurs before
the empty versions check, preventing issues reporting only "4.x" from
being incorrectly tagged as unreleased.

## Testing

-  Verified regex patterns match both singular and plural forms for
Fleet, Orbit, and Fleetd version fields
-  Confirmed the workflow correctly parses versions from various issue
formats
-  Tested that API optimization only fetches releases or tags based on
which version types are present
-  Verified pagination logic fetches all releases and tags, not just
first 100
-  Tested 4.x handling logic correctly treats it as released

The changes maintain backward compatibility with existing issue formats
while adding support for Orbit/Fleetd version detection, handling
template variations, and ensuring comprehensive version checking through
pagination.

---------

Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: iansltx <472804+iansltx@users.noreply.github.com>
Co-authored-by: lukeheath <2495927+lukeheath@users.noreply.github.com>
This commit is contained in:
Claude
2026-03-17 17:32:01 -05:00
committed by GitHub
co-authored by iansltx lukeheath
parent cc02191ae2
commit f8e1623179
+113 -44
View File
@@ -54,56 +54,115 @@ jobs:
// Parse Fleet version from issue body
const body = issue.body || '';
const versionMatch = body.match(/\*\*Fleet version\*\*:\s*(.+)/);
const versionMatch = body.match(/\*\*Fleet versions?\*\*:\s*(.+)/i);
// Also check for Orbit/Fleetd version (case insensitive)
const orbitMatch = body.match(/\*\*(?:Orbit|Fleetd) versions?\*\*:\s*(.+)/i);
if (!versionMatch || !versionMatch[1]) {
console.log('No Fleet version found in issue body');
await tagAsUnreleased();
return;
// If no Fleet version but has Orbit/Fleetd version, check that instead
if (orbitMatch && orbitMatch[1]) {
console.log('Found Orbit/Fleetd version, will check that instead');
} else {
await tagAsUnreleased();
return;
}
}
// Extract version, removing any HTML comments
let reportedVersion = versionMatch[1].trim();
let reportedVersion = versionMatch ? versionMatch[1].trim() : '';
let orbitVersion = orbitMatch ? orbitMatch[1].trim() : '';
// Remove HTML comment if present (e.g., "4.62.0 <!-- comment -->")
reportedVersion = reportedVersion.replace(/\s*<!--.*?-->\s*/g, '').trim();
orbitVersion = orbitVersion.replace(/\s*<!--.*?-->\s*/g, '').trim();
console.log(`Found reported version: ${reportedVersion}`);
if (orbitVersion) {
console.log(`Found Orbit/Fleetd version: ${orbitVersion}`);
}
// Treat as unreleased if reported version is RC/main/unknown/todo
if (!reportedVersion ||
reportedVersion.trim() === '' ||
reportedVersion.toLowerCase().includes('todo') ||
reportedVersion.toLowerCase().includes('unknown') ||
reportedVersion.toLowerCase().includes('main') ||
reportedVersion.toLowerCase().includes('rc')) {
// Check both Fleet version and Orbit/Fleetd version if present
const versionsToCheck = [];
if (reportedVersion &&
reportedVersion.trim() !== '' &&
!reportedVersion.toLowerCase().includes('todo') &&
!reportedVersion.toLowerCase().includes('unknown') &&
!reportedVersion.toLowerCase().includes('main') &&
!reportedVersion.toLowerCase().includes('rc') &&
reportedVersion !== '4.x') {
versionsToCheck.push({ version: reportedVersion, type: 'fleet' });
}
if (orbitVersion &&
orbitVersion.trim() !== '' &&
!orbitVersion.toLowerCase().includes('todo') &&
!orbitVersion.toLowerCase().includes('unknown') &&
!orbitVersion.toLowerCase().includes('main') &&
!orbitVersion.toLowerCase().includes('rc')) {
versionsToCheck.push({ version: orbitVersion, type: 'orbit' });
}
// Special case: "4.x" means all 4.x versions, which is released
if (reportedVersion === '4.x') {
return;
}
// If no valid versions to check, tag as unreleased
if (versionsToCheck.length === 0) {
await tagAsUnreleased();
return;
}
if (reportedVersion === '4.x') {
return; // this is "all 4.x versions" so it's released
// Determine what we need to fetch based on versions present
const needsFleetReleases = versionsToCheck.some(v => v.type === 'fleet');
const needsOrbitTags = versionsToCheck.some(v => v.type === 'orbit');
// Fetch Fleet releases only if we have a Fleet version to check
let releasedFleetVersions = [];
if (needsFleetReleases) {
const allReleases = await github.paginate(github.rest.repos.listReleases, {
owner: "fleetdm",
repo: "fleet",
per_page: 100
});
// Extract version numbers from Fleet releases
// Fleet releases are tagged as "fleet-v4.X.X" or similar
releasedFleetVersions = allReleases
.map(release => {
// Try to extract from name
const nameMatch = release.name?.match(/(\d+\.\d+\.\d+)/);
if (nameMatch) return nameMatch[1];
return null;
})
.filter(v => v !== null);
}
// Fetch most recent 100 releases from the repo; that's realistically enough to match
// any newly created bug
const { data: allReleases } = await github.rest.repos.listReleases({
owner: "fleetdm",
repo: "fleet",
per_page: 100,
page: 1
});
// Fetch tags only if we have an orbit/fleetd version to check
let releasedOrbitVersions = [];
if (needsOrbitTags) {
const allTags = await github.paginate(github.rest.repos.listTags, {
owner: "fleetdm",
repo: "fleet",
per_page: 100
});
// Extract version numbers from releases
// Fleet releases are tagged as "fleet-v4.X.X" or similar
const releasedVersions = allReleases
.map(release => {
// Try to extract from name
const nameMatch = release.name?.match(/(\d+\.\d+\.\d+)/);
if (nameMatch) return nameMatch[1];
return null;
})
.filter(v => v !== null);
// Extract orbit/fleetd versions from tags
// Orbit tags are like "orbit-v1.X.X"
releasedOrbitVersions = allTags
.filter(tag => tag.name.match(/^orbit-v\d+\.\d+\.\d+$/))
.map(tag => {
const match = tag.name.match(/^orbit-v(\d+\.\d+\.\d+)$/);
return match ? match[1] : null;
})
.filter(v => v !== null);
}
// Normalize version for comparison
// Remove common prefixes/suffixes and extract core version number
@@ -111,26 +170,36 @@ jobs:
// First try to extract x.y.z pattern
let match = version.match(/v?(\d+\.\d+\.\d+)/);
if (match) return match[1];
// If no patch version, try x.y pattern and add .0
match = version.match(/v?(\d+\.\d+)(?!\.\d)/);
if (match) return match[1] + '.0';
return version;
};
// Split version string on "&" to handle multiple versions (e.g., "4.60 & 4.61")
const reportedVersions = reportedVersion.split('&').map(v => v.trim());
// Check if ANY of the reported versions matches any released version
// Check if ANY of the reported versions is released
let isReleased = false;
for (const version of reportedVersions) {
const normalizedVersion = normalizeVersion(version);
if (releasedVersions.some(releasedVer => releasedVer === normalizedVersion)) {
console.log(`Found released version: ${normalizedVersion}`);
isReleased = true;
break;
for (const versionInfo of versionsToCheck) {
const { version, type } = versionInfo;
// Split version string on "&" to handle multiple versions (e.g., "4.60 & 4.61")
const versions = version.split('&').map(v => v.trim());
for (const v of versions) {
const normalizedVersion = normalizeVersion(v);
// Check against the appropriate list based on type
const releasedVersions = type === 'orbit' ? releasedOrbitVersions : releasedFleetVersions;
if (releasedVersions.some(releasedVer => releasedVer === normalizedVersion)) {
console.log(`Found released ${type} version: ${normalizedVersion}`);
isReleased = true;
break;
}
}
if (isReleased) break;
}
if (isReleased) {