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:
parent
64f91a00d8
commit
0964eebb10
3 changed files with 193 additions and 169 deletions
177
.github/workflows/ci.yml
vendored
177
.github/workflows/ci.yml
vendored
|
|
@ -35,97 +35,10 @@ jobs:
|
||||||
- name: Detect docs-only changes
|
- name: Detect docs-only changes
|
||||||
id: scope
|
id: scope
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
env:
|
||||||
set -euo pipefail
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
||||||
write_empty_docs_files() {
|
run: ./scripts/ci/detect_change_scope.sh
|
||||||
{
|
|
||||||
echo "docs_files<<EOF"
|
|
||||||
echo "EOF"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
||||||
BASE="${{ github.event.pull_request.base.sha }}"
|
|
||||||
else
|
|
||||||
BASE="${{ github.event.before }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint Gate (Format + Clippy)
|
name: Lint Gate (Format + Clippy)
|
||||||
|
|
@ -279,6 +192,8 @@ jobs:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Post actionable lint failure summary
|
- name: Post actionable lint failure summary
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
|
|
@ -290,84 +205,8 @@ jobs:
|
||||||
DOCS_RESULT: ${{ needs.docs-quality.result }}
|
DOCS_RESULT: ${{ needs.docs-quality.result }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const owner = context.repo.owner;
|
const script = require('./scripts/ci/lint_feedback.js');
|
||||||
const repo = context.repo.repo;
|
await script({github, context, core});
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
workflow-owner-approval:
|
workflow-owner-approval:
|
||||||
name: Workflow Owner Approval
|
name: Workflow Owner Approval
|
||||||
|
|
|
||||||
95
scripts/ci/detect_change_scope.sh
Executable file
95
scripts/ci/detect_change_scope.sh
Executable 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"
|
||||||
90
scripts/ci/lint_feedback.js
Normal file
90
scripts/ci/lint_feedback.js
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue