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
116 changes: 96 additions & 20 deletions .github/workflows/markdown-quality.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Markdown Quality

Check warning on line 1 in .github/workflows/markdown-quality.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

1:1 [document-start] missing document start "---"

on:

Check warning on line 3 in .github/workflows/markdown-quality.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

3:1 [truthy] truthy value should be one of [false, true]
pull_request:
Expand Down Expand Up @@ -70,29 +70,105 @@
with:
# Avoid leaving credentials on disk for subsequent steps.
persist-credentials: false
# Shallow clone is sufficient for linting changed/all markdown files.
fetch-depth: 1
# Full history is required to diff against PR and push base commits.
fetch-depth: 0

- name: Select markdown targets
id: targets
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }}
PUSH_BEFORE_SHA: ${{ github.event.before || '' }}
HEAD_SHA: ${{ github.sha }}
run: |
set -euo pipefail

FILE_LIST="${RUNNER_TEMP}/markdown_targets.txt"
NULL_SHA="0000000000000000000000000000000000000000"
: > "$FILE_LIST"

is_markdown_target() {
local file="$1"

case "$file" in
*.md) ;;
*)
return 1
;;
esac

case "$file" in
*/node_modules/*|node_modules/*|*/.venv/*|.venv/*|*/venv/*|venv/*|*/.git/*|.git/*|*/dist/*|dist/*|*/build/*|build/*|*/.next/*|.next/*|*/.cache/*|.cache/*|data_out/*|mount/*|*/CHANGELOG.md|CHANGELOG.md)
return 1
;;
esac

return 0
}

write_targets_from_diff() {
local base_sha="$1"
local head_sha="$2"
local diff_file="${RUNNER_TEMP}/changed_markdown_files.txt"

if ! git diff --name-only --diff-filter=ACMR -z "$base_sha" "$head_sha" > "$diff_file"; then
echo "::warning::Unable to diff ${base_sha}...${head_sha}; markdown linting will be skipped. Verify repository history is available and the base/head commits exist."

Check warning on line 117 in .github/workflows/markdown-quality.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

117:141 [line-length] line too long (179 > 140 characters)
return 1
fi

while IFS= read -r -d '' file; do
# Skip entries that no longer exist in the working tree (for example, after renames).
[ -f "$file" ] || continue
is_markdown_target "$file" || continue
printf '%s\0' "$file" >> "$FILE_LIST"
done < "$diff_file"
}

if [ "$EVENT_NAME" = "pull_request" ] && [ -n "$PR_BASE_SHA" ] && [ -n "$PR_HEAD_SHA" ]; then
write_targets_from_diff "$PR_BASE_SHA" "$PR_HEAD_SHA" || true
elif [ "$EVENT_NAME" = "push" ] && [ -n "$PUSH_BEFORE_SHA" ] && [ "$PUSH_BEFORE_SHA" != "$NULL_SHA" ]; then
write_targets_from_diff "$PUSH_BEFORE_SHA" "$HEAD_SHA" || true
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then
git fetch --no-tags origin "$DEFAULT_BRANCH"
BASE_SHA="$(git merge-base "origin/$DEFAULT_BRANCH" "$HEAD_SHA")"
write_targets_from_diff "$BASE_SHA" "$HEAD_SHA" || true
fi
Comment on lines +133 to +137

echo "file_list=$FILE_LIST" >> "$GITHUB_OUTPUT"

- name: Lint Markdown files
id: lint
uses: DavidAnson/markdownlint-cli2-action@6b51ade7a9e4a75a7ad929842dd298a3804ebe8b
with:
# Lint all repo markdown except generated/output and mounted volumes.
# Keep this list in sync with .markdownlintignore if present.
globs: |
**/*.md
!**/node_modules/**
!**/.venv/**
!**/venv/**
!**/.git/**
!**/dist/**
!**/build/**
!**/.next/**
!**/.cache/**
!data_out/**
!mount/**
!**/CHANGELOG.md
fix: ${{ github.event_name == 'workflow_dispatch' && inputs.fix == true }}
env:
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail

FILE_LIST="${{ steps.targets.outputs.file_list }}"

if [ -z "$FILE_LIST" ]; then
echo "No Markdown files changed for this run, or all changed Markdown files were excluded from linting."
exit 0
fi

if [ ! -f "$FILE_LIST" ] || [ ! -s "$FILE_LIST" ]; then
echo "No Markdown files changed for this run, or all changed Markdown files were excluded from linting."
exit 0
fi

# Read the NUL-delimited target list written by write_targets_from_diff.
mapfile -d '' -t markdown_files < "$FILE_LIST"

printf 'Linting markdown targets:\n'
printf ' - %s\n' "${markdown_files[@]}"

args=()
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${{ inputs.fix }}" = "true" ]; then
args+=(--fix)
fi

npx --yes markdownlint-cli2 "${args[@]}" "${markdown_files[@]}"

Comment on lines +145 to 172

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

You can view more details about this finding in the Semgrep AppSec Platform.

- name: Generate auto-fix patch
if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.fix == true }}
Expand Down Expand Up @@ -130,7 +206,7 @@
echo "- Commit: \`${GITHUB_SHA}\`"
echo ""
if [ "${OUTCOME}" != "success" ]; then
echo "> ❌ Markdown lint failed. Run \`npx markdownlint-cli2 \"**/*.md\"\` locally, or re-run this workflow via \"Run workflow\" with **fix** enabled to download an auto-fix patch."

Check warning on line 209 in .github/workflows/markdown-quality.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

209:141 [line-length] line too long (194 > 140 characters)
else
echo "> ✅ All Markdown files passed linting."
fi
Expand Down
31 changes: 31 additions & 0 deletions tests/test_markdown_quality_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from pathlib import Path

import pytest
import yaml

pytestmark = pytest.mark.unit


def _load_workflow() -> dict:
workflow_path = Path(__file__).resolve().parents[1] / ".github" / "workflows" / "markdown-quality.yml"
return yaml.safe_load(workflow_path.read_text(encoding="utf-8"))


def test_markdown_quality_workflow_selects_changed_markdown_targets() -> None:
workflow = _load_workflow()
steps = workflow["jobs"]["markdownlint"]["steps"]

checkout_step = next(step for step in steps if step["name"] == "Checkout")
assert checkout_step["with"]["fetch-depth"] == 0

select_step = next(step for step in steps if step["name"] == "Select markdown targets")
lint_step = next(step for step in steps if step["name"] == "Lint Markdown files")

assert "git diff --name-only --diff-filter=ACMR -z" in select_step["run"]
assert 'git fetch --no-tags origin "$DEFAULT_BRANCH"' in select_step["run"]
assert "No Markdown files changed for this run" in lint_step["run"]
assert lint_step["env"]["EVENT_NAME"] == "${{ github.event_name }}"
assert 'markdownlint-cli2 "${args[@]}" "${markdown_files[@]}"' in lint_step["run"]
assert "uses" not in lint_step, "Lint step should run only on selected markdown files, not the entire repo."
Loading