Skip to content

Commit 11090ca

Browse files
authored
Merge pull request #9 from replicatedhq/squizzi/infer-pr-labels-from-commit-msg
feature(ci): Infer PR labels from semantic commit messages
2 parents 1e4dabc + 3ecaa36 commit 11090ca

File tree

6 files changed

+323
-15
lines changed

6 files changed

+323
-15
lines changed

.github/actions/pr-labels/dist/index.js

Lines changed: 158 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9789,60 +9789,214 @@ const BUG_LABELS = ["bug::normal", "bug::regression"];
97899789

97909790
const SEVERITY_LABELS = ["severity::s1", "severity::s2", "severity::s3"];
97919791

9792+
// Map semantic commit types to our label types
9793+
const SEMANTIC_TYPE_TO_LABEL = {
9794+
chore: "type::chore",
9795+
fix: "type::bug",
9796+
bug: "type::bug",
9797+
feat: "type::feature",
9798+
feature: "type::feature",
9799+
security: "type::security",
9800+
};
9801+
9802+
/**
9803+
* Extract semantic commit type from a message
9804+
* Supports formats like:
9805+
* - "feat: add new feature"
9806+
* - "fix(component): fix bug"
9807+
* - "chore: update dependencies"
9808+
* @param {string} message - The commit message or PR title
9809+
* @returns {string|null} - The semantic type or null if not found
9810+
*/
9811+
function extractSemanticType(message) {
9812+
if (!message) return null;
9813+
9814+
core.debug(`Attempting to extract semantic type from: "${message}"`);
9815+
9816+
// Match standard semantic commit format: type(scope): message
9817+
const semanticRegex = /^(\w+)(?:\([\w-]+\))?:\s/;
9818+
const match = message.match(semanticRegex);
9819+
9820+
if (match && match[1]) {
9821+
const type = match[1].toLowerCase();
9822+
core.debug(`Extracted semantic type: "${type}"`);
9823+
return type;
9824+
}
9825+
9826+
core.debug("No semantic type found in message");
9827+
return null;
9828+
}
9829+
97929830
async function run() {
97939831
try {
97949832
// get inputs
97959833
const token = core.getInput("token", { required: true });
9834+
core.debug("Token retrieved successfully");
97969835

97979836
// set up github client
97989837
const octokit = github.getOctokit(token);
9838+
core.debug("GitHub client initialized");
9839+
9840+
// Track if we added a label based on semantic commit
9841+
let addedSemanticLabel = false;
97999842

98009843
// fetch the list of labels
9844+
core.debug("Fetching current PR labels...");
98019845
const labels = (
98029846
await octokit.rest.issues.listLabelsOnIssue({
98039847
...github.context.repo,
98049848
issue_number: github.context.issue.number,
98059849
})
98069850
).data.map((label) => label.name);
9807-
core.debug(`Found labels: ${labels.join(", ")}`);
9851+
core.debug(`Found ${labels.length} labels: ${labels.join(", ")}`);
9852+
9853+
// Get PR details to check for semantic commit messages
9854+
const prNumber = github.context.issue.number;
9855+
core.debug(`Processing PR #${prNumber}`);
9856+
9857+
core.debug("Fetching PR details...");
9858+
const { data: pullRequest } = await octokit.rest.pulls.get({
9859+
...github.context.repo,
9860+
pull_number: prNumber,
9861+
});
9862+
9863+
// Get the PR title and HEAD commit message
9864+
const prTitle = pullRequest.title;
9865+
core.debug(`PR title: "${prTitle}"`);
9866+
9867+
// Get the HEAD commit message
9868+
core.debug("Fetching PR commits...");
9869+
const { data: commits } = await octokit.rest.pulls.listCommits({
9870+
...github.context.repo,
9871+
pull_number: prNumber,
9872+
});
9873+
9874+
core.debug(`Found ${commits.length} commits in PR`);
9875+
const headCommitMessage = commits.length > 0 ? commits[commits.length - 1].commit.message : null;
9876+
if (headCommitMessage) {
9877+
core.debug(`HEAD commit message: "${headCommitMessage}"`);
9878+
} else {
9879+
core.debug("No HEAD commit message found");
9880+
}
9881+
9882+
// Try to extract semantic type from PR title or HEAD commit
9883+
core.debug("Extracting semantic type from PR title...");
9884+
const prTitleType = extractSemanticType(prTitle);
9885+
9886+
core.debug("Extracting semantic type from HEAD commit...");
9887+
const commitType = extractSemanticType(headCommitMessage);
9888+
9889+
// Use PR title type first, then fall back to commit type
9890+
const semanticType = prTitleType || commitType;
9891+
if (semanticType) {
9892+
core.debug(`Using semantic type: "${semanticType}"`);
9893+
} else {
9894+
core.debug("No semantic type found in PR title or HEAD commit");
9895+
}
9896+
9897+
// If we found a semantic type that maps to one of our labels, add it if not present
9898+
if (semanticType && SEMANTIC_TYPE_TO_LABEL[semanticType]) {
9899+
const labelToAdd = SEMANTIC_TYPE_TO_LABEL[semanticType];
9900+
core.debug(`Semantic type "${semanticType}" maps to label "${labelToAdd}"`);
9901+
9902+
// Only add the label if it's not already present
9903+
if (!labels.includes(labelToAdd)) {
9904+
core.info(`Adding label ${labelToAdd} based on semantic commit type: ${semanticType}`);
9905+
9906+
core.debug("Calling GitHub API to add label...");
9907+
await octokit.rest.issues.addLabels({
9908+
...github.context.repo,
9909+
issue_number: prNumber,
9910+
labels: [labelToAdd],
9911+
});
9912+
core.debug("Label added successfully via API");
9913+
9914+
// Update our local labels array to include the new label
9915+
labels.push(labelToAdd);
9916+
addedSemanticLabel = true;
9917+
core.debug(`Updated local labels array: ${labels.join(", ")}`);
9918+
9919+
// If we just added a label, give it time to apply
9920+
if (addedSemanticLabel) {
9921+
core.info("Added label based on semantic commit message. Waiting for label to apply...");
9922+
// Short delay to allow the label to be properly registered
9923+
core.debug("Waiting 2 seconds for label to propagate...");
9924+
await new Promise(resolve => setTimeout(resolve, 2000));
9925+
core.debug("Wait completed");
9926+
9927+
// Refetch the labels to ensure we have the most up-to-date set
9928+
core.info("Refetching labels after adding semantic label...");
9929+
core.debug("Calling GitHub API to get updated labels...");
9930+
const updatedLabelsResponse = await octokit.rest.issues.listLabelsOnIssue({
9931+
...github.context.repo,
9932+
issue_number: github.context.issue.number,
9933+
});
9934+
9935+
// Update our labels array with the freshly fetched labels
9936+
const updatedLabels = updatedLabelsResponse.data.map((label) => label.name);
9937+
core.debug(`Refetched ${updatedLabels.length} labels: ${updatedLabels.join(", ")}`);
9938+
9939+
// Replace our labels array with the updated one
9940+
labels.length = 0;
9941+
updatedLabels.forEach(label => labels.push(label));
9942+
core.debug(`Updated local labels array after refetch: ${labels.join(", ")}`);
9943+
}
9944+
} else {
9945+
core.debug(`Label "${labelToAdd}" already exists on PR, no need to add it`);
9946+
}
9947+
} else if (semanticType) {
9948+
core.debug(`Semantic type "${semanticType}" does not map to any of our labels`);
9949+
}
98089950

98099951
// ensure exactly one primary label is set
9952+
core.debug("Checking for primary labels...");
98109953
const primaryLabels = PRIMARY_LABELS.filter((label) =>
98119954
labels.includes(label)
98129955
);
9813-
core.debug(`Found primary labels: ${primaryLabels.join(", ")}`);
9956+
core.debug(`Found ${primaryLabels.length} primary labels: ${primaryLabels.join(", ")}`);
9957+
98149958
if (primaryLabels.length !== 1) {
9959+
core.debug(`Primary label check failed: found ${primaryLabels.length} primary labels`);
98159960
throw new Error(
98169961
`Exactly one primary label must be set from [${PRIMARY_LABELS.join(", ")}]. Found: ${primaryLabels.join(", ")}`
98179962
);
98189963
}
9964+
core.debug("Primary label check passed");
98199965

98209966
// if the primary label is a bug, ensure a bug label is set
98219967
if (primaryLabels[0] === "type::bug") {
9968+
core.debug("Primary label is type::bug, checking for bug labels...");
98229969
const bugLabels = BUG_LABELS.filter((label) => labels.includes(label));
9823-
core.debug(`type::bug is set, found bug labels: ${bugLabels.join(", ")}`);
9970+
core.debug(`Found ${bugLabels.length} bug labels: ${bugLabels.join(", ")}`);
98249971
if (bugLabels.length !== 1) {
9972+
core.debug(`Bug label check failed: found ${bugLabels.length} bug labels`);
98259973
throw new Error(
98269974
`Exactly one bug label must be set for primary type::bug. Found: ${bugLabels.join(
98279975
", "
98289976
)}`
98299977
);
98309978
}
9979+
core.debug("Bug label check passed");
98319980
}
98329981

98339982
// ensure no more than one severity label is set
9983+
core.debug("Checking for severity labels...");
98349984
const severityLabels = SEVERITY_LABELS.filter((label) =>
98359985
labels.includes(label)
98369986
);
9837-
core.debug(`Found severity labels: ${severityLabels.join(", ")}`);
9987+
core.debug(`Found ${severityLabels.length} severity labels: ${severityLabels.join(", ")}`);
98389988
if (severityLabels.length > 1) {
9989+
core.debug(`Severity label check failed: found ${severityLabels.length} severity labels`);
98399990
throw new Error(
98409991
`No more than one severity label may be set. Found: ${severityLabels.join(
98419992
", "
98429993
)}`
98439994
);
98449995
}
9996+
core.debug("Severity label check passed");
9997+
98459998
} catch (error) {
9999+
core.debug(`Error caught: ${error.message}`);
984610000
if (error instanceof Error) core.setFailed(error.message);
984710001
}
984810002
}

.github/actions/pr-labels/dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)