@postman-cse/onboarding-azure-spec-discovery 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.cjs +149 -42
- package/dist/index.cjs +148 -42
- package/docs/providers.md +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ The action resolves the best specification source for the current repository in
|
|
|
11
11
|
3. **App Service API definition** — a site whose `apiDefinition.url` points at a reachable OpenAPI document.
|
|
12
12
|
4. **Local Azure IaC** — OpenAPI content embedded in ARM/Bicep templates or referenced by `azure.yaml` in the repository.
|
|
13
13
|
|
|
14
|
-
When several Azure candidates match, a four-tier narrowing pipeline (IaC fingerprint, resource-group correlation,
|
|
14
|
+
When several Azure candidates match, a four-tier narrowing pipeline (IaC fingerprint, resource-group correlation, repo-tag prefilter, naming heuristic) orders them, and genuinely ambiguous results surface as a ranked GitHub Step Summary table instead of a guess. The tag prefilter selects outright when exactly one candidate carries a select-grade repo tag for the calling repository: canonical `postman:repo=<org>/<repo>`, the Fox-style `GithubOrg`/`GithubRepo` pair, or any extra keys supplied via the CLI-only `--repo-tag-keys-json` flag. Tag names and values compare case-insensitively (a trailing `.git` is tolerated), and when enumerated candidates carry no matching tags a single targeted Resource Graph tag lookup runs as a fallback — so a gateway tagged with its owning repo resolves automatically, per service repo, with zero further configuration.
|
|
15
15
|
|
|
16
16
|
## Auth and Postman handoff
|
|
17
17
|
|
package/dist/cli.cjs
CHANGED
|
@@ -188459,6 +188459,78 @@ function toAmbiguousViews(ranked) {
|
|
|
188459
188459
|
}));
|
|
188460
188460
|
}
|
|
188461
188461
|
|
|
188462
|
+
// src/lib/resolve/resource-graph-query.ts
|
|
188463
|
+
function escapeKqlString(value) {
|
|
188464
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
188465
|
+
}
|
|
188466
|
+
var CANDIDATE_TYPES = [
|
|
188467
|
+
"microsoft.apimanagement/service/apis",
|
|
188468
|
+
"microsoft.apimanagement/service/workspaces/apis",
|
|
188469
|
+
"microsoft.web/sites",
|
|
188470
|
+
"microsoft.web/customapis",
|
|
188471
|
+
"microsoft.logic/workflows",
|
|
188472
|
+
"microsoft.resources/templatespecs/versions",
|
|
188473
|
+
"microsoft.eventgrid/topics",
|
|
188474
|
+
"microsoft.eventgrid/domains",
|
|
188475
|
+
"microsoft.eventgrid/systemtopics",
|
|
188476
|
+
"microsoft.servicebus/namespaces/topics"
|
|
188477
|
+
];
|
|
188478
|
+
function buildCandidateQuery(resourceGroup) {
|
|
188479
|
+
const typeFilter = CANDIDATE_TYPES.map((type) => `'${type}'`).join(", ");
|
|
188480
|
+
const lines = [
|
|
188481
|
+
"Resources",
|
|
188482
|
+
`| where type in~ (${typeFilter})`
|
|
188483
|
+
];
|
|
188484
|
+
const trimmedGroup = (resourceGroup ?? "").trim();
|
|
188485
|
+
if (trimmedGroup) {
|
|
188486
|
+
lines.push(`| where resourceGroup =~ '${escapeKqlString(trimmedGroup)}'`);
|
|
188487
|
+
}
|
|
188488
|
+
lines.push(
|
|
188489
|
+
"| extend apiType=tostring(properties.apiType), isCurrent=tobool(properties.isCurrent), apiRevision=tostring(properties.apiRevision), apiVersionSetId=tostring(properties.apiVersionSetId), apiDefinitionUrl=tostring(properties.siteConfig.apiDefinition.url)",
|
|
188490
|
+
"| project id, name, type, resourceGroup, tags, apiType, isCurrent, apiRevision, apiVersionSetId, apiDefinitionUrl"
|
|
188491
|
+
);
|
|
188492
|
+
return lines.join("\n");
|
|
188493
|
+
}
|
|
188494
|
+
function tagKeyCaseVariants(key) {
|
|
188495
|
+
const lower = key.toLowerCase();
|
|
188496
|
+
const upperFirst = lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
188497
|
+
const pascalized = lower.split(/[:_-]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
188498
|
+
return [.../* @__PURE__ */ new Set([key, lower, upperFirst, pascalized])];
|
|
188499
|
+
}
|
|
188500
|
+
var FOX_ORG_KEY_VARIANTS = ["GithubOrg", "githuborg", "Githuborg", "GitHubOrg"];
|
|
188501
|
+
var FOX_REPO_KEY_VARIANTS = ["GithubRepo", "githubrepo", "Githubrepo", "GitHubRepo"];
|
|
188502
|
+
function buildRepoTagLookupQuery(repoSlug, repoTagKeys = [], resourceGroup) {
|
|
188503
|
+
const slug = repoSlug.trim().replace(/\.git$/i, "");
|
|
188504
|
+
const slugEscaped = escapeKqlString(slug);
|
|
188505
|
+
const clauses = [];
|
|
188506
|
+
for (const key of ["postman:repo", ...repoTagKeys]) {
|
|
188507
|
+
for (const variant of tagKeyCaseVariants(key)) {
|
|
188508
|
+
clauses.push(`tostring(tags['${escapeKqlString(variant)}']) =~ '${slugEscaped}'`);
|
|
188509
|
+
clauses.push(`tostring(tags['${escapeKqlString(variant)}']) =~ '${slugEscaped}.git'`);
|
|
188510
|
+
}
|
|
188511
|
+
}
|
|
188512
|
+
const [org, ...repoParts] = slug.split("/");
|
|
188513
|
+
const repoName = repoParts.join("/");
|
|
188514
|
+
if (org && repoName) {
|
|
188515
|
+
const orgEscaped = escapeKqlString(org);
|
|
188516
|
+
const repoEscaped = escapeKqlString(repoName);
|
|
188517
|
+
for (const orgVariant of FOX_ORG_KEY_VARIANTS) {
|
|
188518
|
+
for (const repoVariant of FOX_REPO_KEY_VARIANTS) {
|
|
188519
|
+
clauses.push(
|
|
188520
|
+
`(tostring(tags['${orgVariant}']) =~ '${orgEscaped}' and tostring(tags['${repoVariant}']) =~ '${repoEscaped}')`
|
|
188521
|
+
);
|
|
188522
|
+
}
|
|
188523
|
+
}
|
|
188524
|
+
}
|
|
188525
|
+
const lines = ["Resources", `| where ${clauses.join(" or ")}`];
|
|
188526
|
+
const trimmedGroup = (resourceGroup ?? "").trim();
|
|
188527
|
+
if (trimmedGroup) {
|
|
188528
|
+
lines.push(`| where resourceGroup =~ '${escapeKqlString(trimmedGroup)}'`);
|
|
188529
|
+
}
|
|
188530
|
+
lines.push("| project id, name, type, resourceGroup, tags");
|
|
188531
|
+
return lines.join("\n");
|
|
188532
|
+
}
|
|
188533
|
+
|
|
188462
188534
|
// src/lib/resolve/narrowing-pipeline.ts
|
|
188463
188535
|
function slugifyRepoName(repoSlug) {
|
|
188464
188536
|
if (!repoSlug) return [];
|
|
@@ -188494,30 +188566,54 @@ function tierResourceGroupCorrelation(candidates, ctx) {
|
|
|
188494
188566
|
};
|
|
188495
188567
|
}
|
|
188496
188568
|
var CANONICAL_REPO_TAG = "postman:repo";
|
|
188569
|
+
var FOX_ORG_TAG = "githuborg";
|
|
188570
|
+
var FOX_REPO_TAG = "githubrepo";
|
|
188497
188571
|
var GENERIC_TAG_KEYS = ["repo", "repository", "service", "github:repository"];
|
|
188572
|
+
function normalizeTagBag(tags2) {
|
|
188573
|
+
const normalized = {};
|
|
188574
|
+
for (const [key, value] of Object.entries(tags2 ?? {})) {
|
|
188575
|
+
normalized[key.toLowerCase()] = (value ?? "").trim();
|
|
188576
|
+
}
|
|
188577
|
+
return normalized;
|
|
188578
|
+
}
|
|
188579
|
+
function slugEquals(a, b) {
|
|
188580
|
+
const strip = (value) => value.trim().replace(/\.git$/i, "").toLowerCase();
|
|
188581
|
+
const left = strip(a);
|
|
188582
|
+
return left.length > 0 && left === strip(b);
|
|
188583
|
+
}
|
|
188498
188584
|
async function tierTagPreFilter(candidates, ctx) {
|
|
188499
188585
|
if (!ctx.repoSlug) return void 0;
|
|
188500
|
-
const
|
|
188501
|
-
const
|
|
188586
|
+
const repoSlug = ctx.repoSlug;
|
|
188587
|
+
const selectKeys = [CANONICAL_REPO_TAG, ...(ctx.repoTagKeys ?? []).map((key) => key.toLowerCase())];
|
|
188588
|
+
const selectMatches = candidates.filter((candidate) => {
|
|
188589
|
+
const tags2 = normalizeTagBag(candidate.tags);
|
|
188590
|
+
if (selectKeys.some((key) => slugEquals(tags2[key] ?? "", repoSlug))) return true;
|
|
188591
|
+
const org = tags2[FOX_ORG_TAG] ?? "";
|
|
188592
|
+
const repo = tags2[FOX_REPO_TAG] ?? "";
|
|
188593
|
+
return org.length > 0 && repo.length > 0 && slugEquals(`${org}/${repo}`, repoSlug);
|
|
188594
|
+
});
|
|
188595
|
+
const uniqueExact = [...new Set(selectMatches.map((candidate) => candidate.id))];
|
|
188502
188596
|
if (uniqueExact.length === 1) {
|
|
188503
188597
|
return {
|
|
188504
188598
|
ids: uniqueExact,
|
|
188505
188599
|
selectId: uniqueExact[0],
|
|
188506
|
-
evidence: [`Exactly one API
|
|
188600
|
+
evidence: [`Exactly one API carries a select-grade repo tag matching ${repoSlug}`]
|
|
188507
188601
|
};
|
|
188508
188602
|
}
|
|
188509
188603
|
if (uniqueExact.length > 1) {
|
|
188510
188604
|
return {
|
|
188511
188605
|
ids: uniqueExact,
|
|
188512
|
-
evidence: [`Found ${uniqueExact.length} APIs
|
|
188606
|
+
evidence: [`Found ${uniqueExact.length} APIs with select-grade repo tags matching ${repoSlug}`]
|
|
188513
188607
|
};
|
|
188514
188608
|
}
|
|
188515
|
-
const
|
|
188609
|
+
const graphHit = await tierTagGraphFallback(candidates, ctx);
|
|
188610
|
+
if (graphHit) return graphHit;
|
|
188611
|
+
const repoName = repoSlug.split("/").pop()?.trim();
|
|
188516
188612
|
const genericMatches = candidates.filter((candidate) => {
|
|
188517
|
-
const tags2 = candidate.tags
|
|
188613
|
+
const tags2 = normalizeTagBag(candidate.tags);
|
|
188518
188614
|
return GENERIC_TAG_KEYS.some((key) => {
|
|
188519
188615
|
const value = tags2[key] ?? "";
|
|
188520
|
-
return value
|
|
188616
|
+
return slugEquals(value, repoSlug) || repoName !== void 0 && slugEquals(value, repoName);
|
|
188521
188617
|
});
|
|
188522
188618
|
});
|
|
188523
188619
|
if (genericMatches.length > 0) {
|
|
@@ -188528,6 +188624,41 @@ async function tierTagPreFilter(candidates, ctx) {
|
|
|
188528
188624
|
}
|
|
188529
188625
|
return void 0;
|
|
188530
188626
|
}
|
|
188627
|
+
async function tierTagGraphFallback(candidates, ctx) {
|
|
188628
|
+
if (!ctx.resourceGraphClient || !ctx.subscriptionId || !ctx.repoSlug) return void 0;
|
|
188629
|
+
let rows;
|
|
188630
|
+
try {
|
|
188631
|
+
rows = await ctx.resourceGraphClient.queryResources(
|
|
188632
|
+
ctx.subscriptionId,
|
|
188633
|
+
buildRepoTagLookupQuery(ctx.repoSlug, ctx.repoTagKeys ?? [], ctx.resourceGroup)
|
|
188634
|
+
);
|
|
188635
|
+
} catch {
|
|
188636
|
+
return void 0;
|
|
188637
|
+
}
|
|
188638
|
+
if (rows.length === 0) return void 0;
|
|
188639
|
+
const byLowerId = new Map(candidates.map((candidate) => [candidate.id.toLowerCase(), candidate.id]));
|
|
188640
|
+
const matched = [];
|
|
188641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
188642
|
+
for (const row of rows) {
|
|
188643
|
+
const canonical = byLowerId.get(row.id.toLowerCase());
|
|
188644
|
+
if (canonical !== void 0 && !seen.has(canonical)) {
|
|
188645
|
+
seen.add(canonical);
|
|
188646
|
+
matched.push(canonical);
|
|
188647
|
+
}
|
|
188648
|
+
}
|
|
188649
|
+
if (matched.length === 0) return void 0;
|
|
188650
|
+
if (matched.length === 1) {
|
|
188651
|
+
return {
|
|
188652
|
+
ids: matched,
|
|
188653
|
+
selectId: matched[0],
|
|
188654
|
+
evidence: [`Resource Graph tag lookup matched exactly one enumerated candidate for ${ctx.repoSlug}`]
|
|
188655
|
+
};
|
|
188656
|
+
}
|
|
188657
|
+
return {
|
|
188658
|
+
ids: matched,
|
|
188659
|
+
evidence: [`Resource Graph tag lookup matched ${matched.length} enumerated candidate(s) for ${ctx.repoSlug}`]
|
|
188660
|
+
};
|
|
188661
|
+
}
|
|
188531
188662
|
function tierNamingHeuristic(candidates, ctx) {
|
|
188532
188663
|
const slugs = slugifyRepoName(ctx.repoSlug);
|
|
188533
188664
|
if (slugs.length === 0) return void 0;
|
|
@@ -188577,39 +188708,6 @@ async function runNarrowingPipeline(ctx, allCandidates) {
|
|
|
188577
188708
|
return void 0;
|
|
188578
188709
|
}
|
|
188579
188710
|
|
|
188580
|
-
// src/lib/resolve/resource-graph-query.ts
|
|
188581
|
-
function escapeKqlString(value) {
|
|
188582
|
-
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
188583
|
-
}
|
|
188584
|
-
var CANDIDATE_TYPES = [
|
|
188585
|
-
"microsoft.apimanagement/service/apis",
|
|
188586
|
-
"microsoft.apimanagement/service/workspaces/apis",
|
|
188587
|
-
"microsoft.web/sites",
|
|
188588
|
-
"microsoft.web/customapis",
|
|
188589
|
-
"microsoft.logic/workflows",
|
|
188590
|
-
"microsoft.resources/templatespecs/versions",
|
|
188591
|
-
"microsoft.eventgrid/topics",
|
|
188592
|
-
"microsoft.eventgrid/domains",
|
|
188593
|
-
"microsoft.eventgrid/systemtopics",
|
|
188594
|
-
"microsoft.servicebus/namespaces/topics"
|
|
188595
|
-
];
|
|
188596
|
-
function buildCandidateQuery(resourceGroup) {
|
|
188597
|
-
const typeFilter = CANDIDATE_TYPES.map((type) => `'${type}'`).join(", ");
|
|
188598
|
-
const lines = [
|
|
188599
|
-
"Resources",
|
|
188600
|
-
`| where type in~ (${typeFilter})`
|
|
188601
|
-
];
|
|
188602
|
-
const trimmedGroup = (resourceGroup ?? "").trim();
|
|
188603
|
-
if (trimmedGroup) {
|
|
188604
|
-
lines.push(`| where resourceGroup =~ '${escapeKqlString(trimmedGroup)}'`);
|
|
188605
|
-
}
|
|
188606
|
-
lines.push(
|
|
188607
|
-
"| extend apiType=tostring(properties.apiType), isCurrent=tobool(properties.isCurrent), apiRevision=tostring(properties.apiRevision), apiVersionSetId=tostring(properties.apiVersionSetId), apiDefinitionUrl=tostring(properties.siteConfig.apiDefinition.url)",
|
|
188608
|
-
"| project id, name, type, resourceGroup, tags, apiType, isCurrent, apiRevision, apiVersionSetId, apiDefinitionUrl"
|
|
188609
|
-
);
|
|
188610
|
-
return lines.join("\n");
|
|
188611
|
-
}
|
|
188612
|
-
|
|
188613
188711
|
// src/lib/estate/enumerate.ts
|
|
188614
188712
|
var SLUG_TAG_KEYS = ["postman:repo", "github:repository", "repo", "repository"];
|
|
188615
188713
|
var ORG_TAG_KEY = "githuborg";
|
|
@@ -190022,6 +190120,7 @@ function resolveInputs(env = process.env) {
|
|
|
190022
190120
|
const expectedApiIds = [apiId2, ...parseStringArrayJson(expectedApiIdsRaw, "expected-api-ids-json")].filter(
|
|
190023
190121
|
(value) => Boolean(value)
|
|
190024
190122
|
);
|
|
190123
|
+
const repoTagKeysRaw = getInput("repo-tag-keys-json", env) ?? "[]";
|
|
190025
190124
|
return {
|
|
190026
190125
|
mode,
|
|
190027
190126
|
subscriptionId: subscriptionId3,
|
|
@@ -190033,6 +190132,7 @@ function resolveInputs(env = process.env) {
|
|
|
190033
190132
|
expectedApiIds: [...new Set(expectedApiIds)],
|
|
190034
190133
|
apiFilter,
|
|
190035
190134
|
serviceMapping: parseServiceMapping(serviceMappingRaw),
|
|
190135
|
+
repoTagKeys: [...new Set(parseStringArrayJson(repoTagKeysRaw, "repo-tag-keys-json").map((key) => key.toLowerCase()))],
|
|
190036
190136
|
outputDir,
|
|
190037
190137
|
maxCandidates: parseBoundedInteger(getInput("max-candidates", env), "max-candidates", 50, 1, 1e4),
|
|
190038
190138
|
dryRun: parseBoolean(getInput("dry-run", env), "dry-run", false),
|
|
@@ -190438,8 +190538,11 @@ async function runResolveOne(inputs, dependencies) {
|
|
|
190438
190538
|
{
|
|
190439
190539
|
repoSlug: inputs.repoContext.repoSlug,
|
|
190440
190540
|
subscriptionId: subscriptionId3,
|
|
190541
|
+
resourceGroup: inputs.resourceGroup,
|
|
190542
|
+
repoTagKeys: inputs.repoTagKeys,
|
|
190441
190543
|
serviceHints: signals.serviceHints,
|
|
190442
|
-
signals
|
|
190544
|
+
signals,
|
|
190545
|
+
resourceGraphClient: dependencies.createResourceGraphClient?.()
|
|
190443
190546
|
},
|
|
190444
190547
|
enumerated.map((candidate) => ({
|
|
190445
190548
|
id: candidate.id,
|
|
@@ -190551,8 +190654,11 @@ async function runDiscoverMany(inputs, dependencies) {
|
|
|
190551
190654
|
{
|
|
190552
190655
|
repoSlug: inputs.repoContext.repoSlug,
|
|
190553
190656
|
subscriptionId: subscriptionId3,
|
|
190657
|
+
resourceGroup: inputs.resourceGroup,
|
|
190658
|
+
repoTagKeys: inputs.repoTagKeys,
|
|
190554
190659
|
serviceHints: signals.serviceHints,
|
|
190555
|
-
signals
|
|
190660
|
+
signals,
|
|
190661
|
+
resourceGraphClient: dependencies.createResourceGraphClient?.()
|
|
190556
190662
|
},
|
|
190557
190663
|
enumerated.map((candidate) => ({
|
|
190558
190664
|
id: candidate.id,
|
|
@@ -190787,6 +190893,7 @@ var CLI_INPUT_NAMES = [
|
|
|
190787
190893
|
"expected-api-ids-json",
|
|
190788
190894
|
"api-filter",
|
|
190789
190895
|
"service-mapping-json",
|
|
190896
|
+
"repo-tag-keys-json",
|
|
190790
190897
|
"output-dir",
|
|
190791
190898
|
"max-candidates",
|
|
190792
190899
|
"dry-run",
|
package/dist/index.cjs
CHANGED
|
@@ -191045,6 +191045,78 @@ function toAmbiguousViews(ranked) {
|
|
|
191045
191045
|
}));
|
|
191046
191046
|
}
|
|
191047
191047
|
|
|
191048
|
+
// src/lib/resolve/resource-graph-query.ts
|
|
191049
|
+
function escapeKqlString(value) {
|
|
191050
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
191051
|
+
}
|
|
191052
|
+
var CANDIDATE_TYPES = [
|
|
191053
|
+
"microsoft.apimanagement/service/apis",
|
|
191054
|
+
"microsoft.apimanagement/service/workspaces/apis",
|
|
191055
|
+
"microsoft.web/sites",
|
|
191056
|
+
"microsoft.web/customapis",
|
|
191057
|
+
"microsoft.logic/workflows",
|
|
191058
|
+
"microsoft.resources/templatespecs/versions",
|
|
191059
|
+
"microsoft.eventgrid/topics",
|
|
191060
|
+
"microsoft.eventgrid/domains",
|
|
191061
|
+
"microsoft.eventgrid/systemtopics",
|
|
191062
|
+
"microsoft.servicebus/namespaces/topics"
|
|
191063
|
+
];
|
|
191064
|
+
function buildCandidateQuery(resourceGroup) {
|
|
191065
|
+
const typeFilter = CANDIDATE_TYPES.map((type) => `'${type}'`).join(", ");
|
|
191066
|
+
const lines = [
|
|
191067
|
+
"Resources",
|
|
191068
|
+
`| where type in~ (${typeFilter})`
|
|
191069
|
+
];
|
|
191070
|
+
const trimmedGroup = (resourceGroup ?? "").trim();
|
|
191071
|
+
if (trimmedGroup) {
|
|
191072
|
+
lines.push(`| where resourceGroup =~ '${escapeKqlString(trimmedGroup)}'`);
|
|
191073
|
+
}
|
|
191074
|
+
lines.push(
|
|
191075
|
+
"| extend apiType=tostring(properties.apiType), isCurrent=tobool(properties.isCurrent), apiRevision=tostring(properties.apiRevision), apiVersionSetId=tostring(properties.apiVersionSetId), apiDefinitionUrl=tostring(properties.siteConfig.apiDefinition.url)",
|
|
191076
|
+
"| project id, name, type, resourceGroup, tags, apiType, isCurrent, apiRevision, apiVersionSetId, apiDefinitionUrl"
|
|
191077
|
+
);
|
|
191078
|
+
return lines.join("\n");
|
|
191079
|
+
}
|
|
191080
|
+
function tagKeyCaseVariants(key) {
|
|
191081
|
+
const lower = key.toLowerCase();
|
|
191082
|
+
const upperFirst = lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
191083
|
+
const pascalized = lower.split(/[:_-]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
191084
|
+
return [.../* @__PURE__ */ new Set([key, lower, upperFirst, pascalized])];
|
|
191085
|
+
}
|
|
191086
|
+
var FOX_ORG_KEY_VARIANTS = ["GithubOrg", "githuborg", "Githuborg", "GitHubOrg"];
|
|
191087
|
+
var FOX_REPO_KEY_VARIANTS = ["GithubRepo", "githubrepo", "Githubrepo", "GitHubRepo"];
|
|
191088
|
+
function buildRepoTagLookupQuery(repoSlug, repoTagKeys = [], resourceGroup) {
|
|
191089
|
+
const slug = repoSlug.trim().replace(/\.git$/i, "");
|
|
191090
|
+
const slugEscaped = escapeKqlString(slug);
|
|
191091
|
+
const clauses = [];
|
|
191092
|
+
for (const key of ["postman:repo", ...repoTagKeys]) {
|
|
191093
|
+
for (const variant of tagKeyCaseVariants(key)) {
|
|
191094
|
+
clauses.push(`tostring(tags['${escapeKqlString(variant)}']) =~ '${slugEscaped}'`);
|
|
191095
|
+
clauses.push(`tostring(tags['${escapeKqlString(variant)}']) =~ '${slugEscaped}.git'`);
|
|
191096
|
+
}
|
|
191097
|
+
}
|
|
191098
|
+
const [org, ...repoParts] = slug.split("/");
|
|
191099
|
+
const repoName = repoParts.join("/");
|
|
191100
|
+
if (org && repoName) {
|
|
191101
|
+
const orgEscaped = escapeKqlString(org);
|
|
191102
|
+
const repoEscaped = escapeKqlString(repoName);
|
|
191103
|
+
for (const orgVariant of FOX_ORG_KEY_VARIANTS) {
|
|
191104
|
+
for (const repoVariant of FOX_REPO_KEY_VARIANTS) {
|
|
191105
|
+
clauses.push(
|
|
191106
|
+
`(tostring(tags['${orgVariant}']) =~ '${orgEscaped}' and tostring(tags['${repoVariant}']) =~ '${repoEscaped}')`
|
|
191107
|
+
);
|
|
191108
|
+
}
|
|
191109
|
+
}
|
|
191110
|
+
}
|
|
191111
|
+
const lines = ["Resources", `| where ${clauses.join(" or ")}`];
|
|
191112
|
+
const trimmedGroup = (resourceGroup ?? "").trim();
|
|
191113
|
+
if (trimmedGroup) {
|
|
191114
|
+
lines.push(`| where resourceGroup =~ '${escapeKqlString(trimmedGroup)}'`);
|
|
191115
|
+
}
|
|
191116
|
+
lines.push("| project id, name, type, resourceGroup, tags");
|
|
191117
|
+
return lines.join("\n");
|
|
191118
|
+
}
|
|
191119
|
+
|
|
191048
191120
|
// src/lib/resolve/narrowing-pipeline.ts
|
|
191049
191121
|
function slugifyRepoName(repoSlug) {
|
|
191050
191122
|
if (!repoSlug) return [];
|
|
@@ -191080,30 +191152,54 @@ function tierResourceGroupCorrelation(candidates, ctx) {
|
|
|
191080
191152
|
};
|
|
191081
191153
|
}
|
|
191082
191154
|
var CANONICAL_REPO_TAG = "postman:repo";
|
|
191155
|
+
var FOX_ORG_TAG = "githuborg";
|
|
191156
|
+
var FOX_REPO_TAG = "githubrepo";
|
|
191083
191157
|
var GENERIC_TAG_KEYS = ["repo", "repository", "service", "github:repository"];
|
|
191158
|
+
function normalizeTagBag(tags2) {
|
|
191159
|
+
const normalized = {};
|
|
191160
|
+
for (const [key, value] of Object.entries(tags2 ?? {})) {
|
|
191161
|
+
normalized[key.toLowerCase()] = (value ?? "").trim();
|
|
191162
|
+
}
|
|
191163
|
+
return normalized;
|
|
191164
|
+
}
|
|
191165
|
+
function slugEquals(a, b) {
|
|
191166
|
+
const strip = (value) => value.trim().replace(/\.git$/i, "").toLowerCase();
|
|
191167
|
+
const left = strip(a);
|
|
191168
|
+
return left.length > 0 && left === strip(b);
|
|
191169
|
+
}
|
|
191084
191170
|
async function tierTagPreFilter(candidates, ctx) {
|
|
191085
191171
|
if (!ctx.repoSlug) return void 0;
|
|
191086
|
-
const
|
|
191087
|
-
const
|
|
191172
|
+
const repoSlug = ctx.repoSlug;
|
|
191173
|
+
const selectKeys = [CANONICAL_REPO_TAG, ...(ctx.repoTagKeys ?? []).map((key) => key.toLowerCase())];
|
|
191174
|
+
const selectMatches = candidates.filter((candidate) => {
|
|
191175
|
+
const tags2 = normalizeTagBag(candidate.tags);
|
|
191176
|
+
if (selectKeys.some((key) => slugEquals(tags2[key] ?? "", repoSlug))) return true;
|
|
191177
|
+
const org = tags2[FOX_ORG_TAG] ?? "";
|
|
191178
|
+
const repo = tags2[FOX_REPO_TAG] ?? "";
|
|
191179
|
+
return org.length > 0 && repo.length > 0 && slugEquals(`${org}/${repo}`, repoSlug);
|
|
191180
|
+
});
|
|
191181
|
+
const uniqueExact = [...new Set(selectMatches.map((candidate) => candidate.id))];
|
|
191088
191182
|
if (uniqueExact.length === 1) {
|
|
191089
191183
|
return {
|
|
191090
191184
|
ids: uniqueExact,
|
|
191091
191185
|
selectId: uniqueExact[0],
|
|
191092
|
-
evidence: [`Exactly one API
|
|
191186
|
+
evidence: [`Exactly one API carries a select-grade repo tag matching ${repoSlug}`]
|
|
191093
191187
|
};
|
|
191094
191188
|
}
|
|
191095
191189
|
if (uniqueExact.length > 1) {
|
|
191096
191190
|
return {
|
|
191097
191191
|
ids: uniqueExact,
|
|
191098
|
-
evidence: [`Found ${uniqueExact.length} APIs
|
|
191192
|
+
evidence: [`Found ${uniqueExact.length} APIs with select-grade repo tags matching ${repoSlug}`]
|
|
191099
191193
|
};
|
|
191100
191194
|
}
|
|
191101
|
-
const
|
|
191195
|
+
const graphHit = await tierTagGraphFallback(candidates, ctx);
|
|
191196
|
+
if (graphHit) return graphHit;
|
|
191197
|
+
const repoName = repoSlug.split("/").pop()?.trim();
|
|
191102
191198
|
const genericMatches = candidates.filter((candidate) => {
|
|
191103
|
-
const tags2 = candidate.tags
|
|
191199
|
+
const tags2 = normalizeTagBag(candidate.tags);
|
|
191104
191200
|
return GENERIC_TAG_KEYS.some((key) => {
|
|
191105
191201
|
const value = tags2[key] ?? "";
|
|
191106
|
-
return value
|
|
191202
|
+
return slugEquals(value, repoSlug) || repoName !== void 0 && slugEquals(value, repoName);
|
|
191107
191203
|
});
|
|
191108
191204
|
});
|
|
191109
191205
|
if (genericMatches.length > 0) {
|
|
@@ -191114,6 +191210,41 @@ async function tierTagPreFilter(candidates, ctx) {
|
|
|
191114
191210
|
}
|
|
191115
191211
|
return void 0;
|
|
191116
191212
|
}
|
|
191213
|
+
async function tierTagGraphFallback(candidates, ctx) {
|
|
191214
|
+
if (!ctx.resourceGraphClient || !ctx.subscriptionId || !ctx.repoSlug) return void 0;
|
|
191215
|
+
let rows;
|
|
191216
|
+
try {
|
|
191217
|
+
rows = await ctx.resourceGraphClient.queryResources(
|
|
191218
|
+
ctx.subscriptionId,
|
|
191219
|
+
buildRepoTagLookupQuery(ctx.repoSlug, ctx.repoTagKeys ?? [], ctx.resourceGroup)
|
|
191220
|
+
);
|
|
191221
|
+
} catch {
|
|
191222
|
+
return void 0;
|
|
191223
|
+
}
|
|
191224
|
+
if (rows.length === 0) return void 0;
|
|
191225
|
+
const byLowerId = new Map(candidates.map((candidate) => [candidate.id.toLowerCase(), candidate.id]));
|
|
191226
|
+
const matched = [];
|
|
191227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
191228
|
+
for (const row of rows) {
|
|
191229
|
+
const canonical = byLowerId.get(row.id.toLowerCase());
|
|
191230
|
+
if (canonical !== void 0 && !seen.has(canonical)) {
|
|
191231
|
+
seen.add(canonical);
|
|
191232
|
+
matched.push(canonical);
|
|
191233
|
+
}
|
|
191234
|
+
}
|
|
191235
|
+
if (matched.length === 0) return void 0;
|
|
191236
|
+
if (matched.length === 1) {
|
|
191237
|
+
return {
|
|
191238
|
+
ids: matched,
|
|
191239
|
+
selectId: matched[0],
|
|
191240
|
+
evidence: [`Resource Graph tag lookup matched exactly one enumerated candidate for ${ctx.repoSlug}`]
|
|
191241
|
+
};
|
|
191242
|
+
}
|
|
191243
|
+
return {
|
|
191244
|
+
ids: matched,
|
|
191245
|
+
evidence: [`Resource Graph tag lookup matched ${matched.length} enumerated candidate(s) for ${ctx.repoSlug}`]
|
|
191246
|
+
};
|
|
191247
|
+
}
|
|
191117
191248
|
function tierNamingHeuristic(candidates, ctx) {
|
|
191118
191249
|
const slugs = slugifyRepoName(ctx.repoSlug);
|
|
191119
191250
|
if (slugs.length === 0) return void 0;
|
|
@@ -191163,39 +191294,6 @@ async function runNarrowingPipeline(ctx, allCandidates) {
|
|
|
191163
191294
|
return void 0;
|
|
191164
191295
|
}
|
|
191165
191296
|
|
|
191166
|
-
// src/lib/resolve/resource-graph-query.ts
|
|
191167
|
-
function escapeKqlString(value) {
|
|
191168
|
-
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
191169
|
-
}
|
|
191170
|
-
var CANDIDATE_TYPES = [
|
|
191171
|
-
"microsoft.apimanagement/service/apis",
|
|
191172
|
-
"microsoft.apimanagement/service/workspaces/apis",
|
|
191173
|
-
"microsoft.web/sites",
|
|
191174
|
-
"microsoft.web/customapis",
|
|
191175
|
-
"microsoft.logic/workflows",
|
|
191176
|
-
"microsoft.resources/templatespecs/versions",
|
|
191177
|
-
"microsoft.eventgrid/topics",
|
|
191178
|
-
"microsoft.eventgrid/domains",
|
|
191179
|
-
"microsoft.eventgrid/systemtopics",
|
|
191180
|
-
"microsoft.servicebus/namespaces/topics"
|
|
191181
|
-
];
|
|
191182
|
-
function buildCandidateQuery(resourceGroup) {
|
|
191183
|
-
const typeFilter = CANDIDATE_TYPES.map((type) => `'${type}'`).join(", ");
|
|
191184
|
-
const lines = [
|
|
191185
|
-
"Resources",
|
|
191186
|
-
`| where type in~ (${typeFilter})`
|
|
191187
|
-
];
|
|
191188
|
-
const trimmedGroup = (resourceGroup ?? "").trim();
|
|
191189
|
-
if (trimmedGroup) {
|
|
191190
|
-
lines.push(`| where resourceGroup =~ '${escapeKqlString(trimmedGroup)}'`);
|
|
191191
|
-
}
|
|
191192
|
-
lines.push(
|
|
191193
|
-
"| extend apiType=tostring(properties.apiType), isCurrent=tobool(properties.isCurrent), apiRevision=tostring(properties.apiRevision), apiVersionSetId=tostring(properties.apiVersionSetId), apiDefinitionUrl=tostring(properties.siteConfig.apiDefinition.url)",
|
|
191194
|
-
"| project id, name, type, resourceGroup, tags, apiType, isCurrent, apiRevision, apiVersionSetId, apiDefinitionUrl"
|
|
191195
|
-
);
|
|
191196
|
-
return lines.join("\n");
|
|
191197
|
-
}
|
|
191198
|
-
|
|
191199
191297
|
// src/lib/estate/enumerate.ts
|
|
191200
191298
|
var SLUG_TAG_KEYS = ["postman:repo", "github:repository", "repo", "repository"];
|
|
191201
191299
|
var ORG_TAG_KEY = "githuborg";
|
|
@@ -192608,6 +192706,7 @@ function resolveInputs(env = process.env) {
|
|
|
192608
192706
|
const expectedApiIds = [apiId2, ...parseStringArrayJson(expectedApiIdsRaw, "expected-api-ids-json")].filter(
|
|
192609
192707
|
(value) => Boolean(value)
|
|
192610
192708
|
);
|
|
192709
|
+
const repoTagKeysRaw = getInput2("repo-tag-keys-json", env) ?? "[]";
|
|
192611
192710
|
return {
|
|
192612
192711
|
mode,
|
|
192613
192712
|
subscriptionId: subscriptionId3,
|
|
@@ -192619,6 +192718,7 @@ function resolveInputs(env = process.env) {
|
|
|
192619
192718
|
expectedApiIds: [...new Set(expectedApiIds)],
|
|
192620
192719
|
apiFilter,
|
|
192621
192720
|
serviceMapping: parseServiceMapping(serviceMappingRaw),
|
|
192721
|
+
repoTagKeys: [...new Set(parseStringArrayJson(repoTagKeysRaw, "repo-tag-keys-json").map((key) => key.toLowerCase()))],
|
|
192622
192722
|
outputDir,
|
|
192623
192723
|
maxCandidates: parseBoundedInteger(getInput2("max-candidates", env), "max-candidates", 50, 1, 1e4),
|
|
192624
192724
|
dryRun: parseBoolean(getInput2("dry-run", env), "dry-run", false),
|
|
@@ -193034,8 +193134,11 @@ async function runResolveOne(inputs, dependencies) {
|
|
|
193034
193134
|
{
|
|
193035
193135
|
repoSlug: inputs.repoContext.repoSlug,
|
|
193036
193136
|
subscriptionId: subscriptionId3,
|
|
193137
|
+
resourceGroup: inputs.resourceGroup,
|
|
193138
|
+
repoTagKeys: inputs.repoTagKeys,
|
|
193037
193139
|
serviceHints: signals.serviceHints,
|
|
193038
|
-
signals
|
|
193140
|
+
signals,
|
|
193141
|
+
resourceGraphClient: dependencies.createResourceGraphClient?.()
|
|
193039
193142
|
},
|
|
193040
193143
|
enumerated.map((candidate) => ({
|
|
193041
193144
|
id: candidate.id,
|
|
@@ -193147,8 +193250,11 @@ async function runDiscoverMany(inputs, dependencies) {
|
|
|
193147
193250
|
{
|
|
193148
193251
|
repoSlug: inputs.repoContext.repoSlug,
|
|
193149
193252
|
subscriptionId: subscriptionId3,
|
|
193253
|
+
resourceGroup: inputs.resourceGroup,
|
|
193254
|
+
repoTagKeys: inputs.repoTagKeys,
|
|
193150
193255
|
serviceHints: signals.serviceHints,
|
|
193151
|
-
signals
|
|
193256
|
+
signals,
|
|
193257
|
+
resourceGraphClient: dependencies.createResourceGraphClient?.()
|
|
193152
193258
|
},
|
|
193153
193259
|
enumerated.map((candidate) => ({
|
|
193154
193260
|
id: candidate.id,
|
package/docs/providers.md
CHANGED
|
@@ -71,3 +71,5 @@ Not a provider. `mode: discover-estate` runs a separate association-only enumera
|
|
|
71
71
|
## Ordering and narrowing
|
|
72
72
|
|
|
73
73
|
Probe order is `apim`, `app-service`, `custom-apis`, `logic-apps`, `template-specs`, `event-grid`, `service-bus`, `function-bindings`, `iac-local`. Candidates from all available providers enter the same four-tier narrowing pipeline (`iac-fingerprint`, `rg-correlation`, `tag-prefilter`, `naming-heuristic`); the chosen tier is reported in the `narrowing-strategy` output. Tags in the `postman:*` namespace (`postman:repo`, `postman:project-name`) are the strongest ownership signals.
|
|
74
|
+
|
|
75
|
+
The `tag-prefilter` tier treats three tag shapes as select-grade (exactly one match selects at confidence 100): canonical `postman:repo=<org>/<repo>`, the Fox-style `GithubOrg`/`GithubRepo` pair composed as `<org>/<repo>`, and any extra keys registered through the CLI-only `repo-tag-keys-json` input. Tag keys compare case-insensitively (Azure tag names are case-insensitive) and values ignore case plus a trailing `.git`. When no enumerated candidate carries a matching tag bag and a Resource Graph client is available, the tier issues one targeted tag-lookup query (`buildRepoTagLookupQuery`) and intersects the returned ARM IDs with the enumerated candidate set — exactly one intersecting hit is select-grade; query failures narrow nothing (fail-soft). Generic tags (`repo`, `repository`, `service`, `github:repository`) stay narrow-only.
|
package/package.json
CHANGED