Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 94 additions & 14 deletions .github/workflows/pr_validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ on:
required: false
default: ""

# permissions block removed to inherit from caller workflow — enables
# conditional OIDC ref resolution for callers that grant id-token: write
# while keeping legacy v0 fallback for callers that don't.
# permissions:
# contents: read
# issues: write
# pull-requests: write
# statuses: write

jobs:
pr-validation:
name: PR Validation
Expand All @@ -42,6 +51,75 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0

- name: Resolve tooling checkout ref
id: resolve-tooling-ref
uses: actions/github-script@v8
with:
script: |
const workflowRefPattern =
/^([^/]+\/[^/]+)\/\.github\/workflows\/pr_validation\.yml@.+$/;
const shaPattern = /^[0-9a-f]{40}$/i;
const oidcUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL || '';
const oidcTokenRequest = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN || '';

function decodeJwtPayload(jwt) {
const parts = jwt.split('.');
if (parts.length !== 3) {
throw new Error('OIDC token is not a valid JWT');
}

const payload = parts[1]
.replace(/-/g, '+')
.replace(/_/g, '/');
const padLength = (4 - (payload.length % 4)) % 4;
const padded = payload + '='.repeat(padLength);
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
}

if (oidcUrl === '' || oidcTokenRequest === '') {
core.warning('ID token is not available for this caller workflow. Falling back to legacy tooling ref camaraproject/tooling@v0.');
// Operational outputs — used by subsequent checkout steps
core.setOutput('tooling_checkout_repo', 'camaraproject/tooling');
core.setOutput('tooling_checkout_ref', 'v0'); // tag ref, not pinned
core.setOutput('tooling_ref_mode', 'legacy_v0');
// Informational outputs — not available in fallback mode
core.setOutput('tooling_workflow_ref', '');
core.setOutput('tooling_workflow_sha', '');
return;
}

const oidcToken = await core.getIDToken('pr-validation-tooling-ref');
const claims = decodeJwtPayload(oidcToken);
const workflowRef = claims.job_workflow_ref || '';
const workflowSha = claims.job_workflow_sha || '';
const workflowRefMatch = workflowRef.match(workflowRefPattern);

if (!workflowRefMatch) {
core.setFailed(`Unexpected job_workflow_ref claim: ${workflowRef}`);
return;
}

if (!shaPattern.test(workflowSha)) {
core.setFailed(`Invalid job_workflow_sha claim: ${workflowSha}`);
return;
}

const checkoutRepo = workflowRefMatch[1];
const checkoutRef = workflowSha.toLowerCase();

core.info(`tooling_checkout_repo=${checkoutRepo}`);
core.info(`tooling_checkout_ref=${checkoutRef}`);
core.info(`job_workflow_ref=${workflowRef}`);
core.info(`job_workflow_sha=${workflowSha}`);

// Operational outputs — used by subsequent checkout steps
core.setOutput('tooling_checkout_repo', checkoutRepo);
core.setOutput('tooling_checkout_ref', checkoutRef); // 40-char commit SHA for pinned checkout
core.setOutput('tooling_ref_mode', 'resolved_from_oidc');
// Informational outputs — for logging and diagnostics only
core.setOutput('tooling_workflow_ref', workflowRef); // symbolic ref, e.g. .../pr_validation.yml@refs/tags/v0
core.setOutput('tooling_workflow_sha', workflowSha);

- name: Detect changed files
id: changes
uses: tj-actions/changed-files@8cba46e29c11878d930bca7870bb54394d3e8b21 # v47.0.2
Expand Down Expand Up @@ -76,10 +154,22 @@ jobs:
exit 1
fi

- name: Checkout tooling assets
if: steps.changes.outputs.release_plan_any_changed != 'true' || steps.exclusivity.outcome != 'failure'
uses: actions/checkout@v6
with:
repository: ${{ steps.resolve-tooling-ref.outputs.tooling_checkout_repo }}
ref: ${{ steps.resolve-tooling-ref.outputs.tooling_checkout_ref }}
path: tooling-assets
sparse-checkout: |
linting/config
shared-actions/validate-release-plan
sparse-checkout-cone-mode: false

- name: Validate release-plan.yaml
if: steps.changes.outputs.release_plan_any_changed == 'true'
id: validate
uses: camaraproject/tooling/shared-actions/validate-release-plan@v0
uses: ./tooling-assets/shared-actions/validate-release-plan
with:
release_plan_path: release-plan.yaml
check_files: 'true'
Expand Down Expand Up @@ -176,22 +266,12 @@ jobs:
# - release-plan.yaml was changed AND validation passed
# Skipped when release-plan.yaml validation fails (no point linting with invalid rules)

- name: Checkout linting config
if: steps.changes.outputs.release_plan_any_changed != 'true' || steps.validate.outputs.valid == 'true'
uses: actions/checkout@v6
with:
repository: camaraproject/tooling
path: lint-config
# using configurations from v0 floating tag
ref: v0
sparse-checkout: |
linting/config/
sparse-checkout-cone-mode: false

- name: Copy config to workspace
if: steps.changes.outputs.release_plan_any_changed != 'true' || steps.validate.outputs.valid == 'true'
# configurations selects a subfolder inside linting/config from the resolved tooling workflow ref
# or the legacy v0 fallback when the caller workflow does not grant ID-token access
# --strip-trailing-slashes removes any trailing slashes from each SOURCE argument (when ${{ inputs.configurations }} is empty)
run: cp -RT --strip-trailing-slashes ${{ github.workspace }}/lint-config/linting/config/${{ inputs.configurations }} ${{ github.workspace }}
run: cp -RT --strip-trailing-slashes ${{ github.workspace }}/tooling-assets/linting/config/${{ inputs.configurations }} ${{ github.workspace }}

- name: MegaLinter
id: ml
Expand Down
15 changes: 12 additions & 3 deletions linting/docs/Reusable Workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,23 @@ API-repository/
└── spectral-oas-caller.yml
```

The job input parameter `configurations` in caller workflows allows to specify the branch of the `tooling` repository from which the configuration files stored in `/linting/config/` are applied in the reusable workflows.
By default, the main branch of tooling is used.
The caller workflow `uses:` line selects which version of `tooling` provides the reusable workflow, for example:

```yaml
uses: camaraproject/tooling/.github/workflows/pr_validation.yml@v0
```

Inside `pr_validation.yml`, workflow-owned assets are resolved from that same effective tooling repository and commit, so branch-based or SHA-based pilot calls stay internally consistent.

For backward compatibility, callers that still use the older permissions block without `id-token: write` can still start the reusable workflow and automatically fall back to the legacy internal `camaraproject/tooling@v0` asset resolution. That keeps existing API repositories working while a coordinated caller migration is prepared.

The `configurations` input does **not** select a tooling branch. It selects a subfolder inside `/linting/config/` from the tooling ref already chosen by the reusable workflow call.

```yaml
# with:
# configurations: staging
```
This way custom configurations can be used (if needed by given repository or for canary deployment of new configurations) - first the relevant branch needs to be created in the `tooling` repository.
This way custom configurations can be used if needed by a given repository. Create the relevant subfolder in `tooling/linting/config/` and reference it through `configurations`.

## Runnig Linting Workflows

Expand Down
6 changes: 4 additions & 2 deletions linting/workflows/pr_validation_caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ permissions:

jobs:
pr_validation:
# Invoke the reusable PR validation workflow from "v0" tag of camaraproject/tooling
# Invoke the reusable PR validation workflow from the selected tooling ref.
# The reusable workflow resolves its own internal actions and config from that same ref.
# Older callers without id-token permission continue to use the legacy internal v0 fallback.
uses: camaraproject/tooling/.github/workflows/pr_validation.yml@v0
secrets: inherit
# Tools configuration from the tooling repository subfolder of /linting/config/ indicated by `configurations` variable
# If needed, you can specify a configuration from another subfolder of camaraproject/tooling/linting/config/ (uncomment below)
# If needed, you can specify another config subfolder from camaraproject/tooling/linting/config/ (uncomment below)
# with:
# configurations: api-name