Files
copilotkit__copilotkit/.github/workflows/static_quality.yml
2026-06-04 17:18:03 -07:00

326 lines
14 KiB
YAML

name: static / quality
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- "README.md"
- "examples/**"
pull_request:
branches: [main]
paths-ignore:
- "docs/**"
- "README.md"
- "examples/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: "--max-old-space-size=4096"
NX_VERBOSE_LOGGING: true
NX_CI_EXECUTION_ID: ${{ github.head_ref }}-${{ github.sha }}-${{ github.run_attempt }}
NX_CI_EXECUTION_ENV: "Static Quality"
permissions:
contents: read
jobs:
format:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
# Check the head branch out from the head repo, not the base repo.
# For fork PRs the head branch only exists on the fork, so defaulting
# to the base repo makes checkout fail with "a branch or tag with the
# name '<branch>' could not be found". Same-repo PRs resolve to the
# base repo unchanged, so the auto-format push-back below still works.
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
# Full history so we can diff HEAD against the current base branch
# tip to scope the formatter to PR-changed files.
fetch-depth: 0
- name: Install oxfmt
run: npm install -g oxfmt@0.36
- name: Install ruff
# Pin ruff so a compromised or breaking release can't land on the next
# PR run with the persisted-credentials write token in this job.
# Bumped automatically by Dependabot? — no; ruff isn't tracked there.
# Bump manually when needed.
run: pipx install ruff==0.15.13
- name: Collect PR-changed files for formatting
if: github.event_name == 'pull_request'
id: changed
env:
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
base_ref="${PR_BASE_REF}"
# Fetch the current tip of the base branch so the merge-base tracks
# main as it advances (using the PR's stored base.sha would pull in
# every file main has touched since the PR opened).
git fetch --no-tags origin "$base_ref"
# Scope to files changed between the current base-branch merge-base
# and HEAD so advances on main don't drag unrelated files into the
# PR. Restrict to oxfmt-supported extensions so oxfmt never errors
# on an unknown target. Canonical list lives upstream in oxfmt
# (https://github.com/oxc-project/oxc-formatter) — update here when
# oxfmt adds a new format.
#
# Exclude lockfiles: they match *.json / *.yaml but oxfmt rejects
# them internally (size threshold or filename heuristic), which
# caused lockfile-only PRs to fail with "Expected at least one
# target file". Lockfiles are auto-generated by npm/pnpm and should
# never be hand-formatted regardless.
git diff --name-only --diff-filter=ACMR "origin/$base_ref"...HEAD -- \
'*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' \
'*.json' '*.jsonc' '*.json5' \
'*.md' \
'*.css' '*.yml' '*.yaml' '*.html' '*.vue' '*.py' \
':!**/package-lock.json' ':!**/pnpm-lock.yaml' ':!**/yarn.lock' \
> .pr-format-files.txt
: > .pr-format-files.existing.txt
while IFS= read -r f; do
[ -n "$f" ] && [ -f "$f" ] && printf '%s\n' "$f" >> .pr-format-files.existing.txt
done < .pr-format-files.txt
# Drop tracked-but-gitignored paths (e.g. fixtures under a
# `recorded/` rule). Without this, oxfmt would rewrite them and
# the auto-commit step's `git add` would refuse the ignored
# path, killing the whole step and leaving the PR unfixed.
if [ -s .pr-format-files.existing.txt ]; then
git ls-files -i -c --exclude-standard > .pr-format-files.ignored.txt
grep -vxFf .pr-format-files.ignored.txt .pr-format-files.existing.txt > .pr-format-files.scoped.txt || true
mv .pr-format-files.scoped.txt .pr-format-files.existing.txt
fi
count=$(wc -l < .pr-format-files.existing.txt | tr -d ' ')
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "PR-changed format candidates: $count"
cat .pr-format-files.existing.txt
- name: Run formatter (fix on PR)
run: |
if [ "${{ steps.changed.outputs.count }}" = "0" ]; then
echo "No formattable files changed in this PR — skipping."
exit 0
fi
# oxfmt: auto-fix JS/TS/JSON/MD/CSS/YAML/HTML/Vue
if ! xargs -a .pr-format-files.existing.txt oxfmt --no-error-on-unmatched-pattern --write; then
echo "::warning::oxfmt exited with error — auto-fix may be incomplete"
fi
# ruff: auto-fix Python
py_files=$(grep -E '\.py$' .pr-format-files.existing.txt || true)
if [ -n "$py_files" ]; then
echo "$py_files" | xargs ruff format || echo "::warning::ruff format exited with error"
fi
if [ -n "$(git diff --name-only)" ]; then
echo "format_fixed=true" >> "$GITHUB_ENV"
fi
# Check mode: verify everything is formatted
xargs -a .pr-format-files.existing.txt oxfmt --no-error-on-unmatched-pattern --check
if [ -n "$py_files" ]; then
echo "$py_files" | xargs ruff format --check
fi
- name: Configure git for push
if: >-
env.format_fixed == 'true' &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/"
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit formatting fixes
if: >-
env.format_fixed == 'true' &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
run: |
if [ -z "$(git diff --name-only)" ]; then
echo "No formatting changes to commit"
exit 0
fi
# Stage only the files the formatter was scoped to operate on.
# Piping `git diff --name-only` into `git add` is unsafe: if a
# tracked-but-gitignored path shows up in the diff, `git add`
# aborts the whole step and the auto-fix push never lands —
# leaving formatting violations on the PR branch and (post-
# merge) on main.
xargs -a .pr-format-files.existing.txt git add --
git commit -m "style: auto-fix formatting"
git push
oxlint:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup pnpm
# Omit `version:` so pnpm/action-setup inherits from the repo's
# `packageManager` field in package.json (via corepack).
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Use Node.js 20
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20.x
# setup-node built-in cache is fork-safe (fork PRs can't write to base repo cache)
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run oxlint check
run: pnpm run lint
package-quality:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup pnpm
# Omit `version:` so pnpm/action-setup inherits from the repo's
# `packageManager` field in package.json (via corepack).
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Use Node.js 20
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20.x
# setup-node built-in cache is fork-safe (fork PRs can't write to base repo cache)
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Configure Nx Cloud environment
run: |
{
echo "NX_CI_EXECUTION_ID=${{ github.run_id }}-${{ github.run_attempt }}-quality-packages"
echo "NX_CLOUD_NO_TIMEOUTS=true"
echo "NX_CLOUD_DISTRIBUTED_EXECUTION=false"
echo "NX_NO_CLOUD=true"
} >> "$GITHUB_ENV"
- name: Run publint and attw
run: pnpm run check:packages
commitlint:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup pnpm
# Omit `version:` so pnpm/action-setup inherits from the repo's
# `packageManager` field in package.json (via corepack).
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Use Node.js 20
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20.x
# setup-node built-in cache is fork-safe (fork PRs can't write to base repo cache)
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Validate current commit (last commit) with commitlint
if: github.event_name == 'push'
run: |
# Skip merge commits. GitHub's "Create a merge commit" option takes
# the message from the PR body, which can contain markdown lists
# that parse as additional (empty) commit subjects and fail
# subject-empty / type-empty — see commit 5ed233f01.
parents=$(git rev-list --parents -n 1 HEAD | awk '{print NF - 1}')
if [ "$parents" -gt 1 ]; then
echo "HEAD is a merge commit ($parents parents) — skipping commitlint."
exit 0
fi
npx commitlint --last --verbose
- name: Validate PR commits with commitlint
id: commitlint
if: github.event_name == 'pull_request'
continue-on-error: true
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: npx commitlint --from "${PR_BASE_SHA}" --to "${PR_HEAD_SHA}" --verbose 2>&1 | tee /tmp/commitlint-output.txt
- name: Post fix suggestion on failure
if: github.event_name == 'pull_request' && steps.commitlint.outcome == 'failure'
continue-on-error: true
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const fs = require('fs');
const output = fs.readFileSync('/tmp/commitlint-output.txt', 'utf8');
const body = `### ❌ Commitlint failed\n\nCommit messages must follow [Conventional Commits](https://www.conventionalcommits.org/).\n\n**Valid prefixes:** \`feat:\`, \`fix:\`, \`docs:\`, \`style:\`, \`refactor:\`, \`test:\`, \`chore:\`, \`ci:\`, \`perf:\`, \`build:\`\n\n**Example:** \`feat: add user authentication\`\n\n<details><summary>Full output</summary>\n\n\`\`\`\n${output}\n\`\`\`\n</details>\n\nTo fix, amend your commit messages:\n\`\`\`bash\ngit rebase -i HEAD~N # N = number of commits to fix\n# Change 'pick' to 'reword' for bad commits\n\`\`\``;
// Find existing comment to update
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes('Commitlint failed'));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Fail if commitlint failed
if: github.event_name == 'pull_request' && steps.commitlint.outcome == 'failure'
run: exit 1