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
41 changes: 0 additions & 41 deletions .github/workflows/license_check.yml

This file was deleted.

76 changes: 76 additions & 0 deletions .github/workflows/license_check_core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# ******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# ******************************************************************************

name: License Check

on:
workflow_call:
inputs:
head-sha:
description: "Head SHA for trusted checkouts from workflow_run; empty when called with PR context"
required: false
default: ""
type: string
head-repository:
description: "Repository of the head commit (fork PRs); empty for base-repo checkouts"
required: false
default: ""
type: string
secrets:
dash-api-token:
description: "Eclipse GitLab API token for DASH"
required: true

jobs:
license-check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
lfs: true
persist-credentials: false
ref: ${{ inputs.head-sha != '' && inputs.head-sha || github.ref }}
repository: ${{ inputs.head-repository != '' && inputs.head-repository || github.repository }}

- name: Setup Bazel
uses: bazel-contrib/setup-bazel@8cb04a772ab4c1eb984e9c1b493a182e96c5e425 # v0.19.0
with:
bazelisk-version: 1.29.0
disk-cache: true

- name: Run License Check via Bazel
env:
DASH_API_TOKEN: ${{ secrets.dash-api-token }}
run: |
set +e # The tool exits non-zero for items needing review; report, don't fail.
# -review requires a token per the DASH tool's usage contract;
# without a token, run anonymous checks against public license data.
TOKEN_ARGS=""
if [[ -n "$DASH_API_TOKEN" ]]; then
TOKEN_ARGS="-review -token $DASH_API_TOKEN"
fi
bazel run --lockfile_mode=error //:license-check -- \
-project automotive.score \
-repo "${{ github.server_url }}/${{ github.repository }}" \
$TOKEN_ARGS > license-check-output.txt 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "License check passed."
else
echo "License check reported items (exit $EXIT_CODE). See job summary."
echo "exit_code=$EXIT_CODE" >> "$GITHUB_ENV"
fi
cat license-check-output.txt | tail -20
63 changes: 63 additions & 0 deletions .github/workflows/license_check_forks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# ******************************************************************************

name: License Check (forks)

# Trusted chain entry point for fork PRs, per ADR 0001: runs in the base repository context with secrets, but only after
# the approved-for-ci gate passes for the exact head SHA of the original untrusted run.

on:
workflow_run:
workflows: ["PR Checks"]
types: [completed]
branches: [main]

permissions:
contents: read
pull-requests: write
issues: write

jobs:
approved-for-ci-gate:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name != github.repository
uses: ./.github/workflows/trusted_chain_gate.yml
with:
head-sha: ${{ github.event.workflow_run.head_sha }}
secrets: inherit

license-check:
needs: [approved-for-ci-gate]
if: needs.approved-for-ci-gate.outputs.approved == 'true'
uses: ./.github/workflows/license_check_core.yml
with:
head-sha: ${{ github.event.workflow_run.head_sha }}
head-repository: ${{ needs.approved-for-ci-gate.outputs.head-repository }}
secrets:
dash-api-token: ${{ secrets.ECLIPSE_GITLAB_API_TOKEN }}

notify-pending-approval:
needs: [approved-for-ci-gate]
if: needs.approved-for-ci-gate.outputs.approved != 'true' && needs.approved-for-ci-gate.outputs.pr-number != ''
runs-on: ubuntu-latest
steps:
- name: Comment on PR about missing or invalidated approval
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ needs.approved-for-ci-gate.outputs.pr-number }}
comment-author: 'github-actions[bot]'
body: |
The trusted license check was **skipped**: no valid `approved-for-ci` approval for head commit
`${{ github.event.workflow_run.head_sha }}`.

A codeowner can apply the `approved-for-ci` label to this PR. Any
push after labelling invalidates it; re-labelling re-runs the check.
7 changes: 7 additions & 0 deletions .github/workflows/on-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ jobs:
uses: eclipse-score/cicd-workflows/.github/workflows/on-pr.yml@c2c7e6dbab32b902f08ca5efc8e4ad623e6d280c # 2026-08-11
permissions:
contents: read
license-check:
# Trusted-chain job (ADR 0001): runs in base context for repo PRs,
# merge_group and push. Fork PRs are handled by license_check_forks.yml.
if: (github.event_name == 'push') || (github.event_name == 'merge_group') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
uses: ./.github/workflows/license_check_core.yml
secrets:
dash-api-token: ${{ secrets.ECLIPSE_GITLAB_API_TOKEN }}
98 changes: 98 additions & 0 deletions .github/workflows/trusted_chain_gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# ******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# ******************************************************************************

name: Trusted chain gate

on:
workflow_call:
inputs:
head-sha:
description: "Exact head SHA of the fork PR"
required: true
type: string
label-name:
description: "Name of the approval label"
required: false
default: "approved-for-ci"
type: string
outputs:
approved:
description: "true if the trusted chain may run"
value: ${{ jobs.gate.outputs.approved }}
pr-number:
description: "PR number the gate resolved"
value: ${{ jobs.gate.outputs.pr-number }}
head-repository:
description: "Fork repository the head commit lives in"
value: ${{ jobs.gate.outputs.head-repository }}

permissions:
contents: read
pull-requests: read

jobs:
gate:
runs-on: ubuntu-latest
outputs:
approved: ${{ steps.decide.outputs.approved }}
pr-number: ${{ steps.decide.outputs.pr-number }}
head-repository: ${{ steps.decide.outputs.head-repository }}
steps:
- name: Resolve PR and apply approved-for-ci gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ inputs.head-sha }}
LABEL_NAME: ${{ inputs.label-name }}
run: |
set -euo pipefail

REPO="${{ github.repository }}"

# Find the open PR whose head commit is exactly the SHA from the
# original untrusted run. Never use a mutable ref name.
PR_JSON="$(gh api "repos/$REPO/pulls?state=open&per_page=100" \
--jq ".[] | select(.head.sha == \"$HEAD_SHA\")" | head -n 1)"
if [[ -z "$PR_JSON" ]]; then
echo "No open PR found for head SHA $HEAD_SHA" >&2
echo "approved=false" >> "$GITHUB_OUTPUT"
exit 0
fi

PR_NUMBER="$(echo "$PR_JSON" | jq -r .number)"
HEAD_REPO="$(echo "$PR_JSON" | jq -r .head.repo.full_name)"
echo "pr-number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "head-repository=$HEAD_REPO" >> "$GITHUB_OUTPUT"

# Timeline check: the label must exist and no push event may follow
# it. Force pushes surface as head_ref_force_pushed, regular pushes
# as synchronize; both invalidate.
TIMELINE="$(gh api "repos/$REPO/issues/$PR_NUMBER/timeline" --paginate)"
LABEL_TIME="$(echo "$TIMELINE" \
| jq -r --arg label "$LABEL_NAME" \
'.[] | select(.event == "labeled" and .label.name == $label)
| .created_at' | tail -n 1)"
if [[ -z "$LABEL_TIME" ]]; then
echo "approved=false" >> "$GITHUB_OUTPUT"
exit 0
fi

PUSHED_AFTER_LABEL="$(echo "$TIMELINE" \
| jq -r --arg since "$LABEL_TIME" \
'.[] | select((.event == "synchronize" or .event == "head_ref_force_pushed")
and .created_at > $since) | .created_at' | head -n 1)"
if [[ -n "$PUSHED_AFTER_LABEL" ]]; then
echo "approved=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "approved=true" >> "$GITHUB_OUTPUT"
48 changes: 48 additions & 0 deletions docs/adr/0001-trusted-chain-entry-points.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!-- ----------------------------------------------------------------------------
Copyright (c) 2026 Contributors to the Eclipse Foundation

See the NOTICE file(s) distributed with this work for additional
information regarding copyright ownership.

This program and the accompanying materials are made available under the
terms of the Apache License Version 2.0 which is available at
https://www.apache.org/licenses/LICENSE-2.0

SPDX-License-Identifier: Apache-2.0
----------------------------------------------------------------------------- -->

# Trusted-chain entry points: direct call from on-pr plus gated workflow_run for forks

Jobs that need secrets or publish rights (the trusted chain, see
`docs/module/manuals/ci_concept.rst`) are implemented once as reusable
`workflow_call` workflows and get exactly two entry points:

1. A direct call from the repository's PR workflow (`on-pr.yml`), filtered by
`head.repo.full_name == github.repository` so only same-repository PRs,
`merge_group`, and `push` to main reach it.
2. A thin `workflow_run` listener for fork PRs only: it resolves the PR via the
API using the head SHA recorded by the original `pull_request` event and
applies the `approved-for-ci` gate: the label must be present on the PR and
its `labeled` timeline event must be newer than the last `synchronize`
event (any push, rebase, or force-push invalidates the approval; no commit
timestamps are compared). If the gate rejects, the run is skipped with an
explanatory PR comment rather than failing red. On success it calls the
same reusable workflow, checking out the exact SHA.

The fork gate itself is a local reusable `workflow_call` workflow from the
start (not inline in the listener), because it is the piece every trusted-chain
job (docs publisher, QNX test) will reuse and the first candidate to upstream
to cicd-workflows.

We chose this split over a single `workflow_run` listener handling all events
because same-repository runs then run natively in the base context with their
own event context (no SHA reconstruction needed for merge queue or push), while
the fork gate — the only genuinely dangerous path — stays small enough to
audit. The alternative of one monolithic listener would have made the
non-fork paths depend on the same reconstruction logic and the same
single-workflow failure domain. The reusable `workflow_call` core is shared by
both entry points, so behavior cannot drift between them; this shape is also
the candidate for upstreaming to cicd-workflows.

DASH license check (`license_check.yml`) is the pilot: it is trusted-only (no
untrusted part exists, since every run needs `ECLIPSE_GITLAB_API_TOKEN`).
Loading