Postman Onboarding: Azure Spec Discovery
Zero-config discovery and export of API specs from Azure services using only your existing Azure credentials. Use it when a service already runs on Azure and you need a source-of-truth Spec Hub specification that Postman onboarding can turn into deterministic collections, OpenAPI-backed contract checks, smoke tests, mocks, monitors, repo artifacts, and CI runs.
Exactly one repo-local native specification (OpenAPI, AsyncAPI, WSDL, WADL, XSD, protobuf, or GraphQL SDL) resolves without calling Azure. Multiple valid local specs fail closed unless an exact committed .postman binding selects one. Otherwise, all available supported Azure candidates enter the same narrowing and ranking flow: authored or exported sources from API Management (APIM), App Service, custom connectors, API Center, and local IaC; and synthesized or embedded sources from Logic Apps, Template Specs, Event Grid, Service Bus, and Function bindings. Container Apps contribute association-only source-control correlation (normalized repo URL + branch), not specification export. No fixed cascade applies among Azure providers. See Supported providers and provider contracts for exact behavior.
When several Azure candidates match, a six-tier narrowing pipeline orders them and fails closed on ambiguity: (1) exact committed .postman binding / explicit full api-id / exact api-center-definition-id; (2) gateway hostname + API base path, optionally with environment / api-version / api-revision selectors; (3) unique API-level select-grade repo tag; (4) self-hosted / workspace gateway assignment narrowing via gateway-id (never select-grade alone); (5) IaC fingerprint and resource-group correlation; (6) generic name ranking. Genuinely ambiguous results surface as a ranked GitHub Step Summary table instead of a guess. The tag tier 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.
Auth and Postman handoff
The action authenticates with DefaultAzureCredential and adds no secret inputs of its own. Grant only the provider-specific read access described in Security and IAM. It never creates, modifies, or deletes Azure resources.
Supported identity forms (ambient credential chain only):
| Mode | How it reaches the action | Notes |
|---|---|---|
| GitHub OIDC (immutable repository ID) | azure/login with federated credential subject repo:<ORG_ID>/<REPO_ID>:... |
Prefer repository-ID subjects so renames cannot retarget the trust. Requires workflow id-token: write. |
| GitHub OIDC (legacy name subject) | azure/login with subject repo:<org>/<repo>:... |
Still accepted by Entra app federated credentials; treat as legacy and migrate to repository-ID subjects when possible. |
| Azure DevOps workload identity federation | Pipeline service connection / AzureCLI@2 (or equivalent) that federates into Entra |
Used by this repo's live-validation pipeline; no personal az login and no committed tenant/subscription secrets. |
| Azure-hosted managed identity | Runner or compute MSI visible to DefaultAzureCredential |
Scope the MSI to the selected subscription or resource group with least-privilege Reader (plus APIM/API Center roles when those providers are used). |
| Service-principal environment credentials | Standard AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET (or certificate) env vars |
No action inputs accept these secrets; configure them on the runner/OIDC login step only. |
Least-privilege / scoped failure behavior:
- Missing or expired tokens, wrong tenant, and invisible subscriptions fail closed before discovery mutates anything.
- Insufficient RBAC on an unselected provider is fail-soft (
skipped:iam); discovery continues with providers the credential can read. - A selected exact export that is denied remains fatal for the run.
- Absence evidence is always scoped to the selected subscription(s); the action never claims that no APIs exist globally.
Cloud endpoint profiles (Public, US Government, China) are constructed and unit-tested locally. They are not live-validated unless committed live evidence says otherwise. API Center sovereign-cloud parity remains unsupported pending live proof. See docs/providers.md.
The optional postman-api-key / postman-access-token inputs exist only to enrich anonymous telemetry with the session account type. They are never used for Azure calls or Postman asset operations.
Usage
jobs:
discover:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v7
- uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- id: spec
uses: postman-cs/postman-azure-spec-discovery-action@v1
- run: echo "Resolved ${{ steps.spec.outputs.source-type }} -> ${{ steps.spec.outputs.spec-path }}"
The rolling @v1 action reference is convenient and receives compatible v1 updates. For immutable workflows, replace it with a reviewed full commit SHA from this repository; no SHA is implied by this example.
Examples
Zero-config resolve-one
- uses: postman-cs/postman-azure-spec-discovery-action@v1
Known APIM API
- uses: postman-cs/postman-azure-spec-discovery-action@v1
with:
api-id: /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ApiManagement/service/<svc>/apis/<api>
discover-many mode
- uses: postman-cs/postman-azure-spec-discovery-action@v1
with:
mode: discover-many
resource-group: payments-rg
discover-estate mode
Enumerates repo-associated Azure resources across the selected subscription scope(s) — one Resource Graph sweep over Resources and ResourceContainers for repo tags (postman:repo, github:repository, GithubOrg/GithubRepo, repo, repository), deduped to an org/repo roster written as repos.json and emitted on repos-json/repo-count. Association only: no spec export, no PRs, no GitHub writes.
- uses: postman-cs/postman-azure-spec-discovery-action@v1
with:
mode: discover-estate
Chaining into Postman API onboarding
- id: spec
uses: postman-cs/postman-azure-spec-discovery-action@v1
- uses: postman-cs/postman-api-onboarding-action@v1
with:
postman-api-key: ${{ secrets.POSTMAN_API_KEY }}
spec-path: ${{ steps.spec.outputs.spec-path }}
GitLab and other CI (portable CLI)
npx @postman-cse/onboarding-azure-spec-discovery \
--subscription-id "$AZURE_SUBSCRIPTION_ID" \
--result-json postman-azure-spec-discovery-result.json \
--dotenv-path azure-spec.env
The CLI exposes every action input as a --kebab-case flag plus CLI-only flags for repo context and discovery tuning (--repo-root, --expected-service-name, --dry-run, --max-candidates, and friends). --help lists all of them.
Inputs
| Name | Description | Required | Default |
|---|---|---|---|
mode |
Discovery mode: resolve-one selects the single best service for this repository; discover-many exports every exportable candidate; discover-estate enumerates repo-associated Azure resources across the selected subscription scope(s) without exporting specs. | no | resolve-one |
subscription-id |
Optional Azure subscription ID used as the discovery enumeration root. When omitted, the single enabled subscription visible to the credential is used; multiple enabled subscriptions require this input or subscription-ids-json. | no | n/a |
subscription-ids-json |
Optional JSON array of Azure subscription IDs for explicit multi-subscription discovery. Conflicts with subscription-id unless both identify exactly the same one ID. Empty keeps single-enabled-subscription behavior. Never auto-enumerates all visible subscriptions. | no | n/a |
resource-group |
Optional resource group that scopes discovery to one group instead of the whole subscription. | no | n/a |
api-id |
Optional full APIM API ARM resource ID for this service. Use this to bypass broader subscription discovery. Supports historical revisions via ;rev=N. | no | n/a |
environment |
Optional deployment environment selector used to disambiguate APIs that share a repository association across environments. | no | n/a |
gateway-id |
Optional self-hosted or workspace gateway id used to narrow APIM API candidates. The value "managed" is rejected. | no | n/a |
api-version |
Optional APIM API version selector used when multiple versions share the same path or repository association. | no | n/a |
api-revision |
Optional APIM API revision selector used when multiple revisions remain after other evidence. | no | n/a |
api-center-definition-id |
Optional full Azure API Center definition ARM resource ID. Exact match only; conflicts with a different .postman apiCenterDefinitionId binding. | no | n/a |
output-dir |
Directory under the repository root where generated specs are written. | no | discovered-specs |
postman-api-key |
Optional service-account PMAK used to mint or re-mint a postman-access-token for telemetry enrichment (account_type). Not used for any Azure or Postman asset operation. | no | n/a |
postman-access-token |
Optional Postman service-account access token, used only to enrich anonymous telemetry with the session account_type. When omitted, postman-api-key alone can mint one for the same purpose. Not used for any Azure or Postman asset operation. | no | n/a |
enable-logic-apps-list-swagger |
Opt-in Consumption Logic Apps native listSwagger POST. Default false. When denied or capability-absent, falls back to Reader-only Request-trigger synthesis unless require-logic-apps-native-swagger is true. | no | false |
require-logic-apps-native-swagger |
When true with enable-logic-apps-list-swagger, a permanent malformed native listSwagger response fails instead of silently synthesizing. Default false. | no | false |
enable-app-service-scm-spec-fetch |
Opt-in retrieval of App Service aiIntegration.ApiSpecPath bytes through the site SCM/VFS endpoint. Default false. Metadata is still surfaced when the path is present. | no | false |
enable-functions-openapi-extension |
Opt-in detection/export of Azure Functions OpenAPI extension endpoints evidenced by function metadata or an explicit declared path. Default false. Never lists host/function keys. | no | false |
enable-runtime-declared-spec-routes |
Opt-in provider for explicitly declared HTTPS specification URLs on App Service, Functions, Container Apps, Static Web Apps, ACI, and AKS workloads. Default false. No blind common-path probing. | no | false |
runtime-declared-spec-targets-json |
Requested subset of runtime-declared HTTPS specification targets when enable-runtime-declared-spec-routes is true. Each entry requires id, name, workloadKind, and https url. Caller JSON alone is rejected; every target must match a committed .postman/repository binding or authorized ARM association evidence. | no | [] |
Outputs
| Name | Description |
|---|---|
resolution-json |
JSON resolution result describing status, source type, confidence, and evidence. |
resolution-status |
Resolution status: resolved or unresolved. |
source-type |
Resolved source type: repo-spec, apim-export, api-center-export, app-service-api-definition, iac-embedded, custom-api-swagger, logic-apps-workflow, template-spec-embedded, event-grid-webhook, service-bus-topic, function-bindings-trigger, runtime-declared-spec, manual-review, discover-many, or discover-estate. |
mapping-confidence |
Numeric confidence score for the selected service candidate. |
spec-path |
Path to the resolved or generated specification when available. |
spec-files-json |
Optional JSON inventory of an authoritative multi-file definition set (schemaVersion 1: root, format, completeness, provenance, sorted files with path/role/bytes/sha256). Empty for single-file, unresolved, or incomplete/partial source sets. Never embeds file content. |
api-id |
Full APIM API ARM resource ID or API Center definition ARM resource ID when those providers resolve; empty for App Service or IaC-local resolutions. |
service-name |
Resolved service name. |
services-json |
discover-many output: JSON array of exported services. |
service-count |
discover-many output: number of exported services. |
export-summary-json |
JSON summary of attempted, exported, failed, and skipped candidates. |
candidates-json |
Ranked ambiguous candidates as JSON when resolution is unresolved with at least two candidates; empty otherwise. |
provider-type |
Provider that produced the resolved spec: apim, api-center, app-service, iac-local, custom-apis, logic-apps, template-specs, event-grid, service-bus, function-bindings, or runtime-declared. |
spec-format |
Format of the resolved spec: openapi-yaml, openapi-json, asyncapi-yaml, asyncapi-json, wsdl, wadl, xsd, protobuf, graphql-sdl, or mcp-json. |
contract-origin |
Compatibility output; always empty in v1. |
contract-metadata-path |
Compatibility output; always empty in v1. |
variant-count |
Compatibility output; always empty in v1. |
derived-openapi-path |
Path to the derived OpenAPI 3.x document when the source was not already OpenAPI 3.x. |
derived-openapi-version |
OpenAPI version of the derived document: 3.0.3 or 3.1.0. |
derived-openapi-completeness |
Whether the derived OpenAPI document is full or partial. |
derived-openapi-format |
Serialization format of the derived OpenAPI document: openapi-json. |
derived-openapi-evidence-json |
JSON array of evidence strings describing how the derived OpenAPI document was produced. |
narrowing-strategy |
Narrowing tier that produced the candidate ordering: gateway-host-path, tag-prefilter, gateway-assignment, iac-fingerprint, rg-correlation, naming-heuristic, or none. |
repos-json |
discover-estate output: JSON array of deduped org/repo associations discovered from repo tags across the selected subscription scope(s); empty otherwise. |
repo-count |
discover-estate output: number of deduped org/repo associations; empty otherwise. |
Supported providers
Provider and route support/validation labels are generated from coverage/route-claims.json. live requires a matching passing case in committed evidence; unit-only and local-only never promote a route; planned: mappings are harness case ids awaiting sanitized passes. See docs/COVERAGE.md.
| Route | Provider | Contract | Validation | Evidence mapping |
|---|---|---|---|---|
repo.spec.named-file |
repo |
authoritative | local-only | local-only rationale |
iac-local.arm-embedded |
iac-local |
authoritative | live | live:iac-single |
apim.http.explicit-api-id |
apim |
authoritative | live | live:apim-explicit-api-id |
apim.http.service-discovery |
apim |
authoritative | live | live:apim-discovery |
apim.http.workspace |
apim |
authoritative | unit-only | local-only rationale |
apim.revision.current-filter |
apim |
authoritative | live | live:apim-version-set |
apim.soap.wsdl-export |
apim |
authoritative | live | live:apim-soap-wsdl |
apim.graphql.sdl |
apim |
authoritative | live | live:apim-graphql-sdl |
apim.websocket |
apim |
unsupported | unsupported | planned:apim-unsupported-websocket |
apim.grpc |
apim |
authoritative | unit-only | local-only rationale |
apim.odata |
apim |
unsupported | unsupported | planned:apim-unsupported-odata |
app-service.api-definition-url |
app-service |
authoritative | live | live:app-service-api-definition |
custom-apis.inline-swagger |
custom-apis |
authoritative | unit-only | planned:custom-apis-inline-swagger |
logic-apps.request-trigger-synthesis |
logic-apps |
partial | live | live:logic-apps-reader-synthesis |
template-specs.embedded-apim |
template-specs |
partial | live | live:template-specs-embedded-apim |
event-grid.webhook-synthesis |
event-grid |
partial | live | live:event-grid-webhook-partial |
service-bus.topic-publish-synthesis |
service-bus |
partial | unit-only | planned:service-bus-topic-partial |
function-bindings.trigger-synthesis |
function-bindings |
partial | unit-only | local-only rationale |
association.postman-repo-tag |
association |
association-only | live | live:apim-clean-repo-tag |
association.fox-github-org-repo |
association |
association-only | live | live:apim-clean-repo-fox-pair |
association.gateway-hostname-hint |
association |
association-only | live | live:apim-gateway-host-path |
association.gateway-api-assignment |
association |
association-only | unit-only | local-only rationale |
association.exact-binding |
association |
association-only | unit-only | local-only rationale |
association.clean-repo-host-path |
association |
association-only | live | live:apim-gateway-host-path |
association.app-service-container-apps-source-control |
association |
association-only | unit-only | local-only rationale |
mode.discover-many |
runtime |
authoritative | live | live:discover-many |
mode.discover-estate |
runtime |
association-only | unit-only | local-only rationale |
resolver.ambiguity |
runtime |
association-only | live | live:ambiguity |
api-center.definition-export |
api-center |
authoritative | unit-only | planned:api-center-openapi-export |
repo.spec.native-formats-r3 |
repo |
authoritative | local-only | planned:local-r3-format-parser-matrix |
logic-apps.native-list-swagger-r4 |
logic-apps |
reconstructed | live | live:logic-apps-list-swagger |
app-service.documented-scm-spec-r4 |
app-service |
authoritative | live | live:app-service-apispecpath-runtime |
function-bindings.openapi-extension-r4 |
function-bindings |
authoritative | unit-only | planned:function-bindings-openapi-extension |
runtime-declared.explicit-https-r4 |
runtime-declared |
authoritative | unit-only | local-only rationale |
platform.sovereign-cloud |
platform |
authoritative | unit-only | local-only rationale |
platform.multi-subscription-scope |
platform |
authoritative | unit-only | local-only rationale |
APIM SOAP and GraphQL APIs are exportable. APIM gRPC is authoritative native protobuf only when the documented API Schema List/Get route returns a text/protobuf schema; otherwise the API stays visible-unsupported for manual review. WebSocket and OData remain visible-unsupported. Service- and workspace-scoped APIM APIs are both enumerated. Azure API Center definitions are discovered and exported as authoritative native contracts; exact definition ARM IDs export directly without broad inventory. Custom connectors without an inline swagger document stay visible as manual-review candidates. App Service and Container Apps source-control records are association-only correlation evidence (not a spec provider). Management-group enumeration remains out of scope.
How it works
- Resolve inputs, repository context, and the selected subscription scope(s) (explicit
subscription-idand/orsubscription-ids-json, or the single enabled subscription). - Resolve exactly one local native specification when present; when more than one valid local native spec remains, fail closed unless an exact committed binding selects one.
- Probe providers fail-soft: an unauthorized provider is skipped (
skipped:iam), an erroring one is skipped (skipped:error), and discovery continues with the rest. - Enumerate lightweight candidate headers (bounded concurrency), narrow with the six-tier pipeline across the full header set (including association-only App Service / Container Apps source-control correlation when available), then hydrate only the selected partition in
resolve-one(or the post-cap set indiscover-many). See Enumeration cost and hydration and Ordering and narrowing. - Export the winner (APIM ARM export, API Center definition export, guarded HTTPS fetch, or IaC extraction). Repository and API Center native formats are preserved and validated for their detected kind (not OpenAPI-only); OpenAPI/Swagger exports still require at least one path. Write under
output-dirconfined to the repository root. - Emit all 25 outputs; ambiguous resolutions additionally render a ranked Step Summary table.
Resources
- docs/providers.md — provider contracts and evidence semantics.
- docs/LIVE_TESTING_RUNBOOK.md — operator-run live Azure validation.
- RELEASE_POLICY.md, SECURITY.md, SUPPORT.md.
Telemetry
The action emits one anonymous completion event per run (action name azure-spec-discovery) through @postman-cse/automation-telemetry-core. The payload never contains Azure subscription, tenant, or resource identifiers, resource names, tags, spec content, URLs, or credentials. Opt out with POSTMAN_ACTIONS_TELEMETRY=off or DO_NOT_TRACK=1.