Skip to content
Closed
76 changes: 76 additions & 0 deletions .github/actions/get-changed-files/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Get Changed Files
description: >
Compute changed files between base and head SHAs using PR-accurate diff.
Works for PRs from forks by fetching base_sha from an upstream remote.

inputs:
repo_path:
description: Path to the checked-out PR repository
default: pr
base_sha:
description: Base commit SHA (from github.event.pull_request.base.sha)
required: true
head_sha:
description: Head commit SHA (from github.event.pull_request.head.sha)
required: true
base_repo:
description: 'Base repository (owner/name) used to fetch base_sha for forks'
default: ${{ github.repository }}
exclude_pattern:
description: Grep pattern for paths to exclude from changed-files output
default: '^\.github/'

outputs:
all_changed:
description: Newline-separated list of all changed/added non-.github files
value: ${{ steps.compute.outputs.all_changed }}
files_updated:
description: Newline-separated list of changed/added .md files
value: ${{ steps.compute.outputs.files_updated }}

runs:
using: composite
steps:
- name: Compute changed files
id: compute
working-directory: ${{ inputs.repo_path }}
shell: bash
run: |
set -euo pipefail

# Add an 'upstream' remote pointing at the base repository so we can
# fetch the exact base SHA even when the PR comes from a fork (where
# 'origin' only has the fork's commits).
if ! git remote get-url upstream >/dev/null 2>&1; then
git remote add upstream "https://github.com/${{ inputs.base_repo }}.git"
fi

# Fetch just the base commit so the diff is available without a full clone.
git fetch --no-tags --depth=1 upstream "${{ inputs.base_sha }}"

# Defensively confirm the head SHA exists locally (it should via checkout).
git rev-parse --verify "${{ inputs.head_sha }}^{commit}" >/dev/null

# Compute the diff between base and head using the exact PR SHAs.
# --diff-filter=d excludes deleted files; we only want changed/added files.
CHANGED_ALL=$(git diff --name-only --diff-filter=d \
"${{ inputs.base_sha }}...${{ inputs.head_sha }}" \
| grep -v '${{ inputs.exclude_pattern }}' || true)

FILES=$(echo "$CHANGED_ALL" | grep '\.md$' || true)

# Emit newline-separated multiline outputs using the GitHub Actions heredoc delimiter syntax.
{
echo "files_updated<<EOF"
echo "$FILES"
echo "EOF"
} >> "$GITHUB_OUTPUT"

{
echo "all_changed<<EOF"
echo "$CHANGED_ALL"
echo "EOF"
} >> "$GITHUB_OUTPUT"

echo "## Changed files" >> "$GITHUB_STEP_SUMMARY"
echo "$FILES" >> "$GITHUB_STEP_SUMMARY"
25 changes: 16 additions & 9 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,28 @@ Utility action named "Markdown Lint Check" (same name as `md-lint-check.yml`) th
Checks Pull Requests for broken links.

This workflow:
- Checks out the **base branch** into `base/` and the **PR head** into `pr/` (each checkout uses an explicit path so neither overwrites the other)
- Uses inline `git diff` from `pr/` (no third-party action) to list changed files between the base ref and HEAD, excluding deleted files and paths under `.github/`
- Copies **all** changed files (including images and other assets) from `pr/` into `base/` so link targets exist, then runs the link checker only on changed `.md` files so relative links resolve correctly
- Config and scripts are always taken from `base/` (the base branch), not from the PR
- Checks out the **PR head** to the workspace root (provides the composite action files and the PR's content) and the **base branch** (OWASP/wstg `master`) into `base/`
- Uses the `.github/actions/get-changed-files` composite action with the exact `base.sha`/`head.sha` from the PR event for fork-safe changed-file detection
- Copies **all** changed files (including images and other assets) into `base/` so link targets exist, then runs the link checker only on changed `.md` files so relative links resolve correctly
- Config is always taken from `base/` (the base branch), not from the PR

- Trigger: Pull Requests (when `.md` files are changed, excluding `.github/**`).
- Config File: `markdown-link-check-config.json`

## `md-link-check-full.yml`

Checks all Markdown files in the repository for broken links.

- Trigger: Pull Requests (when `.md` files are changed, excluding `.github/**`). Manual (`workflow_dispatch`).
- Trigger: Manual (`workflow_dispatch`), GitHub web UI.
- Config File: `markdown-link-check-config.json`

## `md-lint-check.yml`

Checks Markdown files and flags style or syntax issues.

This workflow:
- Checks out the **base branch** into `base/` and the **PR head** into `pr/` (each checkout uses an explicit path so neither overwrites the other)
- Uses inline `git diff` from `pr/` to list changed `.md` files (excluding deleted files and `.github/`), then runs `markdownlint-cli2` only on those files under `pr/`
- Checks out the **PR head** to the workspace root and the **base branch** (OWASP/wstg `master`) into `base/`
- Uses the `.github/actions/get-changed-files` composite action with the exact `base.sha`/`head.sha` from the PR event for fork-safe changed-file detection, then runs `markdownlint-cli2` only on changed `.md` files
- Uses `format_lint_output.py` from `base/.github/workflows/scripts/` to format output for PR comments
- Uploads artifacts for both success and failure cases to work with `comment.yml`
- Config and scripts are always taken from `base/` (the base branch), not from the PR
Expand All @@ -75,8 +82,8 @@ This workflow:
Checks Markdown files for spelling style and typo issues.

This workflow:
- Checks out the **base branch** into `base/` and the **PR head** into `pr/` (each checkout uses an explicit path so neither overwrites the other)
- Uses inline `git diff` from `pr/` to list changed `.md` files (excluding deleted files and `.github/`), then runs textlint only on those files under `pr/`
- Checks out the **PR head** to the workspace root and the **base branch** (OWASP/wstg `master`) into `base/`
- Uses the `.github/actions/get-changed-files` composite action with the exact `base.sha`/`head.sha` from the PR event for fork-safe changed-file detection, then runs textlint only on changed `.md` files
- Config is always taken from `base/` (the base branch), not from the PR

- Trigger: Pull Requests (when `.md` files are changed, excluding `.github/**`).
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/md-link-check-full.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Markdown Link Check (Full Repository)

on:
workflow_dispatch:

jobs:
link-check:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout Base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: OWASP/wstg
ref: master
path: base
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: 24
- name: Cache npm Global
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-global-markdown-link-check
- name: Install Dependencies
run: npm install -g markdown-link-check@3.11.0
- name: Repository Link Check
run: |
cd base
touch log err
find . -name \*.md -exec markdown-link-check -q -v --config .github/configs/markdown-link-check-config.json {} 1>> log 2>> err \;
if grep -q "ERROR:" err ; then exit 1 ; else echo -e "No broken links found."; fi
echo $(cat log)
echo $(cat err)
- name: Show Broken Links
if: failure()
run: |
cat base/log | awk -v RS="FILE:" 'match($0, /(\S*\.md).*\[✖\].*([0-9]*\slinks\schecked\.)(.*)/, arr ) { print "FILE:"arr[1] arr[3] > "brokenlinks.txt"}'
sed -i 's/\[✖\]/\[❌\]/g' brokenlinks.txt
cat brokenlinks.txt
- name: Create Summary
if: failure()
run: |
echo "**The following links are broken:**" > artifact.txt
cat brokenlinks.txt | tee -a artifact.txt
rm -f base/err base/log
cat artifact.txt >> $GITHUB_STEP_SUMMARY
- name: Upload List of Broken Links
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: broken-links
path: artifact.txt
62 changes: 23 additions & 39 deletions .github/workflows/md-link-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,24 @@ on:
paths:
- '**.md'
- '!.github/**'
workflow_dispatch:

jobs:
link-check:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout Base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: OWASP/wstg
ref: ${{ github.base_ref || 'master' }}
path: base
- name: Checkout PR
if: github.event_name == 'pull_request'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
path: pr
fetch-depth: 0
- name: Checkout Base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: OWASP/wstg
ref: master
path: base
- name: Save PR Number
env:
PR_NUMBER: ${{ github.event.number }}
Expand All @@ -42,58 +39,45 @@ jobs:
- name: Install Dependencies
run: npm install -g markdown-link-check@3.11.0
- name: Get Changed Files
if: github.event_name == 'pull_request'
# Use base/head SHAs from the PR event for fork-safe, PR-accurate changed-file detection.
# github.base_ref resolves differently for forks, while base.sha/head.sha are always correct.
id: files
working-directory: pr
run: |
# Get list of changed .md files (excluding .github/) for link checking
git fetch origin ${{ github.base_ref }}
CHANGED_ALL=$(git diff --name-only --diff-filter=d origin/${{ github.base_ref }}...HEAD | grep -v '^\.github/' || true)
FILES=$(echo "$CHANGED_ALL" | grep '\.md$' || true)
# Convert newlines to spaces for compatibility with expected format
FILES_SPACE_SEPARATED=$(echo "$FILES" | tr '\n' ' ' | xargs)
ALL_SPACE_SEPARATED=$(echo "$CHANGED_ALL" | tr '\n' ' ' | xargs)
echo "files_updated=$FILES_SPACE_SEPARATED" >> $GITHUB_OUTPUT
echo "all_changed=$ALL_SPACE_SEPARATED" >> $GITHUB_OUTPUT
echo "## Changed files" >> $GITHUB_STEP_SUMMARY
echo "$FILES" >> $GITHUB_STEP_SUMMARY
shell: bash
uses: ./.github/actions/get-changed-files
with:
base_sha: ${{ github.event.pull_request.base.sha }}
head_sha: ${{ github.event.pull_request.head.sha }}
base_repo: ${{ github.event.pull_request.base.repo.full_name }}
repo_path: .
- name: PR Link Check
if: github.event_name == 'pull_request'
env:
FILES: '${{ steps.files.outputs.files_updated }}'
ALL_CHANGED: '${{ steps.files.outputs.all_changed }}'
shell: bash
run: |
set -euo pipefail

readarray -t files_arr <<< "$FILES"
readarray -t all_arr <<< "$ALL_CHANGED"

echo "The Following files were changed or created:"
printf '%s\n' $FILES
printf '%s\n' "${files_arr[@]}"
touch log err

# Copy all changed files (md + images etc.) from pr/ to base/ so link targets exist when we check .md files
for FILE in $ALL_CHANGED; do
# Copy all changed files (md + images etc.) to base/ so link targets exist when we check .md files
for FILE in "${all_arr[@]}"; do
[ -z "$FILE" ] && continue
mkdir -p "base/$(dirname "$FILE")"
cp "pr/$FILE" "base/$FILE"
cp "$FILE" "base/$FILE"
done

# Check only .md files in base/ where relative links can be resolved
for FILE in $FILES; do
for FILE in "${files_arr[@]}"; do
[ -z "$FILE" ] && continue
if printf '%s\n' "$FILE" | grep -q '.*\.md$'; then
markdown-link-check -q -v -c base/.github/configs/markdown-link-check-config.json "base/$FILE" 1>> log 2>> err
fi
done

if grep -q "ERROR:" err ; then exit 1 ; else echo -e "No broken links found."; fi
echo $(cat log)
echo $(cat err)
- name: Repository Link Check
if: github.event_name == 'workflow_dispatch'
run: |
cd base
touch log err
find . -name \*.md -exec markdown-link-check -q -v --config .github/configs/markdown-link-check-config.json {} 1>> log 2>> err \;
if grep -q "ERROR:" err ; then exit 1 ; else echo -e "No broken links found."; fi
echo $(cat log)
echo $(cat err)
Expand Down
40 changes: 19 additions & 21 deletions .github/workflows/md-lint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ jobs:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout Base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: OWASP/wstg
ref: ${{ github.base_ref || 'master' }}
path: base
- name: Checkout PR
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
path: pr
fetch-depth: 0
- name: Checkout Base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: OWASP/wstg
ref: master
path: base
- name: Save PR Number
env:
PR_NUMBER: ${{ github.event.number }}
Expand All @@ -42,18 +41,15 @@ jobs:
- name: Install Dependencies
run: npm install -g markdownlint-cli2
- name: Get Changed Files
# Use base/head SHAs from the PR event for fork-safe, PR-accurate changed-file detection.
# github.base_ref resolves differently for forks, while base.sha/head.sha are always correct.
id: files
working-directory: pr
run: |
# Get list of changed .md files (excluding .github/)
git fetch origin ${{ github.base_ref }}
FILES=$(git diff --name-only --diff-filter=d origin/${{ github.base_ref }}...HEAD | grep '\.md$' | grep -v '^\.github/' || true)
# Convert newlines to spaces for compatibility with expected format
FILES_SPACE_SEPARATED=$(echo "$FILES" | tr '\n' ' ' | xargs)
echo "files_updated=$FILES_SPACE_SEPARATED" >> $GITHUB_OUTPUT
echo "## Changed files" >> $GITHUB_STEP_SUMMARY
echo "$FILES" >> $GITHUB_STEP_SUMMARY
shell: bash
uses: ./.github/actions/get-changed-files
with:
base_sha: ${{ github.event.pull_request.base.sha }}
head_sha: ${{ github.event.pull_request.head.sha }}
base_repo: ${{ github.event.pull_request.base.repo.full_name }}
repo_path: .
- name: Run Linter
env:
FILES: '${{ steps.files.outputs.files_updated }}'
Expand All @@ -64,9 +60,11 @@ jobs:
# Initialize lint file (will remain empty if there are no issues)
: > lint.txt

for FILE in $FILES; do
if [ -f "pr/$FILE" ]; then
OUTPUT="$(markdownlint-cli2 "pr/$FILE" --config base/.github/configs/.markdownlint.json 2>&1)" || STATUS=$?
readarray -t files_arr <<< "$FILES"
for FILE in "${files_arr[@]}"; do
[ -z "$FILE" ] && continue
if [ -f "$FILE" ]; then
OUTPUT="$(markdownlint-cli2 "$FILE" --config base/.github/configs/.markdownlint.json 2>&1)" || STATUS=$?
if [ "${STATUS:-0}" -eq 0 ]; then
# File passed linting, continue to next file
continue
Expand Down
Loading