Validation Framework

September 1, 2026 · View on GitHub

Status: Work in progress Last updated: 2026-03-31

This document supplements the Validation Framework Requirements with design and implementation detail for developers and architects. It is expected to migrate to the tooling repository alongside the implementation.


1. Rule Metadata Model

1.1 YAML Structure and Examples

Rule metadata is expressed in YAML. Fields that are not constrained are omitted (omitted = applies in all contexts for that dimension). The primary use of the metadata is post-filter and severity mapping: engines run and produce findings, then the framework applies applicability and conditional level to interpret the results in the current context.

id: "042"                    # flat sequential ID, stable across engine changes
name: path-kebab-case        # human-readable name
engine: spectral             # spectral | yamllint | gherkin | python | manual
engine_rule: "camara-parameter-casing-convention"  # native engine rule ID (if applicable)
message_override: null                       # replaces engine message entirely (optional, rare)
hint: "Use kebab-case for all path segments: /my-resource/{resourceId}"  # additional fix guidance alongside engine message (optional)

applicability:               # only list fields that constrain; omitted = no constraint
  branch_types: [main, release]
  trigger_types: [pr, dispatch, local]
  # ... further conditions as needed

conditional_level:
  default: error             # always present
  overrides:                 # only if level varies by context
    - condition:
        target_release_type: [pre-release-alpha]
      level: hint

Conditional level examples:

# "Test definition must be present" — hint by default, warn for RC/public of stable APIs
conditional_level:
  default: hint
  overrides:
    - condition:
        target_api_maturity: [stable]
        target_release_type: [pre-release-rc, public-release]
      level: warn

# "Commonalities compliance check" — warn by default, suppressed for draft APIs
conditional_level:
  default: warn
  overrides:
    - condition:
        target_api_status: [draft]
      level: muted

1.2 Condition Evaluation

applicability match:
  for each field in rule.applicability:
    if field is array: context value must be IN the array (OR)
    if field is range string: context value must satisfy the range expression
    if field is boolean: context value must equal the field value
  all fields must match (AND)
  omitted fields are unconstrained (always match)

conditional level:
  for each override in rule.conditional_level.overrides (in order):
    if override.condition matches context (same logic as applicability):
      return override.level
  return rule.conditional_level.default

1.3 Spectral Pass-Through Principle

The framework uses Spectral's severity names (error, warn, hint) as its native level values. This gives identity mapping for the primary engine:

SpectralFrameworkNotes
errorerrorIdentity
warnwarnIdentity
hinthintIdentity
infohintMapped (rarely used)
offmutedMapped (disable rule)

Spectral rules already include message fields with fix guidance. Therefore, Spectral rules that do not need context-dependent severity or applicability filtering do not require explicit framework metadata entries. Their findings pass through with direct severity mapping and native messages.

Framework metadata is only needed for Spectral rules when:

  • The level should change based on context (e.g., error on release branch, hint on feature branch)
  • The rule should be suppressed in certain contexts (applicability filtering)
  • The engine message should be replaced (message_override) or additional fix guidance should be added (hint)

This minimizes the metadata surface: only rules with context-dependent behavior need explicit entries.

The framework consumes Spectral output as structured data (JSON), not terminal text. This enables programmatic post-filtering, severity remapping, and merging with findings from other engines.

Spectral CLI natively follows external $ref during linting — it resolves references to code/common/, code/modules/, and other local files, validates the referenced content, and reports findings with the correct source file and line number. No pre-bundling is required for Spectral to lint specs with external refs.

External file findings: When Spectral reports a finding on a file under the Commonalities cache prefix (code/common/, e.g., code/common/CAMARA_common.yaml), the finding is downgraded to hint level. These findings are not directly actionable by the API developer — they originate from shared schemas maintained in Commonalities. The hint ensures visibility without blocking the PR. Findings on repo-owned files reached via $ref (e.g. schema fragments under code/modules/) keep their native Spectral severity, since they are actionable by the API developer.

See section 3.1 (Spectral and $ref Interaction) for the broader bundling context.

For further Spectral-specific details, see spectral-integration-notes.md.

1.4 Derived Context Fields

Two per-API context fields are derived from content rather than declared in release-plan.yaml:

target_api_maturity: Derived from apis[].target_api_versioninitial if major version is 0 (v0.x.y), stable if major version >= 1 (vx.y.z). Determines which asset requirements apply per the API Readiness Checklist (e.g., stable public APIs require enhanced test cases and user stories).

api_pattern: Detected from OpenAPI spec content — request-response, implicit-subscription, or explicit-subscription. Detection logic examines paths (subscription endpoints), callbacks, schema names, and content types. Multiple pattern-specific rule sets (REQ, IMP, EXP, EVT categories) depend on this classification. This detection is a cross-cutting capability used by many rules.

1.5 Spectral Migration Potential

Analysis of the deprecated api_review_validator_v0_6.py shows that approximately 40% of its checks are implementable as Spectral rules (single-file OpenAPI pattern matching), and an additional 15% could use Spectral custom JavaScript functions. This includes:

  • Mandatory error response checks (400, 401, 403)
  • Server URL format validation
  • info.version format validation
  • License and security scheme validation
  • ErrorInfo and XCorrelator schema presence
  • Error response structure validation

The main blocker for migrating ~20% of checks to Spectral is the dependency on api_pattern detection — Spectral cannot natively apply rules conditionally based on detected API type. These checks either need custom JS functions that embed the detection logic, or remain as Python checks that use api_pattern from the context.

The Commonalities audit should evaluate each candidate check against the current design guide version before migration.

1.6 Authoritative Schema References

The execution context fields and their allowed values are defined by the following schemas, which are the authoritative sources:

  • release-plan.yaml: artifacts/metadata-schemas/schemas/release-plan-schema.yaml (in ReleaseManagement)
  • release-metadata.yaml: artifacts/metadata-schemas/schemas/release-metadata-schema.yaml (in ReleaseManagement)

The framework must accept exactly the values defined in these schemas. Any change to the schemas must be reflected in the framework's context model.


2. Check Inventory Detail

2.1 Inventory Status

The per-rule inventory is based on a Commonalities audit that examined CAMARA-API-Design-Guide.md and CAMARA-API-Event-Subscription-and-Notification-Guide.md at both r3.4 and r4.1 versions, cross-referenced against the existing Spectral rules (17 CAMARA custom + core OAS), the OWASP rules from tooling#95, the Linting-rules.md maintained in Commonalities, and the deprecated api_review_validator_v0_6.py (80 checks).

The audit identified 106 machine-checkable rules total: 26 already covered by existing Spectral, 17 by OWASP rules (tooling#95, not yet merged), 28 by the v0_6 validator only, and 20 gaps with no current implementation. Of the 106 rules, 19 are r4.x-only (not applicable to r3.4 repositories), 2 changed between versions, and 7 rules listed in Linting-rules.md are not yet implemented in the .spectral.yaml configuration.

Remaining inventory work:

  • Existing rule classification: Map each current Spectral rule to the framework metadata model (applicability, conditional level, hints). The existing Spectral severity levels are assumed valid for now; detailed severity review is deferred.

2.2 Check Areas by Engine

Spectral (existing rules):

  • OpenAPI version enforcement (3.0.3)
  • Naming conventions (see Appendix A for full list: paths, schemas, operationId, plus gaps for properties, enums, tags)
  • Required descriptions (operations, parameters, responses, properties)
  • Reserved words detection (language-specific + HTTP method names in resource paths)
  • Security: no secrets in path/query parameters
  • HTTP method validity, no request body on GET/DELETE
  • Unused components detection
  • Discriminator on oneOf/anyOf (deprecated in r4.x, now hint)
  • Schema type attribute presence

Spectral (new rules needed):

  • info.version format (wip/alpha.n/rc.n/semver)
  • info.title must not contain "API"
  • info.contact and info.termsOfService must be absent
  • externalDocs presence and format
  • x-correlator header presence and pattern
  • Error code format: not numeric, SCREAMING_SNAKE_CASE (r4.x), API_NAME.SPECIFIC_CODE pattern
  • 403 response required on all operations
  • Array items must have description (r4.x)
  • Tag names: Title Case convention
  • Property names: lowerCamelCase (listed in Linting-rules.md, not yet implemented)
  • Enum values: SCREAMING_SNAKE_CASE (listed in Linting-rules.md, not yet implemented)
  • Subscription API schemas: specversion enum, protocol enum, sink HTTPS, notification content-type

OWASP Spectral rules (from tooling#95, r4.x-only):

  • String limits: maxLength/enum/const (warn, target error)
  • Array limits: maxItems (warn, target error)
  • Integer limits: format + minimum/maximum (warn, target error)
  • String restriction: format/pattern/enum/const (warn)
  • Security: no credentials in URL, no HTTP scheme, write-restricted, read-restricted, short-lived access tokens, no numeric IDs, admin security unique
  • Error responses: 401 required (error), error validation response (warn)
  • Additional properties: constrained or disabled (warn)

Python (cross-field, cross-file, and context-dependent):

  • Server URL version consistency with info.version (cross-field)
  • Version must be wip on main, must not be wip on release branches (context-dependent)
  • release-plan.yaml non-exclusivity check (PR diff analysis)
  • release-plan.yaml schema and semantic validation (existing, to be integrated)
  • Error response structure: ErrorInfo schema compliance, $ref resolution (cross-schema)
  • info.description: authorization and error response template sections (normalized text matching)
  • Security scheme validation: openIdConnect named 'openId', notificationsBearerAuth for callbacks
  • Scope naming: api-name:[resource:]action pattern, subscription-specific scopes
  • Event type format: org.camaraproject.<api>.<version>.<event> (subscription APIs)
  • Subscription API structure: required operations, sinkCredential not in responses
  • Test file existence and version alignment (cross-file)
  • CHANGELOG format and link tag-locking (file content analysis)
  • Common schema consistency across API files (cross-file; partially obsolete with bundling)
  • License and x-camara-commonalities consistency across API files (cross-file)
  • Filename conventions: kebab-case, matches api-name (filesystem)
  • CONFLICT error code deprecated warning (r4.x)
  • User story file existence in documentation/API_documentation/ (conditional: mandatory for stable public APIs per API Readiness Checklist)

Gherkin-lint (test definition files):

  • Structural rules: named features and scenarios, unique names, non-empty backgrounds, scenarios with examples
  • Step ordering: Given → When → Then, use And for repeated keywords
  • Tagging: required tags, no restricted tags (@watch, @wip), no duplicates
  • Formatting: indentation, no trailing spaces, no multiple empty lines
  • Limits: max 50 scenarios per file, max 250 character names

Manual + prompt:

  • Data minimization compliance
  • Meaningful description quality (beyond presence checks)
  • User story adequacy (content quality; file existence is checked automatically for stable public APIs)
  • Breaking change justification

Obsolete (handled by release automation):

  • API Readiness Checklist file management (files should no longer be in the repository)
  • Release tag creation and format
  • Version field replacement on release branches (wip → actual version)
  • release-metadata.yaml generation
  • README update with release information

3. Bundling Pipeline

3.1 Spectral and $ref Interaction

Bundling vs full dereferencing

The framework uses bundling (external ref resolution only), not full dereferencing:

  • Bundling: Resolves each external $ref by placing the referenced component into the appropriate components/ subsection of the output document and replacing the external $ref with an internal $ref (e.g., $ref: '../common/CAMARA_common.yaml#/components/schemas/ErrorInfo' becomes $ref: '#/components/schemas/ErrorInfo'). This applies to all component types — schemas, securitySchemes, headers, parameters, responses, examples — not just schemas. When the same external component is referenced multiple times, it is included once and all references point to the single internal definition (deduplication). Internal $ref already present in the source are preserved unchanged.
  • Full dereferencing: Resolves all $ref including internal ones, producing a flat document with zero $ref and massive duplication. The framework must not use full dereferencing.

Preserving internal $ref ensures that:

  • Spectral rules checking component structure, $ref patterns, and #/components/ organization continue to work on bundled output
  • Bundled output remains readable and structurally equivalent to what reviewers expect
  • No Spectral rule changes are needed between copy-paste and bundled models

Any constraints on where API designers may use external vs internal $ref are defined in the bundling design document, not by the validation framework. The framework enforces whatever ref patterns the design document specifies.

Transition period

During migration from copy-paste to the local copy model, both repository types coexist:

  • Copy-paste repos: All schemas inline. Spectral runs directly on source. No bundling needed.
  • $ref repos: Spectral runs on source files — it natively follows external $ref during linting and reports findings with correct source file and line numbers. Bundling produces standalone artifacts as a separate post-validation output step (section 9.7).

No rule changes are needed between the two models. Rule IDs remain stable across the transition (flat namespace from Requirements section 5). Findings from external files are downgraded to hint level (section 1.3).

Bundling is MVP scope

Bundling support for CAMARA_common.yaml via $ref is within MVP scope. Commonalities 0.7.x requires updated common schemas, and the ability to consume them via $ref — with the framework handling bundling transparently — is the key additional value of the validation framework v1 for codeowners. This avoids repeating the difficult-to-validate copy-paste pattern.

In the MVP, some parts may still be manual — providing the correct copy in code/common/ and ensuring it matches the declared commonalities_release version. But the $ref option is available for early adopters, and the framework handles bundling when $ref is detected. Automated cache synchronization and strict version enforcement are post-MVP enhancements.

3.2 Dependency Categories and File Mapping

Three categories of shared schema dependencies exist, each with different characteristics:

Commonalities (well-known, hardcoded in tooling): The Commonalities repository provides shared schemas that are well-known to automation tooling. The primary file is CAMARA_common.yaml (common data types, error responses, headers, security schemes). The exact set of common files and their directory structure within the Commonalities repository is subject to the ongoing restructuring (Commonalities#603) and may evolve — the tooling must not assume a fixed file list but should be configurable per Commonalities version.

The mapping from release-plan.yaml.dependencies.commonalities_release to the correct source files and their locations is built into the tooling. No per-repository configuration is needed.

ICM (version compatibility constraint): Identity and Consent Management schemas are currently contained within Commonalities files — there are no separate ICM files to cache. The dependencies.identity_consent_management_release in release-plan.yaml is a version compatibility constraint (potentially >= x.y.z) rather than a file-caching relationship. The exact nature of this dependency requires further discussion.

Cross-repository commons (possible future extension): In the future, groups of related API repositories may share common schemas beyond those provided by Commonalities (e.g., QoS-related type definitions shared between quality-on-demand and qos-profiles). How such cross-repository schemas would be organized — in a dedicated common repository, within API repositories themselves, or as additional files in the Commonalities repository — is an open question outside the scope of this design. The framework architecture should not preclude this extension, but no implementation is needed until a concrete use case is agreed.

File caching strategy

Which files are cached in code/common/ — demand-driven (only files actually $ref'd) vs declaration-driven (all files from declared dependencies) — is a sync mechanism concern defined in the bundling design document, not a validation framework decision.

The framework's checks are the same regardless: cached files must match their declared source version, and $ref targets must exist.

3.3 Commonalities Version Matrix

Active versions

The framework must support validation rules that vary by Commonalities version. Active versions at the time of writing:

  • r3.4 (Commonalities v0.6.x) — Fall25 meta-release, frozen, maintenance releases only
  • r4.x (Commonalities v0.7.x) — Spring26 meta-release. r4.1 is the release candidate (available now); r4.2 is the upcoming public release and will replace r4.1

Within a version line (r4.x), the latest release is always authoritative. When r4.2 is available, r4.1 becomes obsolete — new releases must target r4.2. If a maintenance release r4.3 follows, it replaces r4.2 for validation purposes.

Future Commonalities major versions (e.g., r5.x for v1.0.0) will add further version lines. The architecture must not assume a fixed number of active versions.

Spectral ruleset selection (pre-selection)

Each Commonalities major version line gets its own Spectral ruleset (e.g., .spectral-r3.4.yaml, .spectral-r4.yaml). The framework reads commonalities_release from release-plan.yaml and selects the matching ruleset before running Spectral.

This avoids running contradicting rules from different Commonalities versions simultaneously, which would produce confusing Spectral output even if the results were filtered afterwards. The r3.4 ruleset is effectively frozen — only maintenance fixes. New rule development targets the current r4.x ruleset.

Framework rule metadata (post-filter with conditionals)

Framework rule metadata uses a single ruleset with commonalities_release range conditions for version-specific behavior. This is appropriate because:

  • Python checks are framework-controlled and do not produce confusing intermediate output
  • Most framework rules apply across versions; only a minority are version-specific
  • Duplicating shared rules into per-version files would create drift risk

The Commonalities audit will identify which rules changed between r3.4 and r4.x. Those rules receive commonalities_release range conditions in their metadata.

3.4 Placeholder Handling

Current state

The current CAMARA_common.yaml contains placeholder patterns (e.g., {{SPECIFIC_CODE}}) that have no defined resolution rules. These should be removed from Commonalities, with API repositories extending shared schemas via allOf instead (per the bundling design document).

Future direction

Placeholder replacement with defined values could be introduced together with bundling as part of a broader transformation pipeline. This could include dynamic variables such as api_version, commonalities_release, commonalities_version, effectively replacing the current "wip" and "/main/" substitutions done by the snapshot transformer. In this model, bundling + transformation (including placeholder replacement) would produce the release-ready artifact.

3.5 Rule Architecture Integration

Bundling integrates into the rule architecture (Requirements section 5) without requiring changes to the context model or rule metadata:

  • No bundling prerequisite for validation: All engines run on source files. Spectral natively follows $ref (section 1.3). Bundling is a separate output step producing standalone artifacts.
  • No new context fields: The context model from Requirements section 2.2 is sufficient. Whether external refs existed and were resolved is an implementation concern, not a rule applicability condition.
  • Cache sync is a check, not context: The cache synchronization validation (section 3.2) produces findings (warning or error depending on profile). It is not a context field consumed by other rules. Cache sync is not yet implemented (post-MVP).
  • Spectral ruleset selection: The commonalities_release field (already in the context model) drives Spectral ruleset pre-selection (section 3.3). No additional metadata is needed.

4. Artifact Surfacing Detail

4.1 Workflow Artifact Naming

  • Bundled specs are uploaded as GitHub workflow artifacts with a naming convention that identifies the API name, branch, and commit SHA
  • Bundled files include a header comment: # For information only - DO NOT EDIT
  • Workflow artifact retention uses the GitHub default (90 days)

4.2 Temporary Branch Model

Workflow artifacts replace the temporary branch model (/tmp/bundled/<branch>-<SHA>) for MVP. Temporary branches may be revisited post-MVP if reviewers need a browsable view of bundled content.

4.3 "wip" Version Handling

Bundling on main leaves info.version as-is — it contains wip as expected for unreleased code. Version replacement on release branches is handled by release automation (snapshot transformer), not by the validation framework's bundling step. The framework validates version correctness per branch type — this is an existing check (section 2.2), not a new bundling-specific requirement.


5. Caller Workflow Design

5.1 Token Resolution Strategy

The framework uses a layered token resolution strategy. The order prioritizes consistent branding (all findings come from the same bot identity) over using whichever token happens to be available:

  1. Snapshot context: camara-release-automation app token — provided by the calling release workflow. The validation framework does not mint this token; it is passed in by the release automation caller.
  2. Validation default: Dedicated validation app bot token — the framework mints an installation token for the current repository. This is the primary path for all PR and dispatch contexts, ensuring consistent bot identity on annotations and comments.
  3. Fallback: GITHUB_TOKEN with write access — used when the validation app is not installed (e.g., dispatch in a fork, or repositories not yet onboarded to the app). Write capability is probed at runtime.
  4. Read-only: Workflow summary and diagnostic artifacts only — when no write token is available.

In normal operation, both upstream PRs and fork PRs show findings from the validation bot. The GITHUB_TOKEN fallback and read-only mode are degraded paths, not the expected default.

5.2 Validation GitHub App

A dedicated GitHub App handles write surfaces for validation. This is a separate app from camara-release-automation — it has a narrower permission scope and a different purpose.

AspectValidation Appcamara-release-automation
PurposePR annotations, comments, commit statusRelease snapshot creation, branch management
Permissionschecks: write, pull-requests: write, statuses: writecontents: write, workflows: write, plus release management
Commits/pushesNeverYes (snapshot branches, tags, release assets)
EasyCLANot needed (no commits)Required (commits to repos with CLA enforcement)

Org-level configuration:

  • vars.VALIDATION_APP_CLIENT_ID — app client ID (org variable)
  • vars.VALIDATION_APP_SLUG — app slug, used for bot username (org variable)
  • secrets.VALIDATION_APP_PRIVATE_KEY — app private key (org secret)

The validation app is introduced from day one (MVP) to establish consistent bot identity and avoid caller workflow changes later.

Relationship to v0 surfacing

The v0 workflow surfaces findings via MegaLinter's built-in reporters (GITHUB_COMMENT_REPORTER, GITHUB_STATUS_REPORTER) and custom actions/github-script steps for release-plan validation. The v1 framework replaces all of these with its own unified surfacing layer. MegaLinter is no longer used as the orchestration layer.

5.3 Trigger and Concurrency YAML

PR trigger

on:
  pull_request:
    branches:
      - main
      - release-snapshot/**
      - maintenance/**
  • main: Standard development PRs. Profile: pr_profile from config (default: standard)
  • release-snapshot/**: Release review PRs created by release automation on snapshot branches. Profile: release_profile from config (default: standard)
  • maintenance/**: Maintenance branch PRs. Profile: pr_profile from config (default: standard)

Default event types (opened, synchronize, reopened) are sufficient. The framework validates code content, not PR metadata — edited (title/body changes) is not needed.

Dispatch trigger

  workflow_dispatch:

Dispatch runs on whatever branch the user selects in the GitHub UI. The framework derives branch type, release context, and all validation parameters from the checked-out branch content.

Concurrency

concurrency:
  group: ${{ github.ref }}-${{ github.workflow }}
  cancel-in-progress: true

Same model as v0: a new push to a PR branch cancels the previous validation run. Dispatch behaves identically — a second dispatch on the same branch cancels the first.

5.4 Permissions Detail

The caller workflow declares the maximum permission set. The reusable workflow inherits these as a ceiling — it cannot elevate above what the caller declares.

permissions:
  checks: write
  pull-requests: write
  issues: write
  contents: read
  statuses: write
  id-token: write
PermissionPurposeFork PR behavior
checks: writeCheck run annotations (findings inline in PR diff)Restricted by GitHub; validation app token used instead
pull-requests: writePR review interactionsRestricted by GitHub; validation app token used instead
issues: writePR comments (PRs use the Issues API for comments)Restricted by GitHub; validation app token used instead
contents: readRepository checkoutAvailable (read-only)
statuses: writeCommit status (per-check context in checks list)Restricted by GitHub; validation app token used instead
id-token: writeOIDC token for tooling ref resolution (section 5.6)May not be granted for fork PRs — see section 5.6

For fork PRs, GITHUB_TOKEN write permissions are restricted by GitHub regardless of what the caller declares. The validation app token (section 5.1) bypasses this restriction because it is minted from the app's own credentials, independent of GITHUB_TOKEN.

5.5 Input Design Detail

The reusable workflow does not accept inputs that duplicate information derivable from the checked-out branch. This prevents contradictions where an input says one thing but branch content says another.

Example of the problem avoided: If the workflow accepted a release_type input, a user could dispatch on main with release_type: public-release while release-plan.yaml on main says target_release_type: pre-release-alpha. The framework would need reconciliation logic, and the user would get confusing results.

Forbidden inputs: branch_type, release_type, api_status, commonalities_version, configurations — all derivable from branch content or from the central configuration file (Requirements section 10).

Consequence for the caller workflow: No per-repo inputs exist. All per-repo configuration (linting config subfolder, enabled features, rollout stage) lives in the central config file read by the reusable workflow. The caller workflow is identical across all repositories, with no with: block needed in standard operation. This makes it protectable via CODEOWNERS or rulesets — nobody ever needs to edit it.

5.6 Ref Resolution

OIDC-based ref resolution (primary)

The reusable workflow resolves its own tooling repository and commit SHA via OIDC claims (job_workflow_sha), following the pattern established in tooling#121. This ensures all internal checkouts (linting config, shared actions at runtime) use the same tooling version that the caller specified.

The caller workflow declares id-token: write to enable OIDC token generation.

Hardcoded version fallback

If OIDC token generation fails (e.g., fork PRs where id-token: write may not be granted), the reusable workflow falls back to its own hardcoded version tag (e.g., v1). This is the same pattern as v0's hardcoded ref: v0.

This fallback is acceptable because:

  • Fork PRs: Contributors do not need pinned-SHA ref resolution — the release version tag (v1) is correct for production validation
  • Feature branch testing: Done by admins and rule developers who have write access, so OIDC works
  • Release automation: Always triggered by codeowners with write access, so OIDC works

The fallback means fork PR validation always uses the published version of the tooling, not a feature branch. This is the expected behavior — only admins test unreleased tooling versions.

Break-glass override

tooling_ref_override — a 40-character SHA input to the reusable workflow. Takes precedence over both OIDC and the hardcoded fallback. Documented as pilot/break-glass only. Same mechanism as release automation.

Resolution order

  1. tooling_ref_override input (if set) — explicit SHA, highest priority
  2. OIDC job_workflow_sha claim (if id-token available) — exact commit SHA
  3. Hardcoded version tag in the reusable workflow (e.g., v1) — always available

5.7 Caller Workflow Template

The caller workflow is identical across all repositories:

name: CAMARA Validation

on:
  pull_request:
    branches:
      - main
      - release-snapshot/**
      - maintenance/**
  workflow_dispatch:

concurrency:
  group: ${{ github.ref }}-${{ github.workflow }}
  cancel-in-progress: true

permissions:
  checks: write
  pull-requests: write
  issues: write
  contents: read
  statuses: write
  id-token: write

jobs:
  validation:
    uses: camaraproject/tooling/.github/workflows/validation.yml@v1
    secrets: inherit

No with: block in standard operation. The caller is a thin pass-through that provides triggers, permissions, and concurrency. All validation logic, configuration, and surfacing are handled by the reusable workflow.

5.8 Version Tagging and Secrets

Version tagging

The reusable workflow uses a floating version tag (v1) analogous to v0's v0 tag. The tag is moved forward as the framework evolves within the v1 major version. Breaking changes (new required permissions, changed caller contract) require a new major version tag.

Secrets

The caller passes secrets: inherit. The reusable workflow uses:

  • GITHUB_TOKEN — inherited, for checkout and fallback write surfaces
  • Org secrets for validation app token minting (VALIDATION_APP_PRIVATE_KEY) — accessed via secrets context
  • Org variables for app identity (VALIDATION_APP_CLIENT_ID, VALIDATION_APP_SLUG) — accessed via vars context

6. Rollout Implementation

6.1 Why Separate Callers

The v0 reusable workflow has a fundamentally different structure (MegaLinter-based, single job, different permissions and output model). A single caller with version switching would require complex conditional logic.

Separate callers allow independent lifecycle: v0 can be removed per-repo after v1 is proven, without coordinating a simultaneous switch. GitHub rulesets can reference the v1 check name independently of v0.

6.2 Central Config Alternatives Analysis

Rationale for config file over alternatives:

  • Org variable with repo list (rejected): JSON arrays in org variables become unwieldy at 60+ repos and hit variable size limits. Not PR-reviewable.
  • Per-repo variable (rejected): Requires touching each repository to enable. Violates UC-13 — central administration without per-repo configuration changes.
  • Caller version tag (rejected): Would require editing the caller workflow per-repo, undermining the identical-caller-across-all-repos design (section 5.7).
  • Tooling config file (chosen): Version-controlled, PR-reviewable, scalable. Adding a repo is one line in a YAML file. Can hold per-repo settings beyond enable/disable (linting config subfolder, rollout stage). Satisfies UC-13 — no per-repo config changes needed.

Central config file schema

The central config file lives in the tooling repository and maps each API repository to its rollout stage and profile settings. Spectral ruleset selection is not a per-repo config field — it is derived from commonalities_release in the repository's own release-plan.yaml (section 3.3).

# validation-config.yaml in camaraproject/tooling
version: 1
defaults:
  stage: disabled             # default for repositories not listed below
fork_owners: [hdamker, rartych]  # GitHub users allowed to test in their forks
repositories:
  QualityOnDemand:
    stage: enabled            # runs on PRs and dispatch
    pr_profile: standard      # errors block on PRs
    release_profile: standard # errors block on release gates
  DeviceLocation:
    stage: enabled
  ReleaseTest:
    stage: enabled
  NetworkSliceBooking:
    stage: advisory           # dispatch only
FieldTypeDescription
versionintegerSchema version (currently 1). Allows future schema evolution without breaking existing configs.
defaults.stageenumDefault stage for unlisted repositories: disabled, advisory, enabled.
defaults.pr_profileenumDefault profile for PR validation: advisory, standard, strict. If omitted, defaults to standard.
defaults.release_profileenumDefault profile for pre-snapshot and release review PR validation: advisory, standard, strict. If omitted, defaults to standard.
fork_ownersarray of stringsGitHub usernames allowed to run validation in their forks. When the workflow runs in a fork owned by a listed user, stage is overridden to enabled regardless of the repository's upstream stage (section 8.2).
repositories.<name>.stageenumPer-repo rollout stage override. Same values as defaults.stage.
repositories.<name>.pr_profileenumPer-repo PR profile override. Same values as defaults.pr_profile.
repositories.<name>.release_profileenumPer-repo release profile override. Same values as defaults.release_profile.

Stage mapping (see also Requirements section 10.3):

StageConfig valueBehavior
0 (dark)disabledCaller deployed but reusable workflow exits immediately
1 (advisory)advisoryRuns on dispatch only, advisory profile, nothing blocks
2 (enabled)enabledRuns on PRs and dispatch, profile from config

Merge blocking is enforced by a GitHub ruleset (section 6.3), not by a config stage. Any repository at stage enabled can optionally have a blocking ruleset — the two concerns are independent.

Extensibility: Additional per-repo fields (e.g., features, optional overrides) can be added without a version bump — new fields are additive. Future candidates include spectral_ruleset_override and extra_checks.

Self-validation: The reusable workflow validates the config file against a JSON Schema on every run. An invalid config file is a hard failure with an explicit error message naming the file and the problematic entry. This catches typos, unknown stage values, and schema drift before any validation logic runs.

6.3 GitHub Rulesets for Blocking

A new ruleset (org-level or per-repo) requires the v1 validation check to pass before PR merge. The pattern follows the existing release-snapshot-protection ruleset.

  • The ruleset references the v1 workflow by check name (workflow name or job name)
  • The camara-release-automation app can be a bypass actor for automated release PRs that need to merge without validation
  • Ruleset management can reuse the existing admin script pattern (apply-release-rulesets.sh)

6.4 Rollout Sequence

  1. Test repo: ReleaseTest — full cycle through stages 0-3, validates all surfacing paths
  2. Template: Template_API_Repository — ensures new repos get v1 caller from creation
  3. Pilot API repos: 2-3 active repos with engaged codeowners
  4. Batch rollout: Remaining repos, coordinated with v0 removal

6.5 Feature Branch Testing

Admins and rule developers test validation changes on feature branches before merging to main and tagging (UC-15).

The caller workflow in a test repo is temporarily pointed at the feature branch:

uses: camaraproject/tooling/.github/workflows/validation.yml@feature-branch

Ref resolution (section 5.6) ensures internal checkouts match — admins have write access, so OIDC resolves the exact SHA. tooling_ref_override is available as break-glass for composite action changes not on the workflow branch.

Rule developers can dispatch validation on existing release branches in a test repo while calling the feature-branch version of the reusable workflow. This validates rule changes against known-good content before merging (UC-10).

No special framework support is needed — pinned refs are a standard GitHub Actions feature. The framework's only requirement is correct ref resolution (section 5.6).

6.6 Caller Update Strategy

The v1 caller workflow is deployed by copying from Template_API_Repository to each API repo. Since the caller is identical across all repos (section 5.7), deployment is a mechanical copy — no per-repo customization.

Deployment can be batched using the existing admin tooling pattern (scripted multi-repo operations). The caller can be deployed to all repos at once in stage 0 (dark) — it has no effect until the repo is listed in the config file.

6.7 Relationship to tooling#121

tooling#121 fixes ref consistency in the existing v0 reusable workflow. It validates the OIDC ref resolution pattern that v1 reuses and adds the tooling_ref_override break-glass input. tooling#121 does not change the v0 caller — callers still call @v0. The v1 reusable workflow reuses the same ref resolution pattern with the hardcoded version fallback (section 5.6).


7. Release Automation Implementation

7.1 Two-Gate Defense-in-Depth Model

The validation framework and release automation form a two-gate model that provides independent validation at two points in the release lifecycle:

Gate 1: Pre-snapshot validation (section 7.4) — Validation runs on the source branch before snapshot creation, invoked by the /create-snapshot command. This catches issues in the source content (API specs, release-plan, test files) before the snapshot becomes immutable.

Gate 2: Release review PR validation (section 8.6) — When the release review PR is created on the snapshot branch, the validation workflow triggers automatically with full scope. This catches issues introduced by the snapshot creation process (bundling errors, transformation mistakes, version string malformations).

Together, the two gates ensure that both the source content and the transformed snapshot content are validated independently.

Bundling responsibility

Release automation bundles independently during snapshot creation. On the snapshot branch, source API definition files (which contain $ref to code/common/ and code/modules/) are replaced with bundled standalone specs. This is the "swap strategy" described in the bundling design document: the familiar filename (api-name.yaml) is retained, but the content is the fully resolved, consumer-ready artifact.

The validation framework does not produce bundled specs for release automation consumption. Its bundled artifacts (section 9.7) are diagnostic and reviewer aids only. This clean separation keeps validation stateless — it produces findings and diagnostic files, release automation owns repository state.

Mechanical transformations

During snapshot creation, release automation applies version-specific changes after bundling:

  • info.version replacement (wip → calculated release version)
  • Server URL version updates
  • x-camara-commonalities version field
  • Feature file version updates
  • Link replacements

These transformations are release automation's responsibility. The validation framework validates the source content; release automation produces the final release-ready content. Gate 2 then validates the transformed result.

7.2 Token and Findings Output for Pre-Snapshot

Token: The camara-release-automation app token is available in the release automation workflow context. The validation framework runs within this context via the shared run-validation composite action (section 8.5).

Findings output: Validation findings are reported in the bot's response comment on the Release Issue. The release automation workflow reads the validation output files (summary.md, result.json) from the shared action's output directory and includes the findings in its Release Issue comment.

7.3 File Restriction Check

The context field is_release_review_pr (Requirements section 2.2) serves dual roles: profile selection and applicability condition. The file restriction check is the only check currently using it as an applicability condition.

# File restriction check — release review PR only
id: "060"
name: release-review-file-restriction
engine: python
applicability:
  is_release_review_pr: true
conditional_level:
  default: error
description: "Release review PR may only modify CHANGELOG files"
hint: "Only CHANGELOG.md (or CHANGELOG/ directory) may be modified on the release review branch. README Release Information is committed with the snapshot; README, API specs and other files are immutable on the snapshot branch."  # additional guidance shown alongside the engine message

7.4 Pre-Snapshot Invocation Detail

Release automation invokes the validation framework via the shared run-validation composite action (section 8.5) with mode: pre-snapshot. The framework reads release-plan.yaml from the checked-out branch to derive all context fields (target release type, API statuses, Commonalities version, etc.).

When mode is pre-snapshot, the framework:

  • Sets trigger_type to release-automation
  • Selects the release_profile from the central config (default: standard)
  • Runs the full engine pipeline on source files
  • Writes output files (result, summary, findings) to the output directory

The release automation workflow reads the should_fail output from the shared action. If true, snapshot creation is aborted and findings are included in the Release Issue comment. If false, release automation proceeds with bundling and snapshot creation.

The detailed output model (findings format, artifact structure) is defined in section 9.


8. End-to-End Processing Flow

This section describes the reusable workflow's internal structure and the validation engine's processing pipeline. It covers what happens from the moment the workflow starts to the point where raw findings are collected — output formatting and surfacing are in section 9.

8.1 Job Architecture

The reusable workflow uses a single-job design. All steps run sequentially within one job.

This differs from release automation's multi-job architecture. Release automation splits into separate jobs because its phases have fundamentally different conditional logic (trigger classification → state derivation → command validation → command execution, where each command is a separate job). The validation workflow has a single linear pipeline — context flows naturally between steps via environment variables and the shared file system within one job. Multi-job would require serializing the context object through job outputs and re-checking out the repository in each job, adding complexity without benefit.

Step sequence

#StepMechanismSkip condition
1Checkout repository contentinlineNever
2Resolve tooling ref and checkout toolinginline (github-script)Never
3Setup Python and NodeinlineNever
4Detect release-plan changesinlineNon-PR triggers
5Run validationrun-validation shared actionNever (exits internally if disabled)
6Mint validation app tokeninlineNon-PR triggers or secret unavailable
7Create Check Runinline (github-script)No write token
8Emit annotation fallbackinlineCheck Run succeeded
9Post findings to PRinline (github-script)No write token
10Upload diagnosticsinlineNever
11Bundle API specsinline (redocly)No external $ref detected
12Upload bundled specsinlineNo bundled specs produced
13Check resultinlineNever

Step 5 encapsulates the core validation pipeline via a shared composite action (section 8.5): config gate → context builder → engine orchestration → post-filter → output files. Steps 6–9 handle findings surfacing using the output files from step 5. Steps 11–12 produce bundled artifacts as a separate post-validation concern.

8.2 Checkout Strategy

Repository content checkout

- uses: actions/checkout@v6
  with:
    fetch-depth: 0

Full history (fetch-depth: 0) is needed for:

  • PR diff analysis: identifying which files changed (used by release_plan_changed detection and file restriction check)
  • Branch type detection: examining github.base_ref for PR triggers

For dispatch triggers, fetch-depth: 1 would suffice, but a single checkout configuration avoids trigger-specific branching.

Tooling checkout

The tooling repository is checked out via sparse checkout at the resolved ref (section 5.6):

- uses: actions/checkout@v6
  with:
    repository: camaraproject/tooling
    ref: ${{ steps.resolve-ref.outputs.tooling_sha }}
    sparse-checkout: |
      linting/config
      validation
      shared-actions
    path: .tooling

The checkout lands in .tooling/ within the workspace. This path is used by all subsequent steps to locate linting configs, validation scripts, shared actions, and the central config file.

The sparse checkout scope includes:

  • linting/config/ — Spectral rulesets (per Commonalities version), yamllint config, gherkin-lint config
  • validation/ — Python validation scripts, rule metadata, central config file
  • shared-actions/ — composite actions consumed at runtime

Central config gate

Immediately after the tooling checkout, the workflow reads the central config file (section 6.2) and looks up the current repository:

  1. Validate the config file against its JSON Schema
  2. Extract the repository name from github.repository without the owner prefix (e.g., camaraproject/QualityOnDemandQualityOnDemand)
  3. Look up repositories.<repo-name>.stage (fall back to defaults.stage)
  4. Fork override: If the workflow is running in a fork (github.repository_owner is not camaraproject or GSMA-Open-Gateway) and the owner is listed in fork_owners → override stage to enabled. If the owner is not listed → keep the resolved stage (typically disabled during early rollout, which exits the workflow)
  5. Profile resolution: Resolve pr_profile and release_profile from the repository's config entry, falling back to defaults.pr_profile and defaults.release_profile, then to standard if omitted
  6. If stage is disabled: exit the workflow with a notice in the summary ("Validation is not enabled for this repository") and set the overall result to skipped
  7. If stage is advisory and trigger is pull_request: exit similarly ("Validation is in advisory mode — use workflow_dispatch to run")
  8. Otherwise: continue, passing the resolved stage and profile settings to subsequent steps

The fork override (step 4) enables trusted testers to run full validation in their forks even before the upstream repository has been onboarded. This is especially useful during early rollout when most repos are still at disabled. Once upstream repos move to enabled, the fork_owners list becomes less relevant — forks inherit the upstream stage.

8.3 Context Builder

The context builder assembles a unified context object from multiple sources. It follows the same principle as release automation's BotContext: all fields are always present in the output, with missing or inapplicable values defaulting to empty strings or null. Downstream consumers never need to handle missing keys.

Branch type derivation

PatternBranch typeSource
mainmaingithub.base_ref (PR) or github.ref_name (dispatch)
release-snapshot/**releaseSame
maintenance/**maintenanceSame
Everything elsefeatureSame

For pull_request triggers, branch type is derived from the PR's target branch (github.base_ref), not the source branch. This determines which validation rules apply to the content being merged.

For workflow_dispatch, it is derived from the checked-out branch (github.ref_name).

Trigger type derivation

SourceTrigger type
github.event_name == 'pull_request'pr
github.event_name == 'workflow_dispatch'dispatch
mode input == pre-snapshotrelease-automation

The mode input (section 7.4) is the only way the trigger type release-automation is set. In standard caller workflow operation, mode is not set and trigger type comes from the GitHub event.

Profile selection

Profile selection is config-driven for PR and release contexts. The central config (section 6.2) provides pr_profile and release_profile per repository, with defaults.

Trigger typeConditionProfile sourceDefault
dispatchHardcodedadvisory
localHardcodedadvisory
pris_release_review_pr = truerelease_profile from configstandard
prpr_profile from configstandard
release-automationrelease_profile from configstandard

If the profile input (Requirements section 9.2) is explicitly set, it overrides the config-driven profile. This allows dispatch users to preview what a different profile would flag.

release-plan.yaml parsing

The context builder reads release-plan.yaml from the checked-out branch and validates it against the release-plan JSON Schema (section 1.6). Parsing extracts:

  • target_release_type — determines release context for all rules
  • dependencies.commonalities_release — drives Spectral ruleset selection (section 3.3)
  • dependencies.identity_consent_management_release — version constraint (section 3.2)
  • Per-API fields: api_name, target_api_version, target_api_status

If release-plan.yaml is absent (valid for repositories not yet using release automation, or feature branches), the context builder produces a minimal context with target_release_type: null. Release-plan-specific checks are skipped; Spectral and yamllint still run. If the file is present but invalid, schema violations are reported as findings and processing continues with whatever fields could be parsed.

Derived per-API fields

For each API declared in release-plan.yaml:

  • target_api_maturity: Derived from target_api_versioninitial if major version is 0, stable if >= 1 (section 1.4)
  • api_pattern: Detected from the corresponding OpenAPI spec file content in code/API_definitions/ (section 1.4). Detection runs during context building because many downstream rules depend on it

Spectral ruleset selection

The Commonalities version declared in release-plan.yaml (dependencies.commonalities_release) determines which Spectral ruleset is used (section 3.3). The framework maps the release tag to a version line:

  • commonalities_release matching r3.*.spectral-r3.4.yaml
  • commonalities_release matching r4.*.spectral-r4.yaml
  • Future version lines added as new rulesets are created
  • If commonalities_release is absent or unresolvable: default to the latest ruleset (currently r4.x)

This selection is derived from the repository's own release-plan.yaml, not from the central config. Different branches of the same repo can target different Commonalities versions.

Detection of PR-specific context

  • is_release_review_pr: True when the PR targets a release-snapshot/** branch (section 7.3)
  • release_plan_changed: True when release-plan.yaml is in the PR diff. Detected via the GitHub API's changed files list for the PR. Relevant for the release-plan non-exclusivity check (section 2.2)

Fork scenarios

The validation workflow can run in forks as well as upstream:

  • Fork dispatch: A codeowner dispatches validation on their fork to check work before creating an upstream PR. Org secrets are not available in forks, so the validation app token cannot be minted — token resolution falls back to GITHUB_TOKEN, which has full write access within the fork's own context.
  • Fork-internal PRs: PRs within a fork (feature branch → fork's main) trigger the caller if deployed. Same degraded token situation as fork dispatch.
  • Fork-to-upstream PRs: The standard contribution path. The workflow runs in the upstream repo context (triggered by pull_request), so github.repository is the upstream repo. Org secrets are available to the reusable workflow.

The context builder does not expose fork identity as a context field — no validation rule needs it. Fork vs. upstream is a workflow-level concern handled by token resolution (section 5.1).

Context object structure

# Validation context — all fields always present
repository: "QualityOnDemand"    # repo name without owner prefix
branch_type: "main"              # main | release | maintenance | feature
trigger_type: "pr"               # pr | dispatch | release-automation | local
profile: "standard"              # advisory | standard | strict
stage: "enabled"                 # from central config (disabled | advisory | enabled)

# Release context (from release-plan.yaml; null if absent)
target_release_type: "pre-release-rc"
commonalities_release: "r4.1"
icm_release: ">= r1.0"

# PR-specific (null for dispatch)
is_release_review_pr: false
release_plan_changed: true
pr_number: 42

# Per-API contexts (array; empty if no release-plan.yaml)
apis:
  - api_name: "qos-booking"
    target_api_version: "1.0.0-rc.1"
    target_api_status: "maintained"
    target_api_maturity: "stable"     # derived
    api_pattern: "request-response"   # detected from spec
    spec_file: "code/API_definitions/qos-booking.yaml"
  - api_name: "quality-on-demand"
    target_api_version: "0.11.0-alpha.1"
    target_api_status: "initial"
    target_api_maturity: "initial"
    api_pattern: "explicit-subscription"
    spec_file: "code/API_definitions/quality-on-demand.yaml"

# Workflow metadata
workflow_run_url: "https://github.com/camaraproject/QualityOnDemand/actions/runs/12345"
tooling_ref: "abc1234def5678..."

The context object is serialized as JSON and made available to subsequent steps via an environment variable or step output.

8.4 Engine Orchestration

All engines run sequentially on source files within the single job. Each engine captures its findings, and the combined findings are passed to the post-filter.

Sequential execution is appropriate because:

  • GitHub Actions steps within a job are inherently sequential
  • Parallel execution would require multi-job with artifact passing — overhead that exceeds the time saved for a 1-3 minute pipeline
  • No inter-engine data dependencies exist (Spectral natively follows $ref, so bundling is not a prerequisite)

The engine sequence is:

yamllint

Validates YAML syntax of all API spec files (code/API_definitions/*.yaml). Uses a configuration from the tooling checkout (linting/config/.yamllint.yaml). Invalid YAML produces error-level findings. yamllint runs first so that syntax issues are reported before attempting OpenAPI-level linting.

Spectral

Runs the version-selected Spectral ruleset (section 3.3) on source files with --format json for structured output:

spectral lint \
  --ruleset .tooling/linting/config/.spectral-r4.yaml \
  --format json \
  code/API_definitions/*.yaml \
  > spectral-output.json

The JSON output provides per-finding: code (rule name), path (file), message, severity (0=error, 1=warn, 2=info, 3=hint), range.start.line, range.start.character.

Spectral CLI natively follows external $ref during linting. For $ref repos, findings on files under the Commonalities cache prefix (code/common/) are downgraded to hint level (section 1.3) since they are not directly actionable by the API developer; findings on repo-owned files (e.g. code/modules/) keep their native severity.

Python checks

Python checks produce findings directly in the common findings model (section 8.4.1). They receive the full context object and have access to the file system for cross-file analysis.

Check categoryTarget
Cross-field consistencyContext + spec content
Version checks (info.version, server URL)Context + spec content
release-plan.yaml semantic checksrelease-plan.yaml
Error response structureContext + spec content
API pattern-specific checksContext + spec content + api_pattern
File naming and structureRepository layout

gherkin-lint

Validates test definition files in code/Test_definitions/*.feature. Skipped if no .feature files are found. Findings are normalized into the common model by the framework.

Cache sync validation

Cache sync validation (comparing code/common/ content against the declared commonalities_release version) is not yet implemented. This is a post-MVP enhancement — when implemented, it will run as a Python check producing findings at the appropriate severity level.

8.4.1 Common Findings Model

All engine outputs are normalized into a common findings format before post-filtering:

- rule_id: "042"                   # framework rule ID (sequential, stable)
  engine: spectral                  # spectral | yamllint | gherkin | python
  engine_rule: "camara-parameter-casing-convention"  # native engine rule name
  level: error                      # engine-reported level (before post-filter)
  message: "Path segment 'qualityOnDemand' should be kebab-case"
  path: "code/API_definitions/quality-on-demand.yaml"
  line: 47                          # line in source file
  column: 5                         # column (if available from engine)
  api_name: "quality-on-demand"         # which API this finding belongs to
  hint: "Use kebab-case: /quality-on-demand/{sessionId}"  # additional fix guidance (from rule metadata, optional)
FieldSource — SpectralSource — yamllintSource — gherkin-lintSource — Python
rule_idLooked up from rule metadata by engine_rule; auto-assigned if no metadataLooked up similarlyLooked up similarlySet directly by check
engine"spectral""yamllint""gherkin""python"
engine_rulecode field from JSONRule name from outputRule name from outputCheck function name
levelMapped from severity integer: 0→error, 1→warn, 2→hint, 3→hintMapped from yamllint severityMapped from gherkin-lint severitySet directly
messagemessage fieldError message textError message textSet directly
pathsource fieldFile path from outputFile path from outputSet directly
linerange.start.line (0-indexed → 1-indexed)Line number from outputLine number from outputSet directly
columnrange.start.characterColumn from outputColumn from output (or null)Set directly (or null)
api_nameDerived from file pathDerived from file pathDerived from file pathSet directly
message (post-filter)If rule metadata has message_override: replaces engine message. Otherwise: engine message preservedSameSameSet directly (no override)
hintFrom rule metadata hint field (optional additional fix guidance); absent if no metadata or no hint definedFrom rule metadataFrom rule metadataSet directly (optional)

Spectral rules without explicit framework metadata entries pass through with identity mapping (section 1.3): rule_id is auto-assigned, the engine's message is preserved, no hint is added, and the level maps directly. This means the check inventory does not need to be complete before the framework can run — new Spectral rules work immediately.

8.5 Composite Action Boundaries

The primary composite action is run-validation, which encapsulates the full validation pipeline (config gate → context building → engine orchestration → post-filter → output file generation). This action is shared between the standalone validation workflow (step 5 in section 8.1) and the release automation workflow's pre-snapshot gate (section 7.4).

ActionPurposeKey inputsKey outputs
run-validationFull validation pipeline (section 8.4)repo_path, tooling_path, mode, profile, release_plan_changed, tooling_refresult, should_fail, summary, output_dir

The action installs Python and Node dependencies, runs the Python orchestrator, writes the workflow summary, and parses the result file into step outputs. The orchestrator internally handles config gate, context building, engine invocation, post-filter, and output file generation as a single Python process.

Tooling ref resolution (section 5.6) is implemented as inline workflow steps using github-script, not as a separate composite action. The validate-release-plan action from release automation is reused rather than reimplemented.

Per-engine composite actions (pre-bundling, bundling, full-validation, process-findings) were not created — the Python orchestrator handles engine sequencing internally, which simplifies dependency management and testing.

8.6 Release Review PR — Full Scope Validation

When is_release_review_pr is true, the framework runs the complete validation pipeline — all engines at full scope. This provides the second gate in the two-gate defense-in-depth model (section 7.1).

Profile: Determined by release_profile from the central config (same as pre-snapshot gate).

Context on snapshot branches: On the snapshot branch, release-plan.yaml is absent — it is removed by the snapshot creator during snapshot creation. The context builder falls back to release-metadata.yaml (generated by release automation and present on the snapshot branch) to populate the validation context:

  • commonalities_release for Spectral ruleset selection (section 3.3)
  • Per-API metadata (target_api_version, target_api_status) for Python check context

File restriction check: The is_release_review_pr flag remains an applicability condition for the file restriction check (section 7.3), which errors if files outside CHANGELOG and README are modified.

Defense-in-depth rationale: Running full scope on release-review PRs catches issues introduced by the snapshot creation process — bundling errors, version transformation mistakes, server URL malformations — that could not have been caught by the pre-snapshot gate, which validates source content before these transformations occur.


9. Output Pipeline

This section covers the output pipeline: post-filter processing, output formatting, truncation, and error handling. It takes the raw findings from all engines and produces the user-visible output. The output pipeline runs within the run-validation shared action (section 8.5) and writes output files consumed by downstream workflow steps (section 8.1, steps 6-12).

9.1 Post-Filter Processing

The post-filter evaluates each raw finding against the rule metadata (section 1) and the current validation context (section 8.3). It produces a filtered, severity-adjusted findings list ready for output formatting.

Processing steps per finding:

  1. Rule metadata lookup: Match the finding to its framework rule metadata entry by engine + engine_rule. Python-produced findings already carry a rule_id and skip this step.

  2. Applicability evaluation: Apply the rule's applicability conditions against the current context (section 1.2). If any condition does not match, the finding is silently removed — it does not apply in this context.

  3. Conditional level resolution: Apply conditional_level overrides against the context (section 1.2). The first matching override determines the resolved level; if none match, the default level is used. The resolved level replaces the engine-reported level. If the resolved level is muted, the finding is removed.

  4. Pass-through: Spectral rules without explicit framework metadata entries pass through with identity mapping (section 1.3). Their engine-reported severity becomes the resolved level, the engine's message is preserved, and no hint is added. This is the common case for most Spectral rules — the framework runs without requiring a complete metadata inventory.

Per-API evaluation: Findings associated with a specific API (identified by api_name) are evaluated against that API's context fields (target_api_status, target_api_maturity, api_pattern). Repository-level findings (e.g., release-plan.yaml checks) are evaluated against the repository-level context.

9.2 Profile Application and Blocking Decision

After post-filtering, each finding has a resolved level. The active profile (section 8.3) determines which levels block:

ProfileBlocking levelsTypical context
advisoryNone — nothing blocksDispatch, local (hardcoded)
standarderrorDefault for PRs (pr_profile) and release gates (release_profile)
stricterror and warnConfigurable via release_profile for pre-snapshot and release review

Profile selection is config-driven for PR and release contexts (section 6.2). Dispatch and local triggers always use advisory regardless of config.

Overall result — one of three values:

ResultMeaning
passNo blocking findings
failAt least one finding at a blocking level
errorAn engine failure prevented complete evaluation (section 9.6) — even if no blocking findings were collected, the result is uncertain

Finding grouping for output: Findings are grouped for display in the following order: by resolved level (error → warn → hint), then by API name, then by file path, then by line number. This ensures the most actionable items appear first.

9.3 Output Formatting

The framework produces output on multiple surfaces, each with different capabilities and token requirements. All surfaces are generated from the same filtered findings list.

Workflow summary (always available)

The workflow summary is the primary detailed surface. It requires no write token — it is written via $GITHUB_STEP_SUMMARY and is always available, even for fork PRs with read-only tokens.

Structure:

## CAMARA Validation — {result}

**Profile**: {profile} | **Branch**: {branch_type} | **Trigger**: {trigger_type}

### Summary

| API | Errors | Warnings | Hints |
|-----|--------|----------|-------|
| qos-booking | 0 | 2 | 1 |
| quality-on-demand | 1 | 0 | 3 |

### Findings

#### Errors

| Rule | File | Line | Message | Hint |
|------|------|------|---------|------|
| 042 | quality-on-demand.yaml | 47 | Path segment should be kebab-case | Use: /quality-on-demand |

#### Warnings
...

#### Hints
...

### Engine Summary

| Engine | Errors | Warnings | Hints | Status |
|--------|--------|----------|-------|--------|
| yamllint | 0 | 0 | 0 | — |
| spectral | 1 | 2 | 1 | — |
| python | 0 | 0 | 3 | — |
| gherkin | — | — | — | skipped (no test files) |

---
Commit: abc1234 | Tooling: def5678 | [Full workflow run]({workflow_run_url})

Check Run with annotations (write token required)

The framework creates a GitHub Check Run via the Checks API with inline annotations. Each annotation maps to a source file and line number:

  • Annotation level: failure for error, warning for warn, notice for hint
  • Annotation message: includes the rule ID, engine rule name, message (or message_override if set), and hint (if present)
  • Annotation title: rule name from metadata (or engine rule name if no metadata)

GitHub limits annotations to 50 per Check Run API call. Annotations are batched accordingly, prioritizing errors > warnings > hints.

Fallback: For fork PRs without write token, annotations are emitted via workflow commands (::error::, ::warning::, ::notice::) as a fallback. These appear in the Actions log but not as inline PR annotations.

PR comment (write token required)

A concise summary comment on the PR — not a full findings list. Links to the workflow summary for details.

### CAMARA Validation — {result}

{errors} errors, {warnings} warnings, {hints} hints | Profile: {profile}

[View full results]({workflow_run_url})

The comment uses a create-or-update pattern with a marker (<!-- camara-validation -->) to avoid duplicate comments on subsequent pushes. Each new push updates the existing comment rather than creating a new one.

Pre-snapshot context: When invoked by release automation (mode: pre-snapshot), findings are formatted for inclusion in the bot's Release Issue comment (section 7.2) rather than as a standalone PR comment. The framework returns the formatted findings section to the calling workflow.

Commit status (write token required)

Commit statuses provide at-a-glance results in the PR's checks list:

  • Overall status: Context CAMARA Validation, state = success / failure / error
  • Per-engine statuses (optional, for diagnostics): CAMARA Validation / Spectral, CAMARA Validation / yamllint, etc. — each shows the engine's individual pass/fail state

The overall status is the one referenced by GitHub rulesets for blocking (stage 3).

9.4 Truncation Strategy

GitHub limits workflow step summaries to 1 MB per step and 1 MB total per job. The framework must stay within this limit.

Approach: Estimate the rendered size during summary generation. If the cumulative size approaches 900 KB:

  1. Show all errors first — never truncated
  2. Show warnings up to the remaining budget
  3. If budget exhausted before all warnings are shown: display a count of remaining findings and link to the full report
  4. Hints are shown only if budget permits after errors and warnings
> Showing 85 of 214 findings. Full Spectral output available in
> [workflow artifacts]({artifact_url}).

The full Spectral JSON output and the complete findings list (all engines) are always uploaded as workflow artifacts regardless of summary truncation. These artifacts are the authoritative complete record.

9.5 Line Number Handling

All engines run on source files (section 8.4), so findings already reference source file locations directly. No line number mapping or source maps are needed.

Spectral and external $ref: Spectral CLI natively follows external $ref during linting and reports findings with the correct source file and line number. When Spectral reports a finding in an external file (e.g., code/common/CAMARA_common.yaml), the finding references the external file path and line. The Spectral adapter detects findings from files under the Commonalities cache prefix (code/common/) and downgrades them to hint level (section 1.3); findings on repo-owned $ref targets (e.g. code/modules/) keep their native severity.

Other engines: yamllint, Python checks, and gherkin-lint operate on source files directly. Their findings always reference source locations.

9.6 Error Handling

Design principle: Always surface what succeeded; never silently skip. If an engine fails, the failure is reported explicitly and remaining engines continue.

Engine failure

When an engine crashes (Spectral error, Python exception, missing dependency):

  1. Catch the failure in the step (non-zero exit code or exception)
  2. Record an engine-level error finding:
    - rule_id: "engine-failure"
      engine: spectral           # the engine that failed
      engine_rule: null
      level: error
      message: "Spectral exited with code 2: Cannot read ruleset file"
      path: null
      line: null
      api_name: null
      hint: "Check the workflow log for details"
    
  3. Continue with remaining engines
  4. Set overall result to error (distinct from fail — signals incomplete evaluation)
  5. Workflow summary explicitly lists which engines succeeded and which failed (engine status table in section 9.3)

Following the release automation pattern: error messages are shown in code blocks, immediately visible, not collapsed. The workflow run URL links to full logs.

Config file missing or invalid

  • Config file missing from the tooling checkout: hard failure with an explicit error in the summary. This is a tooling repository issue, not a per-repo issue.
  • Config file invalid (bad YAML, unknown stage value, schema violation): hard failure naming the config file and the problematic entry. The framework does not proceed with potentially incorrect configuration.

release-plan.yaml missing or invalid

  • Missing: The context builder produces a minimal context with target_release_type: null. Release-plan-specific checks (version consistency, non-exclusivity, dependency validation) are skipped. Spectral and yamllint still run on source files. This is not an error — repos without release-plan.yaml (or feature branches that haven't added one) are valid.
  • Invalid (present but fails schema validation): Schema violations are reported as findings. The context builder extracts whatever fields it can and continues with a partial context. Remaining checks run with the available context.

Bundling failure

Bundling runs as a post-validation step (section 8.1, steps 11-12) to produce diagnostic artifacts. A bundling failure does not affect the validation result — all engines have already run on source files. If redocly bundle fails:

  1. Log a warning in the workflow summary
  2. Skip artifact upload (no bundled specs produced)
  3. Validation result is unaffected

Token minting failure

When the validation app token cannot be minted (app not installed, secret missing, API error):

  1. Follow the layered token resolution (section 5.1) — fall back to GITHUB_TOKEN, then to read-only
  2. Log the degraded state in the workflow summary header: "Write surfaces unavailable — showing findings in workflow summary only"
  3. Never fail the workflow because of a token issue — validation results are always produced, only the surfacing degrades
  4. Check run annotations, PR comments, and commit statuses are silently skipped when no write token is available

Failure mode summary

FailureBehaviorOverall result
Engine crashRecord engine-failure finding, continue with remaining engineserror
Config file missing/invalidHard failure, explicit error in summaryerror (workflow exits)
release-plan.yaml missingMinimal context, skip release-plan checks, run Spectral/yamllintNormal (pass/fail)
release-plan.yaml invalidReport violations as findings, partial context, continueNormal (pass/fail)
Bundling failureLog warning, skip artifact upload — validation result unaffectedNormal (pass/fail)
Token minting failureDegrade surfacing, never fail the workflowNormal (pass/fail)

9.7 Bundled Spec Artifacts

Bundled API specs are produced as a post-validation step (section 8.1, steps 11-12) and are a distinct output category from findings — they are diagnostic and review artifacts, not validation inputs or results.

Artifact upload

After validation completes, the workflow invokes redocly bundle on each API spec with external $ref and uploads the bundled files as GitHub workflow artifacts, regardless of whether validation passes or fails. Bundled specs are useful for review even when findings exist.

Artifact naming follows the convention validation-bundled-specs. The artifact contains one bundled YAML file per API (retaining the original filename, e.g., qos-booking.yaml).

Artifacts use the GitHub default retention period (90 days). They are available for download from the workflow run's artifact list.

User surfacing

The workflow summary (section 9.3) includes a link to the bundled spec artifacts when bundling ran:

### Bundled Specs

Bundled standalone API specs are available as [workflow artifacts]({artifact_url}).

This gives PR reviewers visibility into the bundled output — they can download and inspect the fully resolved specs to verify that $ref resolution produced the expected result. For copy-paste repositories (no external $ref), this section is omitted.

Relationship to release automation

The validation framework's bundled artifacts are for reviewer inspection only. Release automation bundles independently during snapshot creation using its own redocly bundle invocation — there is no cross-workflow artifact handoff. This separation keeps validation stateless (it produces findings and diagnostic files) while release automation owns repository state.

For details on the two-gate model (pre-snapshot validation + release-review PR validation), see section 7.1.


Appendix A: Naming Conventions

Complete naming convention rules from CAMARA-API-Design-Guide.md and CAMARA-API-Event-Subscription-and-Notification-Guide.md, with current Spectral rule coverage.

ElementConventionExampleDG SectionSpectral RuleStatus
Paths (URLs)kebab-case/customer-segments5.7.1camara-parameter-casing-convention (error)Implemented
Path parameters{entityId} form{userId}, {accountId}5.7.1camara-path-param-id (warn)Implemented (morphology rule missing)
SchemasPascalCaseErrorInfo, DeviceResponse5.8.1camara-schema-casing-convention (warn)Implemented
operationIdcamelCasehelloWorld, retrieveLocation5.7.2camara-operationid-casing-conventionImplemented (severity: hint vs error in Linting-rules.md)
PropertieslowerCamelCasesessionId, phoneNumber5.7.4camara-property-casing-convention (error)Listed in Linting-rules.md, not in .spectral.yaml
Enum valuesSCREAMING_SNAKE_CASEINVALID_ARGUMENT, PERMISSION_DENIED3.2camara-enum-casing-convention (info)Listed in Linting-rules.md, not in .spectral.yaml
Error codesSCREAMING_SNAKE_CASEUNAUTHENTICATED, NOT_FOUND3.2Gap (r4.x explicit requirement)
API-specific error codesAPI_NAME.SPECIFIC_CODECARRIER_BILLING.PAYMENT_DENIED3.2.1Gap
TagsTitle Case with spacesQuality On Demand5.7.3Gap
Headerskebab-casex-correlator5.8.5Implied by convention, no rule
API namekebab-caselocation-verification1.2v0_6 validator only
Scope nameskebab-case with : separatorsqod:sessions:create6.6v0_6 validator only
Event typeorg.camaraproject.<api>.<ver>.<event>org.camaraproject.device-roaming-subscriptions.v1.roaming-statusEvent Guide 3.1Gap
Examples (named)SCREAMING_SNAKE_CASESESSION_CREATION_EXAMPLE_WITH_DEVICE_RESPONSEOAS 3.0.3Gap

Appendix B: Local Validation Considerations

This appendix addresses UC-03 ("Run most validation rules locally, on local clones, via scripts"). Its purpose is not to design the local CLI — that is implementation work — but to identify where the framework implementation must stay modular to avoid duplicated effort when local tooling is built.

B.1 Scope

In scope: Implementation guardrails for the framework that keep local reuse viable.

Out of scope: Entry point design (script, CLI, Makefile), tooling distribution and versioning for local use, local cache management for Commonalities artifacts, IDE integration, performance targets.

B.2 Environment Boundary

The 8 processing steps (section 8) fall into three categories:

Environment-neutral — reusable locally without modification:

  • Spectral engine invocation and output parsing
  • Python check execution
  • Bundling pipeline (external ref resolution, cache sync validation)
  • Rule metadata lookup and condition evaluation
  • Post-filter: applicability matching, conditional level resolution, profile application
  • Findings list construction

Workflow-only — require GitHub infrastructure:

  • Tooling ref resolution (OIDC job_workflow_sha, version tag fallback)
  • Central config lookup (tooling checkout, repository stage)
  • PR metadata detection (is_release_review_pr from target branch, release_plan_changed from PR diff API)
  • Output surfacing: check run annotations, PR comments, commit status

Adaptable — work locally with alternative input:

  • Context building: branch type is derivable from git branch --show-current using the same pattern logic. Trigger type and PR-specific fields need sensible defaults (see B.4)
  • Repository checkout: a local clone replaces the workflow checkout step

B.3 local Trigger Type

The trigger_types vocabulary (section 1.1) includes local alongside pr, dispatch, and release-automation.

Semantics:

  • Profile is always advisory (nothing blocks)
  • No GitHub event context, no PR metadata
  • is_release_review_pr defaults to false
  • release_plan_changed defaults to null (unknown — rules requiring it are skipped)

Most rules have no trigger_types constraint and apply in all contexts including local. Rules that explicitly list only [pr] are automatically excluded.

B.4 Local Context Derivation

How context fields map when running locally (no GitHub event):

FieldWorkflow sourceLocal sourceNotes
branch_typegithub.base_ref / github.ref_namegit branch --show-currentSame pattern-matching logic
trigger_typeGitHub event name / mode inputFixed: local
profileDerived from trigger typeFixed: advisory
stageCentral config lookupNot applicableLocal runs are not gated by rollout stage
target_release_typerelease-plan.yamlrelease-plan.yamlIdentical
commonalities_releaserelease-plan.yamlrelease-plan.yamlIdentical — drives Spectral ruleset selection
icm_releaserelease-plan.yamlrelease-plan.yamlIdentical
target_api_statusrelease-plan.yaml (per-API)release-plan.yaml (per-API)Identical
target_api_maturityDerived from versionDerived from versionIdentical
api_patternDetected from spec contentDetected from spec contentIdentical
is_release_review_prPR target branchFixed: false
release_plan_changedPR diff via APIFixed: nullRules requiring this are skipped

The majority of context fields (8 of 12) are file-derived and work identically in both environments. The workflow-only fields (stage, is_release_review_pr, release_plan_changed) degrade to safe defaults that skip inapplicable rules rather than producing incorrect results.

B.5 Implementation Guardrails

These constraints apply to the framework implementation to preserve local reusability:

  1. Engine invocation must be callable without GitHub context. Spectral and Python check runners accept file paths and a context object as inputs. They must not read GITHUB_EVENT_PATH, GITHUB_OUTPUT, or other Actions environment variables directly. Workflow-level steps pass these values in; engines do not reach for them.

  2. Context construction must be separable from GitHub event parsing. The context builder must support being fed from either a GitHub event payload or from local defaults combined with git-derived values. A clean boundary: the workflow caller resolves GitHub-specific inputs and hands them to the context builder; the builder itself is environment-neutral.

  3. Output formatting must support terminal output. The post-filter produces a structured findings list. Rendering to workflow summary markdown and rendering to terminal are separate formatters behind a common findings-list interface. The terminal formatter is the natural output for local runs.

  4. Bundling must not depend on the workflow artifacts API. The bundling pipeline reads files and writes files. Artifact upload is a separate workflow step, not embedded in the bundler. Locally, bundled output is written to a directory.

  5. Rule metadata and Spectral rulesets must be loadable from a local path. The reusable workflow resolves tooling via OIDC ref or version tag and checks out into .tooling/. Locally, the user either has a tooling clone or a downloaded copy. The engine invocation code must accept a configurable base path for rule metadata and rulesets, not hardcode the .tooling/ checkout location.