Skip to content

Define a general catalog-compatibility contract for external --data catalogs (ADR-015 DD4) #1950

Define a general catalog-compatibility contract for external --data catalogs (ADR-015 DD4)

Define a general catalog-compatibility contract for external --data catalogs (ADR-015 DD4) #1950

Workflow file for this run

# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Issue Triage
on:
issues:
types: [opened, labeled]
workflow_dispatch: {}
permissions:
contents: read
jobs:
triage:
name: Auto-Triage Issues
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
timeout-minutes: 5
steps:
- name: Add needs-triage on new issue
if: github.event.action == 'opened'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['needs-triage']
});
- name: Infer area label on new issue
if: github.event.action == 'opened'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const title = context.payload.issue.title || '';
const body = context.payload.issue.body || '';
const existingLabels = new Set((context.payload.issue.labels || []).map(label => label.name));
const componentPatterns = [
{ pattern: /\bcli\b/, label: 'area/cli' },
{ pattern: /\bapi server\b|\baicrd\b/, label: 'area/api' },
{ pattern: /\brecipe engine\b|\brecipe engine \/ data\b/, label: 'area/recipes' },
{ pattern: /\bbundler\b|\bbundlers\b/, label: 'area/bundler' },
{ pattern: /\bcollectors?\b|\bsnapshotter\b/, label: 'area/collector' },
{ pattern: /\bvalidator\b/, label: 'area/validator' },
{ pattern: /\bcore libraries\b/, label: 'area/infra' },
{ pattern: /\bdocs\b|\bdocumentation\b/, label: 'area/docs' },
];
const tokenMap = new Map([
['api', 'area/api'],
['aicrd', 'area/api'],
['server', 'area/api'],
['bundle', 'area/bundler'],
['bundler', 'area/bundler'],
['bundlers', 'area/bundler'],
['argocd', 'area/bundler'],
['chart', 'area/bundler'],
['charts', 'area/bundler'],
['helm', 'area/bundler'],
['ci', 'area/ci'],
['workflow', 'area/ci'],
['workflows', 'area/ci'],
['action', 'area/ci'],
['actions', 'area/ci'],
['cli', 'area/cli'],
['command', 'area/cli'],
['collector', 'area/collector'],
['collectors', 'area/collector'],
['snapshot', 'area/collector'],
['snapshotter', 'area/collector'],
['topology', 'area/collector'],
['docs', 'area/docs'],
['documentation', 'area/docs'],
['readme', 'area/docs'],
['infra', 'area/infra'],
['infrastructure', 'area/infra'],
['recipe', 'area/recipes'],
['recipes', 'area/recipes'],
['overlay', 'area/recipes'],
['overlays', 'area/recipes'],
['mixin', 'area/recipes'],
['mixins', 'area/recipes'],
['test', 'area/tests'],
['tests', 'area/tests'],
['chainsaw', 'area/tests'],
['e2e', 'area/tests'],
['kwok', 'area/tests'],
['validate', 'area/validator'],
['validation', 'area/validator'],
['validator', 'area/validator'],
['validators', 'area/validator'],
['conformance', 'area/validator'],
['deployment', 'area/validator'],
['performance', 'area/validator'],
]);
function normalize(value) {
return value.trim().toLowerCase();
}
// Sentinel values from issue templates that indicate ambiguous/unknown
// components. When these are selected, ignore the component field
// and continue inferring from title/body text.
const AMBIGUOUS_COMPONENTS = [
'multiple components',
'new component',
'other / unknown',
'other',
'unknown',
];
function inferFromComponent() {
const match = body.match(/^#{2,3}\s+Component\s*$[\r\n]+(?:[\r\n]*)([^\r\n]+)/im);
if (!match) {
return null;
}
const component = normalize(match[1]);
if (AMBIGUOUS_COMPONENTS.includes(component)) {
core.info(`Component "${match[1].trim()}" is ambiguous — falling back to title/body inference`);
return null;
}
for (const { pattern, label } of componentPatterns) {
if (pattern.test(component)) {
return label;
}
}
return null;
}
function inferFromScope() {
const scopeMatch = title.toLowerCase().match(/^[a-z]+\(([^)]+)\):/);
if (!scopeMatch) {
return null;
}
for (const token of scopeMatch[1].split(/[^a-z0-9]+/)) {
const label = tokenMap.get(token);
if (label) {
return label;
}
}
return null;
}
function inferFromText(text) {
const normalized = text.toLowerCase();
const orderedPatterns = [
{ pattern: /\b(pkg\/api|api\/|aicrd|api server)\b/, label: 'area/api' },
{ pattern: /\b(pkg\/bundler|aicr bundle|bundle output|argocd|helm chart|oci bundle)\b/, label: 'area/bundler' },
{ pattern: /\b(pkg\/cli|command line|cli)\b/, label: 'area/cli' },
{ pattern: /\b(pkg\/collector|snapshotter|snapshot|collector|collectors|topology)\b/, label: 'area/collector' },
{ pattern: /\b(docs\/|documentation|readme|docs)\b/, label: 'area/docs' },
{ pattern: /\b(infra\/|infrastructure)\b/, label: 'area/infra' },
{ pattern: /\b(tests\/|chainsaw|kwok|e2e|test trigger|gpu test)\b/, label: 'area/tests' },
{ pattern: /(?:\.github\/|\bgithub actions\b|\bworkflow\b|\bworkflows\b|\bci\b)/, label: 'area/ci' },
{
pattern: /\b(pkg\/validator|validators?\/|validators?|validation|validate command|aicr validate|conformance|deployment(?:-phase checks| validator)|performance check)\b/,
label: 'area/validator'
},
{ pattern: /\b(recipes\/|recipe engine|recipemetadata|reciperesult|registry\.yaml|overlay|overlays|mixin|mixins|recipe)\b/, label: 'area/recipes' },
];
for (const { pattern, label } of orderedPatterns) {
if (pattern.test(normalized)) {
return label;
}
}
return null;
}
let areaLabel = [...existingLabels].find(label => label.startsWith('area/')) || null;
if (!areaLabel) {
areaLabel = inferFromComponent();
}
if (!areaLabel && existingLabels.has('documentation')) {
areaLabel = 'area/docs';
}
if (!areaLabel) {
areaLabel = inferFromScope();
}
if (!areaLabel) {
areaLabel = inferFromText(title);
}
if (!areaLabel) {
areaLabel = inferFromText(body);
}
if (!areaLabel) {
core.info('No area label inferred for this issue');
return;
}
if (!existingLabels.has(areaLabel)) {
core.info(`Adding inferred area label: ${areaLabel}`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [areaLabel],
});
} else {
core.info(`Issue already has ${areaLabel}`);
}
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'needs-triage',
});
core.info('Removed needs-triage after area assignment');
} catch (error) {
if (error.status === 404) {
core.info('needs-triage label not present');
return;
}
throw error;
}
- name: Remove needs-triage when triaged
if: >-
github.event.action == 'labeled' &&
github.event.label.name != 'needs-triage'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
if (labels.some(l => l.name === 'needs-triage')) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'needs-triage',
});
}