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
toolingrepository 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:
| Spectral | Framework | Notes |
|---|---|---|
error | error | Identity |
warn | warn | Identity |
hint | hint | Identity |
info | hint | Mapped (rarely used) |
off | muted | Mapped (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_version — initial 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
Andfor 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
$refby placing the referenced component into the appropriatecomponents/subsection of the output document and replacing the external$refwith 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$refalready present in the source are preserved unchanged. - Full dereferencing: Resolves all
$refincluding internal ones, producing a flat document with zero$refand massive duplication. The framework must not use full dereferencing.
Preserving internal $ref ensures that:
- Spectral rules checking component structure,
$refpatterns, 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.
$refrepos: Spectral runs on source files — it natively follows external$refduring 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_releasefield (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:
- Snapshot context:
camara-release-automationapp token — provided by the calling release workflow. The validation framework does not mint this token; it is passed in by the release automation caller. - 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.
- Fallback:
GITHUB_TOKENwith 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. - 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.
| Aspect | Validation App | camara-release-automation |
|---|---|---|
| Purpose | PR annotations, comments, commit status | Release snapshot creation, branch management |
| Permissions | checks: write, pull-requests: write, statuses: write | contents: write, workflows: write, plus release management |
| Commits/pushes | Never | Yes (snapshot branches, tags, release assets) |
| EasyCLA | Not 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_profilefrom config (default: standard)release-snapshot/**: Release review PRs created by release automation on snapshot branches. Profile:release_profilefrom config (default: standard)maintenance/**: Maintenance branch PRs. Profile:pr_profilefrom 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
| Permission | Purpose | Fork PR behavior |
|---|---|---|
checks: write | Check run annotations (findings inline in PR diff) | Restricted by GitHub; validation app token used instead |
pull-requests: write | PR review interactions | Restricted by GitHub; validation app token used instead |
issues: write | PR comments (PRs use the Issues API for comments) | Restricted by GitHub; validation app token used instead |
contents: read | Repository checkout | Available (read-only) |
statuses: write | Commit status (per-check context in checks list) | Restricted by GitHub; validation app token used instead |
id-token: write | OIDC 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
tooling_ref_overrideinput (if set) — explicit SHA, highest priority- OIDC
job_workflow_shaclaim (ifid-tokenavailable) — exact commit SHA - 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 viasecretscontext - Org variables for app identity (
VALIDATION_APP_CLIENT_ID,VALIDATION_APP_SLUG) — accessed viavarscontext
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
| Field | Type | Description |
|---|---|---|
version | integer | Schema version (currently 1). Allows future schema evolution without breaking existing configs. |
defaults.stage | enum | Default stage for unlisted repositories: disabled, advisory, enabled. |
defaults.pr_profile | enum | Default profile for PR validation: advisory, standard, strict. If omitted, defaults to standard. |
defaults.release_profile | enum | Default profile for pre-snapshot and release review PR validation: advisory, standard, strict. If omitted, defaults to standard. |
fork_owners | array of strings | GitHub 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>.stage | enum | Per-repo rollout stage override. Same values as defaults.stage. |
repositories.<name>.pr_profile | enum | Per-repo PR profile override. Same values as defaults.pr_profile. |
repositories.<name>.release_profile | enum | Per-repo release profile override. Same values as defaults.release_profile. |
Stage mapping (see also Requirements section 10.3):
| Stage | Config value | Behavior |
|---|---|---|
| 0 (dark) | disabled | Caller deployed but reusable workflow exits immediately |
| 1 (advisory) | advisory | Runs on dispatch only, advisory profile, nothing blocks |
| 2 (enabled) | enabled | Runs 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-automationapp 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
- Test repo:
ReleaseTest— full cycle through stages 0-3, validates all surfacing paths - Template:
Template_API_Repository— ensures new repos get v1 caller from creation - Pilot API repos: 2-3 active repos with engaged codeowners
- 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.versionreplacement (wip→ calculated release version)- Server URL version updates
x-camara-commonalitiesversion 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_typetorelease-automation - Selects the
release_profilefrom 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
| # | Step | Mechanism | Skip condition |
|---|---|---|---|
| 1 | Checkout repository content | inline | Never |
| 2 | Resolve tooling ref and checkout tooling | inline (github-script) | Never |
| 3 | Setup Python and Node | inline | Never |
| 4 | Detect release-plan changes | inline | Non-PR triggers |
| 5 | Run validation | run-validation shared action | Never (exits internally if disabled) |
| 6 | Mint validation app token | inline | Non-PR triggers or secret unavailable |
| 7 | Create Check Run | inline (github-script) | No write token |
| 8 | Emit annotation fallback | inline | Check Run succeeded |
| 9 | Post findings to PR | inline (github-script) | No write token |
| 10 | Upload diagnostics | inline | Never |
| 11 | Bundle API specs | inline (redocly) | No external $ref detected |
| 12 | Upload bundled specs | inline | No bundled specs produced |
| 13 | Check result | inline | Never |
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_changeddetection and file restriction check) - Branch type detection: examining
github.base_reffor 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 configvalidation/— Python validation scripts, rule metadata, central config fileshared-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:
- Validate the config file against its JSON Schema
- Extract the repository name from
github.repositorywithout the owner prefix (e.g.,camaraproject/QualityOnDemand→QualityOnDemand) - Look up
repositories.<repo-name>.stage(fall back todefaults.stage) - Fork override: If the workflow is running in a fork (
github.repository_owneris notcamaraprojectorGSMA-Open-Gateway) and the owner is listed infork_owners→ override stage toenabled. If the owner is not listed → keep the resolved stage (typicallydisabledduring early rollout, which exits the workflow) - Profile resolution: Resolve
pr_profileandrelease_profilefrom the repository's config entry, falling back todefaults.pr_profileanddefaults.release_profile, then tostandardif omitted - 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 toskipped - If stage is
advisoryand trigger ispull_request: exit similarly ("Validation is in advisory mode — use workflow_dispatch to run") - 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
| Pattern | Branch type | Source |
|---|---|---|
main | main | github.base_ref (PR) or github.ref_name (dispatch) |
release-snapshot/** | release | Same |
maintenance/** | maintenance | Same |
| Everything else | feature | Same |
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
| Source | Trigger type |
|---|---|
github.event_name == 'pull_request' | pr |
github.event_name == 'workflow_dispatch' | dispatch |
mode input == pre-snapshot | release-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 type | Condition | Profile source | Default |
|---|---|---|---|
dispatch | — | Hardcoded | advisory |
local | — | Hardcoded | advisory |
pr | is_release_review_pr = true | release_profile from config | standard |
pr | — | pr_profile from config | standard |
release-automation | — | release_profile from config | standard |
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 rulesdependencies.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 fromtarget_api_version—initialif major version is 0,stableif >= 1 (section 1.4)api_pattern: Detected from the corresponding OpenAPI spec file content incode/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_releasematchingr3.*→.spectral-r3.4.yamlcommonalities_releasematchingr4.*→.spectral-r4.yaml- Future version lines added as new rulesets are created
- If
commonalities_releaseis 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 arelease-snapshot/**branch (section 7.3)release_plan_changed: True whenrelease-plan.yamlis 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), sogithub.repositoryis 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 category | Target |
|---|---|
| Cross-field consistency | Context + spec content |
| Version checks (info.version, server URL) | Context + spec content |
| release-plan.yaml semantic checks | release-plan.yaml |
| Error response structure | Context + spec content |
| API pattern-specific checks | Context + spec content + api_pattern |
| File naming and structure | Repository 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)
| Field | Source — Spectral | Source — yamllint | Source — gherkin-lint | Source — Python |
|---|---|---|---|---|
rule_id | Looked up from rule metadata by engine_rule; auto-assigned if no metadata | Looked up similarly | Looked up similarly | Set directly by check |
engine | "spectral" | "yamllint" | "gherkin" | "python" |
engine_rule | code field from JSON | Rule name from output | Rule name from output | Check function name |
level | Mapped from severity integer: 0→error, 1→warn, 2→hint, 3→hint | Mapped from yamllint severity | Mapped from gherkin-lint severity | Set directly |
message | message field | Error message text | Error message text | Set directly |
path | source field | File path from output | File path from output | Set directly |
line | range.start.line (0-indexed → 1-indexed) | Line number from output | Line number from output | Set directly |
column | range.start.character | Column from output | Column from output (or null) | Set directly (or null) |
api_name | Derived from file path | Derived from file path | Derived from file path | Set directly |
message (post-filter) | If rule metadata has message_override: replaces engine message. Otherwise: engine message preserved | Same | Same | Set directly (no override) |
hint | From rule metadata hint field (optional additional fix guidance); absent if no metadata or no hint defined | From rule metadata | From rule metadata | Set 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).
| Action | Purpose | Key inputs | Key outputs |
|---|---|---|---|
run-validation | Full validation pipeline (section 8.4) | repo_path, tooling_path, mode, profile, release_plan_changed, tooling_ref | result, 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_releasefor 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:
-
Rule metadata lookup: Match the finding to its framework rule metadata entry by
engine+engine_rule. Python-produced findings already carry arule_idand skip this step. -
Applicability evaluation: Apply the rule's
applicabilityconditions 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. -
Conditional level resolution: Apply
conditional_leveloverrides 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 ismuted, the finding is removed. -
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
messageis preserved, and nohintis 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:
| Profile | Blocking levels | Typical context |
|---|---|---|
advisory | None — nothing blocks | Dispatch, local (hardcoded) |
standard | error | Default for PRs (pr_profile) and release gates (release_profile) |
strict | error and warn | Configurable 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:
| Result | Meaning |
|---|---|
pass | No blocking findings |
fail | At least one finding at a blocking level |
error | An 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:
failurefor error,warningfor warn,noticefor hint - Annotation message: includes the rule ID, engine rule name, message (or
message_overrideif 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:
- Show all errors first — never truncated
- Show warnings up to the remaining budget
- If budget exhausted before all warnings are shown: display a count of remaining findings and link to the full report
- 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):
- Catch the failure in the step (non-zero exit code or exception)
- 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" - Continue with remaining engines
- Set overall result to
error(distinct fromfail— signals incomplete evaluation) - 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:
- Log a warning in the workflow summary
- Skip artifact upload (no bundled specs produced)
- Validation result is unaffected
Token minting failure
When the validation app token cannot be minted (app not installed, secret missing, API error):
- Follow the layered token resolution (section 5.1) — fall back to
GITHUB_TOKEN, then to read-only - Log the degraded state in the workflow summary header: "Write surfaces unavailable — showing findings in workflow summary only"
- Never fail the workflow because of a token issue — validation results are always produced, only the surfacing degrades
- Check run annotations, PR comments, and commit statuses are silently skipped when no write token is available
Failure mode summary
| Failure | Behavior | Overall result |
|---|---|---|
| Engine crash | Record engine-failure finding, continue with remaining engines | error |
| Config file missing/invalid | Hard failure, explicit error in summary | error (workflow exits) |
| release-plan.yaml missing | Minimal context, skip release-plan checks, run Spectral/yamllint | Normal (pass/fail) |
| release-plan.yaml invalid | Report violations as findings, partial context, continue | Normal (pass/fail) |
| Bundling failure | Log warning, skip artifact upload — validation result unaffected | Normal (pass/fail) |
| Token minting failure | Degrade surfacing, never fail the workflow | Normal (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.
| Element | Convention | Example | DG Section | Spectral Rule | Status |
|---|---|---|---|---|---|
| Paths (URLs) | kebab-case | /customer-segments | 5.7.1 | camara-parameter-casing-convention (error) | Implemented |
| Path parameters | {entityId} form | {userId}, {accountId} | 5.7.1 | camara-path-param-id (warn) | Implemented (morphology rule missing) |
| Schemas | PascalCase | ErrorInfo, DeviceResponse | 5.8.1 | camara-schema-casing-convention (warn) | Implemented |
| operationId | camelCase | helloWorld, retrieveLocation | 5.7.2 | camara-operationid-casing-convention | Implemented (severity: hint vs error in Linting-rules.md) |
| Properties | lowerCamelCase | sessionId, phoneNumber | 5.7.4 | camara-property-casing-convention (error) | Listed in Linting-rules.md, not in .spectral.yaml |
| Enum values | SCREAMING_SNAKE_CASE | INVALID_ARGUMENT, PERMISSION_DENIED | 3.2 | camara-enum-casing-convention (info) | Listed in Linting-rules.md, not in .spectral.yaml |
| Error codes | SCREAMING_SNAKE_CASE | UNAUTHENTICATED, NOT_FOUND | 3.2 | — | Gap (r4.x explicit requirement) |
| API-specific error codes | API_NAME.SPECIFIC_CODE | CARRIER_BILLING.PAYMENT_DENIED | 3.2.1 | — | Gap |
| Tags | Title Case with spaces | Quality On Demand | 5.7.3 | — | Gap |
| Headers | kebab-case | x-correlator | 5.8.5 | — | Implied by convention, no rule |
| API name | kebab-case | location-verification | 1.2 | — | v0_6 validator only |
| Scope names | kebab-case with : separators | qod:sessions:create | 6.6 | — | v0_6 validator only |
| Event type | org.camaraproject.<api>.<ver>.<event> | org.camaraproject.device-roaming-subscriptions.v1.roaming-status | Event Guide 3.1 | — | Gap |
| Examples (named) | SCREAMING_SNAKE_CASE | SESSION_CREATION_EXAMPLE_WITH_DEVICE_RESPONSE | OAS 3.0.3 | — | Gap |
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_prfrom target branch,release_plan_changedfrom 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-currentusing 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_prdefaults tofalserelease_plan_changeddefaults tonull(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):
| Field | Workflow source | Local source | Notes |
|---|---|---|---|
branch_type | github.base_ref / github.ref_name | git branch --show-current | Same pattern-matching logic |
trigger_type | GitHub event name / mode input | Fixed: local | |
profile | Derived from trigger type | Fixed: advisory | |
stage | Central config lookup | Not applicable | Local runs are not gated by rollout stage |
target_release_type | release-plan.yaml | release-plan.yaml | Identical |
commonalities_release | release-plan.yaml | release-plan.yaml | Identical — drives Spectral ruleset selection |
icm_release | release-plan.yaml | release-plan.yaml | Identical |
target_api_status | release-plan.yaml (per-API) | release-plan.yaml (per-API) | Identical |
target_api_maturity | Derived from version | Derived from version | Identical |
api_pattern | Detected from spec content | Detected from spec content | Identical |
is_release_review_pr | PR target branch | Fixed: false | |
release_plan_changed | PR diff via API | Fixed: null | Rules 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:
-
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. -
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.
-
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.
-
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.
-
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.