Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions .github/workflows/npf-pr-labeler.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: NPF Pull Request Labeler

on:
pull_request_target:
types:
- opened
- synchronize
- reopened
branches:
- master
paths:
- "Solutions/**"

permissions:
contents: read
pull-requests: write

jobs:
label-npf-pr:
if: ${{ github.actor != 'dependabot[bot]' }}
name: Label NPF Migration, Validation, Qualification, and Solution Metadata
runs-on: ubuntu-latest
steps:
- name: Inspect PR files, apply NPF labels, and write npf-meta block
uses: actions/github-script@b7fb2001b410c9390cbe9e2c7d5cab7eefb7b29c
with:
script: |
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100
});

const activePaths = files
.filter(f => f.status !== 'removed')
.map(f => f.filename);

const solutionNames = new Set();
for (const p of activePaths) {
const m = p.match(/^Solutions\/([^/]+)\//);
if (m && m[1] !== 'Images') {
solutionNames.add(m[1]);
}
}

const hasXdrContent = activePaths.some(p =>
/^Solutions\/[^/]+\/(XDR Detections|XDR Workbooks)\//.test(p)
);
const hasValidationEvidence = activePaths.some(p =>
/^Solutions\/[^/]+\/Evidence\/(pr-qualification\/(pr-qualification-result\.json|qualification-report\.html|qualification-summary\.md)|workflow-state\.json|decision-manifest\.json|[^/]+\/evidence\.json)$/.test(p)
);
const hasQualificationEvidence = activePaths.some(p =>
/^Solutions\/[^/]+\/Evidence\/(qualification-evidence\.json|receipts\/(alert-parity-live|workbook-queries-live|arm-deployment-readback|graph-detection-deployment)\.json)$/.test(p)
);

const desired = new Set();
const npfModes = [];
if (solutionNames.size > 0) {
desired.add('Solution');
}
if (hasXdrContent) {
desired.add('XDR');
desired.add('Migration');
npfModes.push('Migration');
}
if (hasValidationEvidence) {
desired.add('Validation');
npfModes.push('Validation');
}
if (hasQualificationEvidence) {
desired.add('Qualification');
npfModes.push('Qualification');
}

// Determine 1P Solution / ISV Solution / Community Solution from SolutionMetadata.json
const headSha = context.payload.pull_request?.head?.sha;
const baseSha = context.payload.pull_request?.base?.sha;
const publisherTiers = new Set();
const solutionDetails = [];

for (const sol of [...solutionNames].sort()) {
const metaPath = `Solutions/${sol}/SolutionMetadata.json`;
let metaText = null;
for (const ref of [headSha, baseSha].filter(Boolean)) {
try {
const resp = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: metaPath,
ref
});
if (resp.data && resp.data.content) {
metaText = Buffer.from(resp.data.content, 'base64').toString('utf8');
break;
}
} catch (_) {
// Try fallback ref
}
}
let tierLabel = 'Unknown';
if (metaText) {
try {
const meta = JSON.parse(metaText.replace(/^\uFEFF/, ''));
const tier = String(meta?.support?.tier || '').trim().toLowerCase();
const pub = String(meta?.publisherId || '').trim().toLowerCase();
if (tier === 'community' || pub === 'microsoftsentinelcommunity') {
tierLabel = 'Community Solution';
} else if (tier === 'partner') {
tierLabel = 'ISV Solution';
} else if (tier === 'microsoft' || pub === 'azuresentinel' || pub === 'microsoftcorporation') {
tierLabel = '1P Solution';
}
if (tierLabel !== 'Unknown') {
desired.add(tierLabel);
publisherTiers.add(tierLabel);
}
} catch (e) {
core.warning(`Failed to parse ${metaPath}: ${e.message}`);
}
}
solutionDetails.push({ name: sol, tier: tierLabel });
}

if (desired.size === 0) {
core.info('No Solution or NPF labels detected.');
return;
}

const existing = new Set(
(context.payload.pull_request?.labels || []).map(l => l.name)
);
const labelsToAdd = [...desired].filter(l => !existing.has(l));

if (labelsToAdd.length > 0) {
core.info(`Adding labels to PR #${context.issue.number}: ${labelsToAdd.join(', ')}`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: labelsToAdd
});
} else {
core.info(`All applicable labels already present: ${[...desired].join(', ')}`);
}

// Build machine-readable npf-meta block for PR body and step summary
const npfMeta = {
solutionCount: solutionDetails.length,
solutions: solutionDetails.map(s => s.name),
publisherTiers: [...publisherTiers],
npfModes
};
const metaComment = `<!-- npf-meta: ${JSON.stringify(npfMeta)} -->`;

const currentBody = context.payload.pull_request?.body || '';
const metaRegex = /<!--\s*npf-meta:\s*\{.*?\}\s*-->/s;
let updatedBody = currentBody;
if (metaRegex.test(currentBody)) {
updatedBody = currentBody.replace(metaRegex, metaComment);
} else {
updatedBody = currentBody.trimEnd() + `\n\n${metaComment}\n`;
}

if (updatedBody !== currentBody) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: updatedBody
});
core.info(`Updated PR #${context.issue.number} body with npf-meta block.`);
}

await core.summary
.addHeading('NPF PR Metadata Summary', 3)
.addRaw(`<p><strong>Solutions Count:</strong> ${npfMeta.solutionCount}</p>`, true)
.addCodeBlock(JSON.stringify(npfMeta, null, 2), 'json')
.write();
Loading