diff --git a/.github/workflows/pr_validation.yml b/.github/workflows/pr_validation.yml index a7df413..ec71c8c 100644 --- a/.github/workflows/pr_validation.yml +++ b/.github/workflows/pr_validation.yml @@ -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 @@ -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 @@ -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' @@ -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 diff --git a/linting/docs/Reusable Workflows.md b/linting/docs/Reusable Workflows.md index 7b60b96..dd6399f 100644 --- a/linting/docs/Reusable Workflows.md +++ b/linting/docs/Reusable Workflows.md @@ -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 diff --git a/linting/workflows/pr_validation_caller.yml b/linting/workflows/pr_validation_caller.yml index d847f6e..30e1ba0 100644 --- a/linting/workflows/pr_validation_caller.yml +++ b/linting/workflows/pr_validation_caller.yml @@ -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