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
97 changes: 97 additions & 0 deletions .github/actions/basic-checks/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: "Basic PR Quality Checks"
description: >
Composite action that runs 4 basic PR quality checks: PR title,
branch name, commit messages, and merge conflicts

inputs:
types:
description: "Valid commit type prefixes (pipe-separated)"
required: false
default: >
build|chore|ci|docs|feat|fix|hotfix|perf|refactor|revert|style|test
max-length:
description: "Maximum allowed length for PR title and commit messages"
required: false
default: "75"
pr-title:
description: "The PR title to validate"
required: true
branch-name:
description: "The branch name to validate"
required: true
base-sha:
description: "Base commit SHA for commit message comparison"
required: true
head-sha:
description: "Head commit SHA for commit message comparison"
required: true

outputs:
pr-title-status:
description: "Status of the PR title check (success or failure)"
value: ${{ steps.pr-title.outputs.status }}
pr-title-summary:
description: "Summary message for the PR title check"
value: ${{ steps.pr-title.outputs.summary }}
branch-name-status:
description: "Status of the branch name check (success or failure)"
value: ${{ steps.branch-name.outputs.status }}
branch-name-summary:
description: "Summary message for the branch name check"
value: ${{ steps.branch-name.outputs.summary }}
commit-messages-status:
description: "Status of the commit messages check (success or failure)"
value: ${{ steps.commit-messages.outputs.status }}
commit-messages-summary:
description: "Summary message for the commit messages check"
value: ${{ steps.commit-messages.outputs.summary }}
commit-messages-report:
description: "Detailed report for failed commit messages"
value: ${{ steps.commit-messages.outputs.report }}
conflicts-status:
description: "Status of the merge conflicts check (success or failure)"
value: ${{ steps.conflicts.outputs.status }}
conflicts-summary:
description: "Summary message for the merge conflicts check"
value: ${{ steps.conflicts.outputs.summary }}

runs:
using: "composite"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check PR Title
id: pr-title
shell: bash
env:
TYPES: ${{ inputs.types }}
MAX_LENGTH: ${{ inputs.max-length }}
PR_TITLE: ${{ inputs.pr-title }}
run: ${{ github.action_path }}/scripts/check-pr-title.sh

- name: Check Branch Name
id: branch-name
shell: bash
env:
TYPES: ${{ inputs.types }}
BRANCH_NAME: ${{ inputs.branch-name }}
run: ${{ github.action_path }}/scripts/check-branch-name.sh

- name: Check Commit Messages
id: commit-messages
shell: bash
env:
TYPES: ${{ inputs.types }}
MAX_LENGTH: ${{ inputs.max-length }}
BASE_SHA: ${{ inputs.base-sha }}
HEAD_SHA: ${{ inputs.head-sha }}
run: ${{ github.action_path }}/scripts/check-commit-messages.sh

- name: Check Merge Conflicts
id: conflicts
shell: bash
run: ${{ github.action_path }}/scripts/check-conflicts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
# Check Branch Name Format
# Validates branch naming convention with special case for 'dev' branch

# Script reads from environment variables:
# - TYPES: Valid branch type prefixes
# - BRANCH_NAME: The branch name to validate

STATUS="success" # Default to success

# Allow 'dev' as a special case; it's always valid
Expand All @@ -14,12 +10,12 @@ if [[ "$BRANCH_NAME" = "dev" ]]; then
STATUS="success"
# Otherwise, validate against the standard format
else
FULL_REGEX="^($TYPES)\/([a-z0-9][a-z0-9-]*)$"
FULL_REGEX="^($TYPES)/([a-z0-9][a-z0-9-]*)$"

if [[ ! "$BRANCH_NAME" =~ / ]]; then
MESSAGE="❌ **Branch Name:** Missing '/' separator. Current: \`$BRANCH_NAME\`. Expected: \`type/description\`"
STATUS="failure"
elif [[ ! "$BRANCH_NAME" =~ ^($TYPES)\/ ]]; then
elif [[ ! "$BRANCH_NAME" =~ ^($TYPES)/ ]]; then
MESSAGE="❌ **Branch Name:** Invalid type prefix. Current: \`$BRANCH_NAME\`. Must start with one of: $TYPES\`"
STATUS="failure"
elif [[ ! "$BRANCH_NAME" =~ $FULL_REGEX ]]; then
Expand All @@ -39,8 +35,6 @@ fi
echo "summary=$MESSAGE"
} >>"$GITHUB_OUTPUT"

# If the check failed, exit with 0 to prevent the script from stopping the workflow.
# The orchestrator job will handle the overall failure.
if [ "$STATUS" == "failure" ]; then
exit 0
fi
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@
# Check Commit Messages Format
# Validates all commit messages in PR against conventional commit format

# Force standard locale to ensure [a-z] ranges work as expected
export LC_ALL=C

# Script reads from environment variables:
# - TYPES: Valid commit types
# - MAX_LENGTH: Maximum commit message length
# - BASE_SHA: Base commit for comparison
# - HEAD_SHA: Head commit for comparison

REGEX="^($TYPES)(\(.+\))?(!?):\s.+"
# Use [[:space:]] for POSIX compatibility
REGEX="^($TYPES)(\(.+\))?(!?):[[:space:]].+"

# Get all commits between the base and head of the PR
COMMITS=$(git log --format="%H:::%s" "$BASE_SHA".."$HEAD_SHA")
# Added --no-color to prevent ANSI codes from breaking regex
COMMITS=$(git log --no-color --format="%H:::%s" "$BASE_SHA".."$HEAD_SHA")

FAILED_COMMITS_REPORT=""
TOTAL_COMMITS=0
FAILED_COUNT=0

# Use a while loop to read each commit line by line
while IFS= read -r line; do
# Skip empty lines, which can happen with git log
# Skip empty lines
if [ -z "$line" ]; then
continue
fi
Expand All @@ -41,23 +47,29 @@ while IFS= read -r line; do
ERRORS="${ERRORS} ↳ Missing ':' separator\n"
elif [[ ! "$COMMIT_MSG" =~ ^($TYPES) ]]; then
ERRORS="${ERRORS} ↳ Invalid type prefix\n"
elif [[ "$COMMIT_MSG" =~ ^($TYPES)(\(.+\))?:[[:space:]]*$ ]]; then
elif [[ "$COMMIT_MSG" =~ ^($TYPES)(\(.+\))?(!?)?:[[:space:]]*$ ]]; then
ERRORS="${ERRORS} ↳ Missing description after ':'\n"
else
ERRORS="${ERRORS} ↳ Invalid format\n"
fi
fi

# 3. Lowercase Description Check
DESC=$(echo "$COMMIT_MSG" | sed -E "s/^($TYPES)(\(.+\))?:\s+//")
if [[ ! "$DESC" =~ ^[a-z] ]]; then
ERRORS="${ERRORS} ↳ Description must start with a lowercase letter\n"
CAPTURE_REGEX="^($TYPES)(\(.+\))?(!?)?:[[:space:]]+(.+)"

if [[ "$COMMIT_MSG" =~ $CAPTURE_REGEX ]]; then
DESC="${BASH_REMATCH[4]}"

if [[ ! "$DESC" =~ ^[a-z] ]]; then
ERRORS="${ERRORS} ↳ Description must start with a lowercase letter\n"
fi
else
:
fi

# If any errors were found, add them to the report
if [ -n "$ERRORS" ]; then
FAILED_COUNT=$((FAILED_COUNT + 1))
# Append the formatted error message for this commit
FAILED_COMMITS_REPORT="${FAILED_COMMITS_REPORT}- [\`${COMMIT_SHA}\`] \`${COMMIT_MSG}\`\n${ERRORS}"
fi
done <<<"$COMMITS"
Expand All @@ -70,15 +82,12 @@ if [ "$FAILED_COUNT" -eq 0 ]; then
else
MESSAGE="❌ **Commit Messages:** $FAILED_COUNT of $TOTAL_COMMITS commit(s) failed validation"
STATUS="failure"
# Construct a detailed report for failed commits
REPORT="Expected format: \`type(scope): description\` (max $MAX_LENGTH chars)\n"
REPORT+="Valid types: $TYPES\n\n"
REPORT+="Failed commits:\n"
REPORT+="$FAILED_COMMITS_REPORT"
fi

# Write the outputs to the GITHUB_OUTPUT file for the workflow to use
# The '<<EOF' syntax handles multi-line strings correctly
{
echo "status=$STATUS"
echo "summary=$MESSAGE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# Check PR Title Format
# Validates PR title with cascading checks: length, format, lowercase

# Force standard locale
export LC_ALL=C

# Script reads from environment variables:
# - TYPES: Valid commit type prefixes
# - MAX_LENGTH: Maximum title length
Expand All @@ -18,14 +21,15 @@ fi

# 2. Format Check (only if length check passed)
if [ "$STATUS" == "success" ]; then
REGEX="^($TYPES)(\(.+\))?:\s.+"
# Use [[:space:]] for POSIX compatibility
REGEX="^($TYPES)(\(.+\))?(!?):[[:space:]].+"
if [[ ! "$PR_TITLE" =~ $REGEX ]]; then
STATUS="failure"
if [[ ! "$PR_TITLE" =~ : ]]; then
ERROR_DETAIL="Missing ':' separator"
elif [[ ! "$PR_TITLE" =~ ^($TYPES) ]]; then
ERROR_DETAIL="Invalid or missing type prefix (must be one of: $TYPES)"
elif [[ "$PR_TITLE" =~ ^($TYPES)(\(.+\))?:[[:space:]]*$ ]]; then
elif [[ "$PR_TITLE" =~ ^($TYPES)(\(.+\))?(!?)?:[[:space:]]*$ ]]; then
ERROR_DETAIL="Missing description after ':'"
else
ERROR_DETAIL="Invalid format"
Expand All @@ -36,26 +40,34 @@ fi

# 3. Lowercase Description Check (only if previous checks passed)
if [ "$STATUS" == "success" ]; then
DESC=$(echo "$PR_TITLE" | sed -E "s/^($TYPES)(\(.+\))?:\s+//")
if [[ ! "$DESC" =~ ^[a-z] ]]; then
STATUS="failure"
MESSAGE="❌ **PR Title:** Description must start with a lowercase letter. Current: \`$PR_TITLE\`"
fi
# 使用與 Step 2 相同的 Regex,但在最後加上 (.+) 來捕獲描述
# Group 1: Types, Group 2: Scope, Group 3: Bang, Group 4: Description
CAPTURE_REGEX="^($TYPES)(\(.+\))?(!?)?:[[:space:]]+(.+)"

if [[ "$PR_TITLE" =~ $CAPTURE_REGEX ]]; then
DESC="${BASH_REMATCH[4]}"

if [[ ! "$DESC" =~ ^[a-z] ]]; then
STATUS="failure"
MESSAGE="❌ **PR Title:** Description must start with a lowercase letter. Current: \`$PR_TITLE\`"
fi
else
# 理論上 Step 2 已攔截格式錯誤,這裡只是防禦性編程
STATUS="failure"
MESSAGE="❌ **PR Title:** Could not parse description from title."
fi
fi

# Set final success message if no failure occurred
if [ "$STATUS" == "success" ]; then
MESSAGE="✅ **PR Title:** Passed (Length: $TITLE_LENGTH/$MAX_LENGTH, Format: OK). \`$PR_TITLE\`"
fi

# Write outputs for the workflow
{
echo "status=$STATUS"
echo "summary=$MESSAGE"
} >>"$GITHUB_OUTPUT"

# If the check failed, exit with 0 to prevent the script from stopping the workflow.
# The orchestrator will handle the overall failure.
if [ "$STATUS" == "failure" ]; then
exit 0
fi
65 changes: 65 additions & 0 deletions .github/actions/config-checks/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
name: "Config-Checks"
description: "Composite action to validate YAML, JSON, and TOML config files"

outputs:
yaml-status:
description: "Status of YAML validation (success/failure)"
value: ${{ steps.yaml-check.outputs.status }}
yaml-summary:
description: "Summary message of YAML validation"
value: ${{ steps.yaml-check.outputs.summary }}
json-status:
description: "Status of JSON validation (success/failure)"
value: ${{ steps.json-check.outputs.status }}
json-summary:
description: "Summary message of JSON validation"
value: ${{ steps.json-check.outputs.summary }}
toml-status:
description: "Status of TOML validation (success/failure)"
value: ${{ steps.toml-check.outputs.status }}
toml-summary:
description: "Summary message of TOML validation"
value: ${{ steps.toml-check.outputs.summary }}

runs:
using: "composite"
steps:
- name: "Install yamllint"
shell: bash
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: "Install yamllint via uv"
shell: bash
run: |
uv tool install yamllint

- name: "Install jq"
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y jq

- name: "Install taplo-cli"
shell: bash
run: |
BASE_URL="https://github.com/tamasfe/taplo/releases/latest/download"
curl -fsSL "${BASE_URL}/taplo-linux-x86_64.gz" \
| gzip -d - | install -m 755 /dev/stdin /usr/local/bin/taplo

- name: "Check YAML files"
id: yaml-check
shell: bash
run: ${{ github.action_path }}/scripts/check-yaml-files.sh

- name: "Check JSON files"
id: json-check
shell: bash
run: ${{ github.action_path }}/scripts/check-json-files.sh

- name: "Check TOML files"
id: toml-check
shell: bash
run: ${{ github.action_path }}/scripts/check-toml-files.sh
Loading