Skip to content
Merged
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
63 changes: 63 additions & 0 deletions .github/workflows/repo_checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
###############################################################################
# Copyright (c) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
###############################################################################

name: repo_checks

# Repository-wide text checks.
#
# THIS LIVES IN ITS OWN WORKFLOW BECAUSE IT MUST NOT BE PATH-FILTERED, and a
# path filter is a property of a workflow rather than of a job. The check
# selects its input with `git ls-files` and scans every tracked file, so any
# file at all can carry a finding.
#
# That is not hypothetical here. The pass that introduced the fixed
# disclosure line covered source files only, and the drift it was meant to end
# survived for months afterwards in CMake files, toolchain files, shell and
# PowerShell scripts, a GDB script and a Visual Studio manifest -- precisely
# the files a source-path filter would have skipped. A workflow that gates no
# pull request anybody opens is worse than no workflow, because it looks like
# coverage.
#
# Cheap enough that running it on everything costs nothing worth measuring: it
# is grep over a repository this size, with no toolchain, no build and no
# cache.

on:
# No `paths:` on either trigger, deliberately. See above.
push:
branches: [ dev, master ]
pull_request:
branches: [ dev, master ]

# A second push to the same branch makes the first answer irrelevant.
concurrency:
group: repo-checks-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
checks:
runs-on: ubuntu-24.04
steps:
# Actions are pinned to a commit SHA with the version in the trailing
# comment. A tag can be moved; a SHA cannot, which is what makes "which
# code ran in our CI" answerable from the repository.
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Check the AI disclosure comments
run: scripts/check_ai_disclosure.sh
120 changes: 120 additions & 0 deletions scripts/check_ai_disclosure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/bin/bash
###############################################################################
# Copyright (c) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
###############################################################################
#
# Fail the build if a file's AI disclosure comment has drifted from the one
# accepted form.
#
# A file that was edited with AI assistance carries exactly one line:
#
# Portions of this file were generated with AI assistance.
#
# written with the comment character that file already uses. It names no
# product, no model and no version, and a file carries at most one of it, ever.
#
# The text is fixed for a reason that is easy to miss. An earlier convention
# named the product and the model -- "Some portions generated by <product>
# (<model>)" -- and the result was not attribution but accumulation: each tool
# that touched a file failed to recognise the line another tool had left, and
# appended its own. Files reached three stacked lines, and one product ended
# up spelled four different ways across the tree, which made the record
# unusable for the one question it was meant to answer.
#
# Precise attribution lives on the commit instead, where the Assisted-by
# trailer is dated and attached to the diff it describes:
#
# git log --format='%h %(trailers:key=Assisted-by,valueonly)' -- <path>
#
# A file-level flag answers WHETHER; the history answers WHO. A header line
# cannot hold the second honestly, because the code it names gets rewritten
# and the line stays.
#
# The AI Disclosure paragraph in a new file's copyright header is different and
# is not checked here: it keeps its product and model version, because a file
# is created once and that record cannot grow.
#
# This script is excluded from its own scan. It has to spell the rejected
# forms in order to look for them.

set -euo pipefail

readonly ROOT="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)"
readonly SELF='scripts/check_ai_disclosure.sh'
readonly FIXED='Portions of this file were generated with AI assistance.'

cd "${ROOT}"

# Tracked files only. A build tree is not this repository's text to police,
# and scanning one would make the check depend on whether somebody had built.
mapfile -d '' -t FILES < <(git ls-files -z | grep -zZv "^${SELF}$")

status=0

report() {
printf '%s\n\n' "$1" >&2
printf '%s\n\n' "$2" >&2
status=1
}

# 1. The superseded per-edit form, which names a product and a model.
hits="$(grep -nI 'Some portions generated by' -- "${FILES[@]}" 2>/dev/null || true)"
if [ -n "${hits}" ]; then
report "AI disclosure check FAILED: superseded per-edit form.

Replace each of these with the fixed line, keeping the file's comment
character:

${FIXED}" "${hits}"
fi

# 2. A doubled comment marker, such as '; //' or '@ //'. Assembly dialects
# differ -- armasm and IAR use ';', GNU as uses '@' or '//' -- and writing
# both is a symptom of a tool guessing rather than reading the file.
hits="$(grep -nIE '(//|[;@#])[[:space:]]*//[[:space:]]*Portions of this file were generated' \
-- "${FILES[@]}" 2>/dev/null || true)"
if [ -n "${hits}" ]; then
report "AI disclosure check FAILED: doubled comment marker.

Use the single comment character the rest of the file uses." "${hits}"
fi

# 3. More than one disclosure line in a file. This is the failure the fixed
# text exists to prevent, so it is worth catching directly rather than
# inferring it from the form.
hits="$(grep -cIF "${FIXED}" -- "${FILES[@]}" 2>/dev/null | awk -F: '$NF > 1' || true)"
if [ -n "${hits}" ]; then
report "AI disclosure check FAILED: more than one disclosure line.

A file carries at most one, ever. Keep the first and delete the rest; the
commit trailer, not the header, records which agents have touched the file." "${hits}"
fi

# 4. A near miss. A line that is clearly meant to be the disclosure but is
# not spelled exactly right defeats every deduplication that follows it.
hits="$(grep -nIF 'AI assistance' -- "${FILES[@]}" 2>/dev/null \
| grep -vF "${FIXED}" || true)"
if [ -n "${hits}" ]; then
report "AI disclosure check FAILED: the text is not spelled exactly.

The accepted text, character for character, is:

${FIXED}" "${hits}"
fi

if [ "${status}" -eq 0 ]; then
echo "AI disclosure check passed."
fi

exit "${status}"