refactor(ci): extract large inline scripts to scripts/ci/ (#587)

The CI workflow contained a ~90-line bash script for change-detection
(lines 38-128) and a ~80-line JavaScript block for lint feedback
(lines 292-370) directly inline in the YAML. Large inline scripts are
harder to test, lint, and maintain than standalone files.

Extract:
- Change-detection logic → scripts/ci/detect_change_scope.sh
- Lint feedback logic → scripts/ci/lint_feedback.js

The workflow now calls these external scripts. GitHub expression values
that were previously interpolated inline are passed as environment
variables instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Alex Gorevski 2026-02-17 11:17:00 -08:00 committed by GitHub
parent 64f91a00d8
commit 0964eebb10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 193 additions and 169 deletions

View file

@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Detect change scope for CI pipeline.
# Classifies changed files into docs-only, rust, workflow categories
# and writes results to $GITHUB_OUTPUT.
#
# Required environment variables:
# GITHUB_OUTPUT — GitHub Actions output file
# EVENT_NAME — github.event_name (push or pull_request)
# BASE_SHA — base commit SHA to diff against
set -euo pipefail
write_empty_docs_files() {
{
echo "docs_files<<EOF"
echo "EOF"
} >> "$GITHUB_OUTPUT"
}
BASE="$BASE_SHA"
if [ -z "$BASE" ] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
{
echo "docs_only=false"
echo "docs_changed=false"
echo "rust_changed=true"
echo "workflow_changed=false"
echo "base_sha="
} >> "$GITHUB_OUTPUT"
write_empty_docs_files
exit 0
fi
CHANGED="$(git diff --name-only "$BASE" HEAD || true)"
if [ -z "$CHANGED" ]; then
{
echo "docs_only=false"
echo "docs_changed=false"
echo "rust_changed=false"
echo "workflow_changed=false"
echo "base_sha=$BASE"
} >> "$GITHUB_OUTPUT"
write_empty_docs_files
exit 0
fi
docs_only=true
docs_changed=false
rust_changed=false
workflow_changed=false
docs_files=()
while IFS= read -r file; do
[ -z "$file" ] && continue
if [[ "$file" == .github/workflows/* ]]; then
workflow_changed=true
fi
if [[ "$file" == docs/* ]] \
|| [[ "$file" == *.md ]] \
|| [[ "$file" == *.mdx ]] \
|| [[ "$file" == "LICENSE" ]] \
|| [[ "$file" == ".markdownlint-cli2.yaml" ]] \
|| [[ "$file" == .github/ISSUE_TEMPLATE/* ]] \
|| [[ "$file" == .github/pull_request_template.md ]]; then
if [[ "$file" == *.md ]] \
|| [[ "$file" == *.mdx ]] \
|| [[ "$file" == "LICENSE" ]] \
|| [[ "$file" == .github/pull_request_template.md ]]; then
docs_changed=true
docs_files+=("$file")
fi
continue
fi
docs_only=false
if [[ "$file" == src/* ]] \
|| [[ "$file" == tests/* ]] \
|| [[ "$file" == "Cargo.toml" ]] \
|| [[ "$file" == "Cargo.lock" ]] \
|| [[ "$file" == "deny.toml" ]]; then
rust_changed=true
fi
done <<< "$CHANGED"
{
echo "docs_only=$docs_only"
echo "docs_changed=$docs_changed"
echo "rust_changed=$rust_changed"
echo "workflow_changed=$workflow_changed"
echo "base_sha=$BASE"
echo "docs_files<<EOF"
printf '%s\n' "${docs_files[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

View file

@ -0,0 +1,90 @@
// Post actionable lint failure summary as a PR comment.
// Used by the lint-feedback CI job via actions/github-script.
//
// Required environment variables:
// RUST_CHANGED — "true" if Rust files changed
// DOCS_CHANGED — "true" if docs files changed
// LINT_RESULT — result of the lint job
// LINT_DELTA_RESULT — result of the strict delta lint job
// DOCS_RESULT — result of the docs-quality job
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const issueNumber = context.payload.pull_request?.number;
if (!issueNumber) return;
const marker = "<!-- ci-lint-feedback -->";
const rustChanged = process.env.RUST_CHANGED === "true";
const docsChanged = process.env.DOCS_CHANGED === "true";
const lintResult = process.env.LINT_RESULT || "skipped";
const lintDeltaResult = process.env.LINT_DELTA_RESULT || "skipped";
const docsResult = process.env.DOCS_RESULT || "skipped";
const failures = [];
if (rustChanged && !["success", "skipped"].includes(lintResult)) {
failures.push("`Lint Gate (Format + Clippy)` failed.");
}
if (rustChanged && !["success", "skipped"].includes(lintDeltaResult)) {
failures.push("`Lint Gate (Strict Delta)` failed.");
}
if (docsChanged && !["success", "skipped"].includes(docsResult)) {
failures.push("`Docs Quality` failed.");
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
const existing = comments.find((comment) => (comment.body || "").includes(marker));
if (failures.length === 0) {
if (existing) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existing.id,
});
}
core.info("No lint/docs gate failures. No feedback comment required.");
return;
}
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const body = [
marker,
"### CI lint feedback",
"",
"This PR failed one or more fast lint/documentation gates:",
"",
...failures.map((item) => `- ${item}`),
"",
"Open the failing logs in this run:",
`- ${runUrl}`,
"",
"Local fix commands:",
"- `./scripts/ci/rust_quality_gate.sh`",
"- `./scripts/ci/rust_strict_delta_gate.sh`",
"- `./scripts/ci/docs_quality_gate.sh`",
"",
"After fixes, push a new commit and CI will re-run automatically.",
].join("\n");
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
}
};