From a1a230409a300fbb673155bfcb1cf4e867724fb3 Mon Sep 17 00:00:00 2001 From: G36maid Date: Mon, 2 Feb 2026 06:48:48 +0800 Subject: [PATCH 1/7] refactor(basic): convert to composite action and fix regex Move basic checks to local actions and update scripts to use POSIX-compliant regex for bett portability. --- .github/actions/basic-checks/action.yml | 97 +++++++++++++++ .../basic-checks/scripts/check-branch-name.sh | 40 +++++++ .../scripts/check-commit-messages.sh | 97 +++++++++++++++ .../basic-checks/scripts/check-conflicts.sh | 40 +++++++ .../basic-checks/scripts/check-pr-title.sh | 73 ++++++++++++ .github/workflows/reusables-basic.yml | 111 ++++++------------ 6 files changed, 380 insertions(+), 78 deletions(-) create mode 100644 .github/actions/basic-checks/action.yml create mode 100755 .github/actions/basic-checks/scripts/check-branch-name.sh create mode 100755 .github/actions/basic-checks/scripts/check-commit-messages.sh create mode 100755 .github/actions/basic-checks/scripts/check-conflicts.sh create mode 100755 .github/actions/basic-checks/scripts/check-pr-title.sh diff --git a/.github/actions/basic-checks/action.yml b/.github/actions/basic-checks/action.yml new file mode 100644 index 0000000..7dbebe9 --- /dev/null +++ b/.github/actions/basic-checks/action.yml @@ -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 diff --git a/.github/actions/basic-checks/scripts/check-branch-name.sh b/.github/actions/basic-checks/scripts/check-branch-name.sh new file mode 100755 index 0000000..8dcdee4 --- /dev/null +++ b/.github/actions/basic-checks/scripts/check-branch-name.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Check Branch Name Format +# Validates branch naming convention with special case for 'dev' branch + +STATUS="success" # Default to success + +# Allow 'dev' as a special case; it's always valid +if [[ "$BRANCH_NAME" = "dev" ]]; then + MESSAGE="✅ **Branch Name:** Valid special branch (\`$BRANCH_NAME\`)" + STATUS="success" +# Otherwise, validate against the standard format +else + 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 + MESSAGE="❌ **Branch Name:** Invalid type prefix. Current: \`$BRANCH_NAME\`. Must start with one of: $TYPES\`" + STATUS="failure" + elif [[ ! "$BRANCH_NAME" =~ $FULL_REGEX ]]; then + MESSAGE="❌ **Branch Name:** Invalid format after '/'. Must be lowercase alphanumeric and hyphens. Current: \`$BRANCH_NAME\`" + STATUS="failure" + fi +fi + +# Set final success message if no failure occurred +if [ "$STATUS" == "success" ] && [[ "$BRANCH_NAME" != "dev" ]]; then + MESSAGE="✅ **Branch Name:** Follows naming convention (\`$BRANCH_NAME\`)" +fi + +# Write outputs for the workflow +{ + echo "status=$STATUS" + echo "summary=$MESSAGE" +} >>"$GITHUB_OUTPUT" + +if [ "$STATUS" == "failure" ]; then + exit 0 +fi diff --git a/.github/actions/basic-checks/scripts/check-commit-messages.sh b/.github/actions/basic-checks/scripts/check-commit-messages.sh new file mode 100755 index 0000000..f47826f --- /dev/null +++ b/.github/actions/basic-checks/scripts/check-commit-messages.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# 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 + +# Use [[:space:]] for POSIX compatibility +REGEX="^($TYPES)(\(.+\))?(!?):[[:space:]].+" + +# Get all commits between the base and head of the PR +# 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 + if [ -z "$line" ]; then + continue + fi + + TOTAL_COMMITS=$((TOTAL_COMMITS + 1)) + COMMIT_SHA=$(echo "$line" | cut -d':' -f1 | cut -c1-7) + # Handle cases where the commit message itself contains colons + COMMIT_MSG=$(echo "$line" | cut -d':' -f4-) + + ERRORS="" + + # 1. Length Check + if [ "${#COMMIT_MSG}" -gt "$MAX_LENGTH" ]; then + ERRORS="${ERRORS} ↳ Title is too long (is **${#COMMIT_MSG}** chars, max is **$MAX_LENGTH**)\n" + fi + + # 2. Format Check (Conventional Commit) + if [[ ! "$COMMIT_MSG" =~ $REGEX ]]; then + if [[ ! "$COMMIT_MSG" =~ : ]]; then + ERRORS="${ERRORS} ↳ Missing ':' separator\n" + elif [[ ! "$COMMIT_MSG" =~ ^($TYPES) ]]; then + ERRORS="${ERRORS} ↳ Invalid type prefix\n" + elif [[ "$COMMIT_MSG" =~ ^($TYPES)(\(.+\))?(!?)?:[[:space:]]*$ ]]; then + ERRORS="${ERRORS} ↳ Missing description after ':'\n" + else + ERRORS="${ERRORS} ↳ Invalid format\n" + fi + fi + + # 3. Lowercase Description Check + 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)) + FAILED_COMMITS_REPORT="${FAILED_COMMITS_REPORT}- [\`${COMMIT_SHA}\`] \`${COMMIT_MSG}\`\n${ERRORS}" + fi +done <<<"$COMMITS" + +# Determine the final status and construct the output summary +if [ "$FAILED_COUNT" -eq 0 ]; then + MESSAGE="✅ **Commit Messages:** All $TOTAL_COMMITS commit(s) passed (Length, Format, Case)" + STATUS="success" + REPORT="" +else + MESSAGE="❌ **Commit Messages:** $FAILED_COUNT of $TOTAL_COMMITS commit(s) failed validation" + STATUS="failure" + 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 + +{ + echo "status=$STATUS" + echo "summary=$MESSAGE" + echo "report<>"$GITHUB_OUTPUT" diff --git a/.github/actions/basic-checks/scripts/check-conflicts.sh b/.github/actions/basic-checks/scripts/check-conflicts.sh new file mode 100755 index 0000000..2040236 --- /dev/null +++ b/.github/actions/basic-checks/scripts/check-conflicts.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Check for Merge Conflict Markers +# Searches for unresolved conflict markers in the codebase + +# Search for conflict markers recursively in the current directory. +# Exclude common directories like .git and node_modules. +# The '|| true' prevents the script from exiting if grep finds no matches. +CONFLICT_FILES=$(grep -r -n -E '^(<<<<<<<|=======|>>>>>>>)' \ + --exclude-dir=.git \ + --exclude-dir=node_modules \ + --exclude-dir=dist \ + --exclude-dir=build \ + --exclude=reusables-basic.yml \ + . 2>/dev/null || true) + +# If conflict markers were found... +if [ -n "$CONFLICT_FILES" ]; then + # Get the name of the first file containing conflicts for the summary message. + FIRST_FILE=$(echo "$CONFLICT_FILES" | head -n 1 | cut -d: -f1) + # Count the total number of unique files with conflicts. + FILE_COUNT=$(echo "$CONFLICT_FILES" | cut -d: -f1 | sort -u | wc -l) + + MESSAGE="❌ **Conflicts:** Found unresolved merge markers in $FILE_COUNT file(s) (e.g., \`$FIRST_FILE\`)" + STATUS="failure" +# If no conflicts were found... +else + MESSAGE="✅ **Conflicts:** No merge conflict markers found" + STATUS="success" +fi + +# Write the outputs for the workflow. +{ + echo "status=$STATUS" + echo "summary=$MESSAGE" +} >>"$GITHUB_OUTPUT" + +# If the check failed, exit with 0. The orchestrator will handle the failure. +if [ "$STATUS" == "failure" ]; then + exit 0 +fi diff --git a/.github/actions/basic-checks/scripts/check-pr-title.sh b/.github/actions/basic-checks/scripts/check-pr-title.sh new file mode 100755 index 0000000..9f263fa --- /dev/null +++ b/.github/actions/basic-checks/scripts/check-pr-title.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# 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 +# - PR_TITLE: The PR title to validate + +TITLE_LENGTH=${#PR_TITLE} +STATUS="success" # Default to success + +# 1. Length Check +if [ "$TITLE_LENGTH" -gt "$MAX_LENGTH" ]; then + MESSAGE="❌ **PR Title:** Title is too long (is **$TITLE_LENGTH** chars, max is **$MAX_LENGTH**). Current: \`$PR_TITLE\`" + STATUS="failure" +fi + +# 2. Format Check (only if length check passed) +if [ "$STATUS" == "success" ]; then + # 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 + ERROR_DETAIL="Missing description after ':'" + else + ERROR_DETAIL="Invalid format" + fi + MESSAGE="❌ **PR Title:** $ERROR_DETAIL. Current: \`$PR_TITLE\`. Expected: \`type: description\` (e.g., \`feat: add new feature\`)" + fi +fi + +# 3. Lowercase Description Check (only if previous checks passed) +if [ "$STATUS" == "success" ]; then + # 使用與 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 + +{ + echo "status=$STATUS" + echo "summary=$MESSAGE" +} >>"$GITHUB_OUTPUT" + +if [ "$STATUS" == "failure" ]; then + exit 0 +fi diff --git a/.github/workflows/reusables-basic.yml b/.github/workflows/reusables-basic.yml index a386016..5bfb9c8 100644 --- a/.github/workflows/reusables-basic.yml +++ b/.github/workflows/reusables-basic.yml @@ -1,115 +1,70 @@ --- -name: 'Basic Code Quality Checks' +name: "Basic Code Quality Checks" on: # yamllint disable-line rule:truthy workflow_call: secrets: CHECKER_TOKEN: - description: 'The GITHUB_TOKEN passed from the caller' + description: "The GITHUB_TOKEN passed from the caller" required: true outputs: pr-title-status: description: "The status of the PR title check." - value: ${{ jobs.check-pr-title.outputs.status }} + value: ${{ jobs.basic-checks.outputs.pr-title-status }} pr-title-summary: description: "The summary of the PR title check." - value: ${{ jobs.check-pr-title.outputs.summary }} + value: ${{ jobs.basic-checks.outputs.pr-title-summary }} branch-name-status: description: "The status of the branch name check." - value: ${{ jobs.check-branch-name.outputs.status }} + value: ${{ jobs.basic-checks.outputs.branch-name-status }} branch-name-summary: description: "The summary of the branch name check." - value: ${{ jobs.check-branch-name.outputs.summary }} + value: ${{ jobs.basic-checks.outputs.branch-name-summary }} commit-messages-status: description: "The status of the commit messages check." - value: ${{ jobs.check-commits.outputs.status }} + value: ${{ jobs.basic-checks.outputs.commit-messages-status }} commit-messages-summary: description: "The summary of the commit messages check." - value: ${{ jobs.check-commits.outputs.summary }} + value: ${{ jobs.basic-checks.outputs.commit-messages-summary }} commit-messages-report: description: "A detailed report for failed commit messages." - value: ${{ jobs.check-commits.outputs.report }} + value: ${{ jobs.basic-checks.outputs.commit-messages-report }} conflicts-status: description: "The status of the merge conflicts check." - value: ${{ jobs.check-conflicts.outputs.status }} + value: ${{ jobs.basic-checks.outputs.conflicts-status }} conflicts-summary: description: "The summary of the merge conflicts check." - value: ${{ jobs.check-conflicts.outputs.summary }} + value: ${{ jobs.basic-checks.outputs.conflicts-summary }} permissions: contents: read pull-requests: write issues: write -env: - TYPES: "feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|hotfix" - MAX_LENGTH: 75 - -jobs: - check-commits: - name: 'Check Commit Messages' - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} - report: ${{ steps.check.outputs.report }} - steps: - - name: Checkout Caller Code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.head_ref }} - - - name: Check Commit Messages Format - id: check - env: - TYPES: ${{ env.TYPES }} - MAX_LENGTH: ${{ env.MAX_LENGTH }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: .github/scripts/check-commit-messages.sh - - check-conflicts: - name: 'Check Merge Conflicts' +jobs: # yamllint disable-line rule:line-length + basic-checks: + name: "Basic Code Quality Checks" runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} + outputs: # yamllint disable rule:line-length + pr-title-status: ${{ steps.basic-checks.outputs.pr-title-status }} + pr-title-summary: ${{ steps.basic-checks.outputs.pr-title-summary }} + branch-name-status: ${{ steps.basic-checks.outputs.branch-name-status }} + branch-name-summary: ${{ steps.basic-checks.outputs.branch-name-summary }} + commit-messages-status: ${{ steps.basic-checks.outputs.commit-messages-status }} + commit-messages-summary: ${{ steps.basic-checks.outputs.commit-messages-summary }} + commit-messages-report: ${{ steps.basic-checks.outputs.commit-messages-report }} + conflicts-status: ${{ steps.basic-checks.outputs.conflicts-status }} + conflicts-summary: ${{ steps.basic-checks.outputs.conflicts-summary }} + # yamllint enable rule:line-length steps: - name: Checkout Code uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - name: Check for conflict markers - id: check - run: .github/scripts/check-conflicts.sh - - check-pr-title: - name: 'Check PR Title' - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} - steps: - - name: Check PR Title Format - id: check - env: - TYPES: ${{ env.TYPES }} - MAX_LENGTH: ${{ env.MAX_LENGTH }} - PR_TITLE: ${{ github.event.pull_request.title }} - run: .github/scripts/check-pr-title.sh - - check-branch-name: - name: 'Check Branch Name' - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} - steps: - - name: Check Branch Name Format - id: check - env: - TYPES: ${{ env.TYPES }} - BRANCH_NAME: ${{ github.head_ref }} - run: .github/scripts/check-branch-name.sh + - name: Run Basic Code Quality Checks + id: basic-checks + uses: ./.github/actions/basic-checks + with: + pr-title: ${{ github.event.pull_request.title }} + branch-name: ${{ github.head_ref }} + base-sha: ${{ github.event.pull_request.base.sha }} + head-sha: ${{ github.event.pull_request.head.sha }} From 253c7465f5d7c83f67c283ee3dd551df8e9b150b Mon Sep 17 00:00:00 2001 From: G36maid Date: Mon, 2 Feb 2026 06:49:49 +0800 Subject: [PATCH 2/7] refactor(config): convert to composite action Migrate JSON, TOML, and YAML validation logic to dedicated composite actions. --- .github/actions/config-checks/action.yml | 65 ++++++++++++++ .../config-checks/scripts/check-json-files.sh | 59 +++++++++++++ .../config-checks/scripts/check-toml-files.sh | 63 ++++++++++++++ .../config-checks/scripts/check-yaml-files.sh | 57 ++++++++++++ .github/workflows/reusables-config.yml | 87 ++++--------------- 5 files changed, 261 insertions(+), 70 deletions(-) create mode 100644 .github/actions/config-checks/action.yml create mode 100755 .github/actions/config-checks/scripts/check-json-files.sh create mode 100755 .github/actions/config-checks/scripts/check-toml-files.sh create mode 100755 .github/actions/config-checks/scripts/check-yaml-files.sh diff --git a/.github/actions/config-checks/action.yml b/.github/actions/config-checks/action.yml new file mode 100644 index 0000000..5f0b8ac --- /dev/null +++ b/.github/actions/config-checks/action.yml @@ -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 diff --git a/.github/actions/config-checks/scripts/check-json-files.sh b/.github/actions/config-checks/scripts/check-json-files.sh new file mode 100755 index 0000000..e9a449c --- /dev/null +++ b/.github/actions/config-checks/scripts/check-json-files.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Check JSON Files +# Validates JSON syntax using jq and generates GitHub annotations + +# Find all JSON files excluding node_modules and .git +JSON_FILES=$(find . -type f -name "*.json" \ + ! -path "*/node_modules/*" \ + ! -path "*/.git/*" \ + ! -path "*/venv/*" \ + ! -path "*/.venv/*" \ + ! -path "*/.next/*" \ + ! -path "*/dist/*" \ + ! -path "*/build/*" \ + 2>/dev/null || true) + +if [ -z "$JSON_FILES" ]; then + MESSAGE="✅ **JSON Files:** No JSON files found to check" + STATUS="success" +else + FILE_COUNT=$(echo "$JSON_FILES" | wc -l) + FAILED_FILES="" + FAILED_COUNT=0 + + # Check each JSON file + while IFS= read -r file; do + if ! jq empty "$file" >/dev/null 2>&1; then + FAILED_COUNT=$((FAILED_COUNT + 1)) + if [ -z "$FAILED_FILES" ]; then + FAILED_FILES="$file" + fi + # Get error details + ERROR_OUTPUT=$(jq empty "$file" 2>&1 || true) + + # Extract line and column if possible + # Example: parse error: ... at line 10, column 20 + if echo "$ERROR_OUTPUT" | grep -q "at line [0-9]\+, column [0-9]\+"; then + line=$(echo "$ERROR_OUTPUT" | grep -o "at line [0-9]\+" | grep -o "[0-9]\+") + col=$(echo "$ERROR_OUTPUT" | grep -o "column [0-9]\+" | grep -o "[0-9]\+") + msg=$(echo "$ERROR_OUTPUT" | sed -E 's/.*: (.*) at line.*/\1/') + echo "::error file=${file},line=${line},col=${col}::${msg}" + else + echo "::error file=${file}::JSON validation failed: ${ERROR_OUTPUT}" + fi + fi + done <<<"$JSON_FILES" + + if [ $FAILED_COUNT -eq 0 ]; then + MESSAGE="✅ **JSON Files:** All $FILE_COUNT file(s) are valid" + STATUS="success" + else + MESSAGE="❌ **JSON Files:** $FAILED_COUNT of $FILE_COUNT file(s) failed validation (e.g., \`$FAILED_FILES\`)" + STATUS="failure" + fi +fi + +{ + echo "status=$STATUS" + echo "summary=$MESSAGE" +} >>"$GITHUB_OUTPUT" diff --git a/.github/actions/config-checks/scripts/check-toml-files.sh b/.github/actions/config-checks/scripts/check-toml-files.sh new file mode 100755 index 0000000..c7510ad --- /dev/null +++ b/.github/actions/config-checks/scripts/check-toml-files.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Check TOML Files +# Validates TOML syntax using taplo and generates GitHub annotations + +# Find all TOML files excluding node_modules and .git +TOML_FILES=$(find . -type f -name "*.toml" \ + ! -path "*/node_modules/*" \ + ! -path "*/.git/*" \ + ! -path "*/venv/*" \ + ! -path "*/.venv/*" \ + 2>/dev/null || true) + +if [ -z "$TOML_FILES" ]; then + MESSAGE="✅ **TOML Files:** No TOML files found to check" + STATUS="success" +else + FILE_COUNT=$(echo "$TOML_FILES" | wc -l) + + # Run taplo check + set +e + TAPLO_OUTPUT=$(echo "$TOML_FILES" | xargs taplo check 2>&1) + TAPLO_EXIT=$? + set -e + + if [ $TAPLO_EXIT -eq 0 ]; then + MESSAGE="✅ **TOML Files:** All $FILE_COUNT file(s) are valid" + STATUS="success" + else + # Extract failed files from taplo output + FAILED_FILES=$(echo "$TAPLO_OUTPUT" | grep -oP '(?<=")[^"]+\.toml(?=")' | head -n 1 || echo "unknown") + FAILED_COUNT=$(echo "$TAPLO_OUTPUT" | grep -c "error" || echo "1") + + MESSAGE="❌ **TOML Files:** Found syntax errors in TOML files (e.g., \`$FAILED_FILES\`)" + STATUS="failure" + + # Show errors as GitHub annotations + # Taplo output is visual, e.g.: + # ┌─ file.toml:1:15 + echo "$TAPLO_OUTPUT" | while IFS= read -r line; do + # Check for location line: ┌─ file:line:col + if echo "$line" | grep -q "┌─ "; then + # Extract info using sed + # Remove leading whitespace and ┌─ + clean_line=$(echo "$line" | sed 's/^[[:space:]]*┌─ //') + file=$(echo "$clean_line" | cut -d':' -f1) + line_num=$(echo "$clean_line" | cut -d':' -f2) + col_num=$(echo "$clean_line" | cut -d':' -f3) + + if [ -n "$file" ] && [ -n "$line_num" ]; then + echo "::error file=${file},line=${line_num},col=${col_num}::TOML syntax error" + fi + elif echo "$line" | grep -q "error:"; then + # Fallback for general errors + echo "::error::TOML: ${line}" + fi + done + fi +fi + +{ + echo "status=$STATUS" + echo "summary=$MESSAGE" +} >>"$GITHUB_OUTPUT" diff --git a/.github/actions/config-checks/scripts/check-yaml-files.sh b/.github/actions/config-checks/scripts/check-yaml-files.sh new file mode 100755 index 0000000..be0eeac --- /dev/null +++ b/.github/actions/config-checks/scripts/check-yaml-files.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Check YAML Files +# Runs yamllint on all YAML files and generates GitHub annotations + +# Find all YAML files (*.yml, *.yaml) excluding node_modules and .git +YAML_FILES=$(find . -type f \( -name "*.yml" -o -name "*.yaml" \) \ + ! -path "*/node_modules/*" \ + ! -path "*/.git/*" \ + ! -path "*/venv/*" \ + ! -path "*/.venv/*" \ + 2>/dev/null || true) + +if [ -z "$YAML_FILES" ]; then + MESSAGE="✅ **YAML Files:** No YAML files found to check" + STATUS="success" +else + FILE_COUNT=$(echo "$YAML_FILES" | wc -l) + + # Run yamllint with relaxed configuration + set +e + YAMLLINT_OUTPUT=$(echo "$YAML_FILES" | xargs uv run yamllint -f parsable 2>&1) + YAMLLINT_EXIT=$? + set -e + + if [ $YAMLLINT_EXIT -eq 0 ]; then + MESSAGE="✅ **YAML Files:** All $FILE_COUNT file(s) passed validation" + STATUS="success" + else + # Count failed files + FAILED_COUNT=$(echo "$YAMLLINT_OUTPUT" | cut -d':' -f1 | sort -u | wc -l) + # Get first failed file for summary + FIRST_FAILED=$(echo "$YAMLLINT_OUTPUT" | head -n 1 | cut -d':' -f1) + + MESSAGE="❌ **YAML Files:** Found issues in $FAILED_COUNT of $FILE_COUNT file(s) (e.g., \`$FIRST_FAILED\`)" + STATUS="failure" + + # Show errors in GitHub annotations + echo "$YAMLLINT_OUTPUT" | while IFS=: read -r file line col level msg; do + if [ -n "$file" ] && [ -n "$line" ]; then + # Clean up level (remove brackets) + clean_level=$(echo "$level" | tr -d '[]' | xargs) + # Map yamllint level to GitHub annotation level + if [ "$clean_level" = "warning" ]; then + gh_level="warning" + else + gh_level="error" + fi + echo "::${gh_level} file=${file},line=${line},col=${col}::${msg}" + fi + done + fi +fi + +{ + echo "status=$STATUS" + echo "summary=$MESSAGE" +} >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/reusables-config.yml b/.github/workflows/reusables-config.yml index d88c49a..7e0d0fa 100644 --- a/.github/workflows/reusables-config.yml +++ b/.github/workflows/reusables-config.yml @@ -10,22 +10,22 @@ on: # yamllint disable-line rule:truthy outputs: yaml-status: description: "The status of the YAML validation." - value: ${{ jobs.check-yaml.outputs.status }} + value: ${{ jobs.config-checks.outputs.yaml-status }} yaml-summary: description: "The summary of the YAML validation." - value: ${{ jobs.check-yaml.outputs.summary }} + value: ${{ jobs.config-checks.outputs.yaml-summary }} json-status: description: "The status of the JSON validation." - value: ${{ jobs.check-json.outputs.status }} + value: ${{ jobs.config-checks.outputs.json-status }} json-summary: description: "The summary of the JSON validation." - value: ${{ jobs.check-json.outputs.summary }} + value: ${{ jobs.config-checks.outputs.json-summary }} toml-status: description: "The status of the TOML validation." - value: ${{ jobs.check-toml.outputs.status }} + value: ${{ jobs.config-checks.outputs.toml-status }} toml-summary: description: "The summary of the TOML validation." - value: ${{ jobs.check-toml.outputs.summary }} + value: ${{ jobs.config-checks.outputs.toml-summary }} permissions: contents: read @@ -33,73 +33,20 @@ permissions: issues: write jobs: - check-yaml: - name: 'Check YAML Files' + config-checks: + name: 'Configuration Files Quality Checks' runs-on: ubuntu-latest outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} + yaml-status: ${{ steps.config-checks.outputs.yaml-status }} + yaml-summary: ${{ steps.config-checks.outputs.yaml-summary }} + json-status: ${{ steps.config-checks.outputs.json-status }} + json-summary: ${{ steps.config-checks.outputs.json-summary }} + toml-status: ${{ steps.config-checks.outputs.toml-status }} + toml-summary: ${{ steps.config-checks.outputs.toml-summary }} steps: - name: Checkout Code uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - name: Install uv - id: setup_uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Add uv to PATH - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Install yamllint - run: | - uv tool install yamllint - - - name: Find and Check YAML Files - id: check - run: .github/scripts/check-yaml-files.sh - - check-json: - name: 'Check JSON Files' - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} - steps: - - name: Checkout Code - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Install jq - run: | - sudo apt-get update -qq - sudo apt-get install -y jq - - - name: Find and Check JSON Files - id: check - run: .github/scripts/check-json-files.sh - - check-toml: - name: 'Check TOML Files' - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} - steps: - - name: Checkout Code - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Install taplo-cli - run: | - # Install taplo - BASE_URL="https://github.com/tamasfe/taplo/releases/latest/download" - curl -fsSL "${BASE_URL}/taplo-full-linux-x86_64.gz" \ - | gzip -d - | install -m 755 /dev/stdin /usr/local/bin/taplo - - - name: Find and Check TOML Files - id: check - run: .github/scripts/check-toml-files.sh + - name: Run Config Checks + id: config-checks + uses: ./.github/actions/config-checks From ffce47b9194e4ab9efd7f1ee619af8445bc5b19c Mon Sep 17 00:00:00 2001 From: G36maid Date: Mon, 2 Feb 2026 06:50:59 +0800 Subject: [PATCH 3/7] refactor(frontend): convert to composite action with templates Refactor frontend checks to separate logic from config using templates and local actions. --- .github/actions/frontend-checks/action.yml | 78 +++++++++++++++++ .../frontend-checks/scripts/check-prettier.sh | 23 +++++ .../scripts/parse-eslint-json.sh | 39 +++++++++ .../frontend-checks/scripts/setup-eslint.sh | 11 +++ .../frontend-checks/scripts/setup-prettier.sh | 10 +++ .../templates/.prettierrc.json | 9 ++ .../templates/eslint.config.mjs | 36 ++++++++ .github/workflows/reusables-frontend.yml | 87 ++++--------------- 8 files changed, 221 insertions(+), 72 deletions(-) create mode 100644 .github/actions/frontend-checks/action.yml create mode 100755 .github/actions/frontend-checks/scripts/check-prettier.sh create mode 100755 .github/actions/frontend-checks/scripts/parse-eslint-json.sh create mode 100755 .github/actions/frontend-checks/scripts/setup-eslint.sh create mode 100755 .github/actions/frontend-checks/scripts/setup-prettier.sh create mode 100644 .github/actions/frontend-checks/templates/.prettierrc.json create mode 100644 .github/actions/frontend-checks/templates/eslint.config.mjs diff --git a/.github/actions/frontend-checks/action.yml b/.github/actions/frontend-checks/action.yml new file mode 100644 index 0000000..10b6a6e --- /dev/null +++ b/.github/actions/frontend-checks/action.yml @@ -0,0 +1,78 @@ +--- +name: Frontend Code Quality Checks +description: > + Runs Prettier and ESLint checks for frontend code + quality + +inputs: + node-version: + description: Node.js version to use + required: false + default: "20" + +outputs: + prettier-status: + description: Status of Prettier check (success/failure) + value: ${{ steps.prettier.outputs.status }} + prettier-summary: + description: Summary of Prettier check results + value: ${{ steps.prettier.outputs.summary }} + eslint-status: + description: Status of ESLint check (success/failure) + value: ${{ steps.eslint.outputs.status }} + eslint-summary: + description: Summary of ESLint check results + value: ${{ steps.eslint.outputs.summary }} + +runs: + using: composite + steps: + - name: Set up default Prettier config + shell: bash + env: + TEMPLATE_PATH: ${{ github.action_path }}/templates/.prettierrc.json + run: ${{ github.action_path }}/scripts/setup-prettier.sh + + - name: Set up default ESLint config + shell: bash + env: + TEMPLATE_PATH: ${{ github.action_path }}/templates/eslint.config.mjs + run: ${{ github.action_path }}/scripts/setup-eslint.sh + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Install Dev Dependencies + shell: bash + run: | + if [ -f "package.json" ]; then + npm install --only=dev + else + echo "No package.json found, skipping npm install." + fi + + - name: Ensure Prettier is installed + shell: bash + run: | + + npm list prettier > /dev/null 2>&1 || npm install --no-save prettier + + - name: Ensure ESLint is installed + shell: bash + run: | + npm list eslint > /dev/null 2>&1 || \ + npm install --no-save eslint @eslint/js + npm list globals > /dev/null 2>&1 || \ + npm install --no-save globals + + - name: Check Prettier Formatting + id: prettier + shell: bash + run: ${{ github.action_path }}/scripts/check-prettier.sh + + - name: Check ESLint + id: eslint + shell: bash + run: ${{ github.action_path }}/scripts/parse-eslint-json.sh diff --git a/.github/actions/frontend-checks/scripts/check-prettier.sh b/.github/actions/frontend-checks/scripts/check-prettier.sh new file mode 100755 index 0000000..e464c8e --- /dev/null +++ b/.github/actions/frontend-checks/scripts/check-prettier.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Check Prettier Formatting +# Runs prettier --check and generates annotations for files needing formatting + +echo "Running prettier format check..." +set +e # Don't exit on prettier failure +npx prettier --check ./src 2>&1 | tee prettier_output.txt +PRETTIER_EXIT_CODE=$? +set -e + +if [ $PRETTIER_EXIT_CODE -ne 0 ]; then + # Extract filenames from prettier output and generate annotations + grep -E "^\S+\.(js|jsx|ts|tsx|json|css|scss|md|yaml|yml)$" prettier_output.txt | while read -r file; do + if [ -f "$file" ]; then + echo "::error title=Prettier Formatting,file=${file},line=1,col=1::File needs formatting. Run 'npm run format' to fix." + fi + done + echo "status=failure" >>"$GITHUB_OUTPUT" + echo "summary=❌ **Prettier:** Formatting issues found. Please review the annotations above." >>"$GITHUB_OUTPUT" +else + echo "status=success" >>"$GITHUB_OUTPUT" + echo "summary=✅ **Prettier:** All files are properly formatted." >>"$GITHUB_OUTPUT" +fi diff --git a/.github/actions/frontend-checks/scripts/parse-eslint-json.sh b/.github/actions/frontend-checks/scripts/parse-eslint-json.sh new file mode 100755 index 0000000..96380f8 --- /dev/null +++ b/.github/actions/frontend-checks/scripts/parse-eslint-json.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Parse ESLint JSON Output +# Converts ESLint JSON to GitHub annotations + +echo "Running ESLint with GitHub annotations..." +set +e +npx eslint ./src --format json --output-file eslint_output.json +ESLINT_EXIT_CODE=$? + +if [ -f eslint_output.json ]; then + node -e " +const fs = require('fs'); +const results = JSON.parse(fs.readFileSync('eslint_output.json', 'utf8')); + +let hasErrors = false; +results.forEach(result => { + result.messages.forEach(message => { + const file = result.filePath.replace(process.cwd() + '/', ''); + const line = message.line || 1; + const column = message.column || 1; + const severity = message.severity === 2 ? 'error' : 'warning'; + const ruleId = message.ruleId ? \` (\${message.ruleId})\` : ''; + const messageText = message.message; + + if (message.severity === 2) hasErrors = true; + + console.log(\`::\${severity} title=ESLint\${ruleId},file=\${file},line=\${line},col=\${column}::\${file}:\${line}:\${column}: \${messageText}\`); + }); +}); +" +fi + +if [ $ESLINT_EXIT_CODE -ne 0 ]; then + echo "status=failure" >>"$GITHUB_OUTPUT" + echo "summary=❌ **ESLint:** Issues found. Please review the annotations above." >>"$GITHUB_OUTPUT" +else + echo "status=success" >>"$GITHUB_OUTPUT" + echo "summary=✅ **ESLint:** No issues found." >>"$GITHUB_OUTPUT" +fi diff --git a/.github/actions/frontend-checks/scripts/setup-eslint.sh b/.github/actions/frontend-checks/scripts/setup-eslint.sh new file mode 100755 index 0000000..d6a02bf --- /dev/null +++ b/.github/actions/frontend-checks/scripts/setup-eslint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +if [ ! -f ".eslintrc" ] && [ ! -f ".eslintrc.json" ] && \ + [ ! -f ".eslintrc.js" ] && [ ! -f "eslint.config.js" ] && \ + [ ! -f "eslint.config.mjs" ]; then + + echo "Creating default eslint.config.mjs..." + cp "$ESLINT_TEMPLATE_PATH" eslint.config.mjs +else + echo "ESLint config already exists, skipping creation." +fi diff --git a/.github/actions/frontend-checks/scripts/setup-prettier.sh b/.github/actions/frontend-checks/scripts/setup-prettier.sh new file mode 100755 index 0000000..f8b4f41 --- /dev/null +++ b/.github/actions/frontend-checks/scripts/setup-prettier.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +if [ ! -f ".prettierrc" ] && [ ! -f ".prettierrc.json" ] && \ + [ ! -f "prettier.config.js" ]; then + + echo "Creating default .prettierrc.json..." + cp "$TEMPLATE_PATH" .prettierrc.json +else + echo "Prettier config already exists, skipping creation." +fi diff --git a/.github/actions/frontend-checks/templates/.prettierrc.json b/.github/actions/frontend-checks/templates/.prettierrc.json new file mode 100644 index 0000000..4e499f5 --- /dev/null +++ b/.github/actions/frontend-checks/templates/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "printWidth": 80 +} diff --git a/.github/actions/frontend-checks/templates/eslint.config.mjs b/.github/actions/frontend-checks/templates/eslint.config.mjs new file mode 100644 index 0000000..0631c9a --- /dev/null +++ b/.github/actions/frontend-checks/templates/eslint.config.mjs @@ -0,0 +1,36 @@ +import js from "@eslint/js"; +import globals from "globals"; + +export default [ + js.configs.recommended, + { + files: ["**/*.{js,mjs,cjs,jsx}"], + + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2021 + }, + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }, + rules: { + "no-console": "warn", + "no-unused-vars": "warn", + "no-var": "error" + }, + }, + { + ignores: [ + "dist/**", + "node_modules/**", + "build/**" + ] + } +]; diff --git a/.github/workflows/reusables-frontend.yml b/.github/workflows/reusables-frontend.yml index de80fdc..eb3edeb 100644 --- a/.github/workflows/reusables-frontend.yml +++ b/.github/workflows/reusables-frontend.yml @@ -16,16 +16,16 @@ on: # yamllint disable-line rule:truthy outputs: prettier-status: description: "The status of the Prettier formatting check." - value: ${{ jobs.check-prettier.outputs.status }} + value: ${{ jobs.frontend-checks.outputs.prettier-status }} prettier-summary: description: "A summary of the Prettier formatting check." - value: ${{ jobs.check-prettier.outputs.summary }} + value: ${{ jobs.frontend-checks.outputs.prettier-summary }} eslint-status: description: "The status of the ESLint check." - value: ${{ jobs.check-eslint.outputs.status }} + value: ${{ jobs.frontend-checks.outputs.eslint-status }} eslint-summary: description: "A summary of the ESLint check." - value: ${{ jobs.check-eslint.outputs.summary }} + value: ${{ jobs.frontend-checks.outputs.eslint-summary }} permissions: contents: read @@ -83,76 +83,19 @@ env: jobs: - check-prettier: - name: 'Check Prettier Formatting' + frontend-checks: + name: 'Frontend Code Quality Checks' runs-on: ubuntu-latest + # yamllint disable rule:line-length outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} + prettier-status: ${{ steps.frontend-checks.outputs.prettier-status }} + prettier-summary: ${{ steps.frontend-checks.outputs.prettier-summary }} + eslint-status: ${{ steps.frontend-checks.outputs.eslint-status }} + eslint-summary: ${{ steps.frontend-checks.outputs.eslint-summary }} + # yamllint enable steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Set up default Prettier config - run: | - if [ ! -f ".prettierrc" ] && [ ! -f ".prettierrc.json" ] && \ - [ ! -f "prettier.config.js" ]; then - echo '${{ env.DEFAULT_PRETTIER_CONFIG }}' > .prettierrc.json - fi - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ inputs.node-version }} - - - name: Install Dev Dependencies - run: | - npm install --only=dev - - - name: Ensure Prettier is installed - run: | - npm list prettier > /dev/null 2>&1 || npm install --save-dev prettier - - - name: Check Prettier Formatter - id: check - run: .github/scripts/check-prettier.sh - - check-eslint: - name: 'Check ESLint' - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.status }} - summary: ${{ steps.check.outputs.summary }} - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Set up default ESLint config - run: | - if [ ! -f ".eslintrc" ] && [ ! -f ".eslintrc.json" ] && \ - [ ! -f ".eslintrc.js" ] && [ ! -f "eslint.config.js" ] && \ - [ ! -f "eslint.config.mjs" ]; then - echo '${{ env.DEFAULT_ESLINT_CONFIG }}' > eslint.config.mjs - fi - - - name: Set up Node.js - uses: actions/setup-node@v4 + - name: Run Frontend Checks + id: frontend-checks + uses: ./.github/actions/frontend-checks with: node-version: ${{ inputs.node-version }} - - - name: Install Dev Dependencies - run: | - npm install --only=dev - - - name: Ensure ESLint is installed - run: | - npm list eslint > /dev/null 2>&1 || npm install --save-dev eslint - npm list globals > /dev/null 2>&1 || npm install --save-dev globals - - - name: Check ESLint - id: check - run: .github/scripts/parse-eslint-json.sh From adb876bb4edd821029103af880ded08bb34ca0e2 Mon Sep 17 00:00:00 2001 From: G36maid Date: Mon, 2 Feb 2026 06:51:37 +0800 Subject: [PATCH 4/7] refactor(lang): convert Python and Go checks to composite actions Modularize Go and Python workflows into reusable composite actions. --- .github/actions/go-checks/action.yml | 137 ++++++++++++++++++ .github/actions/python-checks/action.yml | 127 ++++++++++++++++ .../scripts/parse-mypy-output.sh | 35 +++++ .../python-checks/scripts/setup-pyproject.sh | 33 +++++ .github/scripts/go-check-outcome.sh | 25 ---- .github/workflows/reusables-go.yml | 117 +++------------ .github/workflows/reusables-python.yml | 128 ++-------------- 7 files changed, 363 insertions(+), 239 deletions(-) create mode 100644 .github/actions/go-checks/action.yml create mode 100644 .github/actions/python-checks/action.yml create mode 100755 .github/actions/python-checks/scripts/parse-mypy-output.sh create mode 100755 .github/actions/python-checks/scripts/setup-pyproject.sh delete mode 100755 .github/scripts/go-check-outcome.sh diff --git a/.github/actions/go-checks/action.yml b/.github/actions/go-checks/action.yml new file mode 100644 index 0000000..c9762b6 --- /dev/null +++ b/.github/actions/go-checks/action.yml @@ -0,0 +1,137 @@ +--- +name: 'Go Code Quality Checks' +description: 'Runs Go code quality checks (formatting, linting, testing).' +inputs: + go-version: + description: 'The Go version to use.' + required: false + default: 'stable' + working-directory: + description: 'Directory where Go code is located.' + required: false + default: '.' + token: + description: 'GITHUB_TOKEN for API operations (reporting).' + required: true +outputs: + go-status: + description: "The status of the Go checks." + value: ${{ steps.outcome.outputs.status }} + go-summary: + description: "The summary of the Go checks." + value: ${{ steps.outcome.outputs.summary }} + +runs: + using: "composite" + steps: + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + cache: false + + - name: Set up default golangci-lint config + shell: bash + env: + DEFAULT_GOLANGCI_CONFIG: | + linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + run: + timeout: 5m + run: | + WORKDIR="${{ inputs.working-directory }}" + CONFIG_FILES="golangci.yml golangci.yaml golangci.toml" + NO_CONFIG=true + for f in $CONFIG_FILES; do + if [ -f "$WORKDIR/.$f" ]; then + NO_CONFIG=false + break + fi + done + if $NO_CONFIG; then + echo "$DEFAULT_GOLANGCI_CONFIG" > "$WORKDIR/.golangci.yml" + fi + + - name: Check go mod tidy + id: tidy + shell: bash + working-directory: ${{ inputs.working-directory }} + continue-on-error: true + run: | + go mod tidy + if [ -n "$(git status --porcelain)" ]; then + echo "::error::go.mod/sum dirty, run 'go mod tidy'" + exit 1 + fi + + - name: Install gotestsum + shell: bash + run: go install gotest.tools/gotestsum@latest + + - name: Run golangci-lint + id: lint + uses: golangci/golangci-lint-action@v6 + continue-on-error: true + with: + version: latest + working-directory: ${{ inputs.working-directory }} + args: --timeout=5m + + - name: Run Tests (Race Detector) + id: test + shell: bash + working-directory: ${{ inputs.working-directory }} + continue-on-error: true + run: | + GOTESTSUM_BIN="$(go env GOPATH)/bin/gotestsum" + $GOTESTSUM_BIN --junitfile test-results.xml -- -race ./... + + - name: Publish Test Results + uses: mikepenz/action-junit-report@v4 + if: always() + with: + report_paths: "**/test-results.xml" + check_name: "Go Test Results" + token: ${{ inputs.token }} + + - name: Build Check (Dry Run) + id: build + shell: bash + working-directory: ${{ inputs.working-directory }} + continue-on-error: true + run: go build -v ./... + + - name: Set Go Checks Outcome + id: outcome + if: always() + shell: bash + env: + TIDY: ${{ steps.tidy.outcome }} + LINT: ${{ steps.lint.outcome }} + TEST: ${{ steps.test.outcome }} + BUILD: ${{ steps.build.outcome }} + run: | + ALL_PASSED=true + if [[ "$TIDY" != "success" || \ + "$LINT" != "success" || \ + "$TEST" != "success" || \ + "$BUILD" != "success" ]]; then + ALL_PASSED=false + fi + + if $ALL_PASSED; then + echo "status=success" >> "$GITHUB_OUTPUT" + echo "summary=✅ **Go Quality:** All checks passed." \ + >> "$GITHUB_OUTPUT" + else + echo "status=failure" >> "$GITHUB_OUTPUT" + DETAILS="tidy=$TIDY, lint=$LINT, test=$TEST, build=$BUILD" + echo "summary=❌ **Go Quality:** Failed ($DETAILS)." \ + >> "$GITHUB_OUTPUT" + fi diff --git a/.github/actions/python-checks/action.yml b/.github/actions/python-checks/action.yml new file mode 100644 index 0000000..84a6450 --- /dev/null +++ b/.github/actions/python-checks/action.yml @@ -0,0 +1,127 @@ +--- +name: "Python Code Quality Checks" +description: "Run Python code quality checks (ruff format, ruff lint, mypy)" + +inputs: + python-version: + description: "The Python version to use" + required: false + default: "3.11" + +outputs: + python-status: + description: "The overall status of the Python checks (success or failure)" + value: ${{ steps.outcome.outputs.status }} + python-summary: + description: "A summary of the Python code quality checks" + value: ${{ steps.outcome.outputs.summary }} + +runs: + using: "composite" + steps: + - name: Create default pyproject.toml if missing + shell: bash + env: + DEFAULT_PROJECT_CONFIG: | + [build-system] + requires = ["setuptools>=61.0", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "project" + version = "0.1.0" + description = "Project" + requires-python = ">=3.11" + dependencies = [] + + DEFAULT_RUFF_CONFIG: | + [tool.ruff.format] + quote-style = "double" + indent-style = "space" + + [tool.ruff] + line-length = 88 + + [tool.ruff.lint] + select = ["E", "F", "N", "I"] + + DEFAULT_MYPY_CONFIG: | + [tool.mypy] + strict = true + namespace_packages = true + warn_unused_ignores = false + ignore_missing_imports = true + + run: ${{ github.action_path }}/scripts/setup-pyproject.sh + + - name: Set up Python + id: setup_python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Install Dependencies and Linters + shell: bash + run: | + uv sync + + - name: Ensure Ruff and Mypy are installed + shell: bash + run: | + uv pip install ruff mypy + + - name: Check Ruff Formatter + id: ruff_format + continue-on-error: true + shell: bash + run: | + echo "Running ruff format check..." + uv run ruff format . --check --output-format=github + + - name: Check Ruff Linter (with Annotations) + id: ruff_lint + continue-on-error: true + shell: bash + run: | + echo "Running ruff check with GitHub annotations..." + uv run ruff check . --output-format=github + + - name: Check Mypy (with Annotations) + id: mypy_check + continue-on-error: true + shell: bash + run: | + echo "Running mypy with GitHub annotations..." + set +e # Don't exit on mypy failure + uv run mypy . > mypy_output.txt 2>&1 + MYPY_EXIT_CODE=$? + + if [ -f mypy_output.txt ]; then + sed -i -E \ + 's/^([^:]+:[0-9]+):[[:space:]]+(error|warning|note):/\1:: \2:/' \ + mypy_output.txt + fi + # Parse mypy output and generate GitHub annotations + ${{ github.action_path }}/scripts/parse-mypy-output.sh + # Exit with mypy's original exit code + exit $MYPY_EXIT_CODE + + - name: Set Python Checks Outcome + id: outcome + if: always() + shell: bash + run: | + if [[ "${{ steps.ruff_format.outcome }}" == "failure" || \ + "${{ steps.ruff_lint.outcome }}" == "failure" || \ + "${{ steps.mypy_check.outcome }}" == "failure" ]]; then + echo "status=failure" >> $GITHUB_OUTPUT + echo "summary=❌ **Python Quality:** Checks failed." \ + >> $GITHUB_OUTPUT + else + echo "status=success" >> $GITHUB_OUTPUT + echo "summary=✅ **Python Quality:** All checks passed." \ + >> $GITHUB_OUTPUT + fi diff --git a/.github/actions/python-checks/scripts/parse-mypy-output.sh b/.github/actions/python-checks/scripts/parse-mypy-output.sh new file mode 100755 index 0000000..cca2d85 --- /dev/null +++ b/.github/actions/python-checks/scripts/parse-mypy-output.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Parse Mypy Output +# Reads mypy_output.txt and generates GitHub annotations + +if [ -f mypy_output.txt ]; then + while IFS= read -r line; do + # Match pattern: file.py:line:column: error_type: message + if echo "$line" | grep -qE '^[^:]+:[0-9]+:[0-9]*:'; then + # Extract file, line, column, error type, and message + file=$(echo "$line" | cut -d':' -f1) + line_num=$(echo "$line" | cut -d':' -f2) + col_num=$(echo "$line" | cut -d':' -f3) + rest=$(echo "$line" | cut -d':' -f4-) + + # Extract error type (error, warning, note) and message + error_type=$(echo "$rest" | sed -E 's/^[[:space:]]*(error|warning|note):.*/\1/') + message=$(echo "$rest" | sed -E 's/^[[:space:]]*(error|warning|note):[[:space:]]*//') + + # Determine annotation level + case "$error_type" in + error) level="error" ;; + warning) level="warning" ;; + note) level="notice" ;; + *) level="error" ;; + esac + + # Generate GitHub annotation + if [ -n "$col_num" ] && [ "$col_num" != " " ]; then + echo "::${level} title=Mypy (${error_type}),file=${file},line=${line_num},col=${col_num}::${file}:${line_num}:${col_num}: ${error_type}: ${message}" + else + echo "::${level} title=Mypy (${error_type}),file=${file},line=${line_num}::${file}:${line_num}: ${error_type}: ${message}" + fi + fi + done pyproject.toml +else + # Check and add ruff config if missing + if ! grep -q "\[tool.ruff\]" pyproject.toml; then + echo "Adding default ruff configuration to pyproject.toml..." + echo "" >>pyproject.toml + echo "${DEFAULT_RUFF_CONFIG}" >>pyproject.toml + fi + + # Check and add mypy config if missing + if ! grep -q "\[tool.mypy\]" pyproject.toml; then + echo "Adding default mypy configuration to pyproject.toml..." + echo "" >>pyproject.toml + echo "${DEFAULT_MYPY_CONFIG}" >>pyproject.toml + fi +fi diff --git a/.github/scripts/go-check-outcome.sh b/.github/scripts/go-check-outcome.sh deleted file mode 100755 index 14c3b87..0000000 --- a/.github/scripts/go-check-outcome.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# Go Checks Outcome Script -# Aggregates results from tidy, lint, test, and build steps - -TIDY_STATUS="$1" -LINT_STATUS="$2" -TEST_STATUS="$3" -BUILD_STATUS="$4" - -ALL_PASSED=true -for status in "$TIDY_STATUS" "$LINT_STATUS" "$TEST_STATUS" "$BUILD_STATUS"; do - if [ "$status" != "success" ]; then - ALL_PASSED=false - break - fi -done - -if $ALL_PASSED; then - echo "status=success" >>"$GITHUB_OUTPUT" - echo "summary=✅ **Go Quality:** All checks passed." >>"$GITHUB_OUTPUT" -else - echo "status=failure" >>"$GITHUB_OUTPUT" - DETAILS="tidy=$TIDY_STATUS, lint=$LINT_STATUS, test=$TEST_STATUS, build=$BUILD_STATUS" - echo "summary=❌ **Go Quality:** Checks failed ($DETAILS)." >>"$GITHUB_OUTPUT" -fi diff --git a/.github/workflows/reusables-go.yml b/.github/workflows/reusables-go.yml index f0504a9..265f5b9 100644 --- a/.github/workflows/reusables-go.yml +++ b/.github/workflows/reusables-go.yml @@ -1,134 +1,51 @@ --- -name: Reusable Go Checks +name: "Reusable Go Checks" on: # yamllint disable-line rule:truthy workflow_call: inputs: go-version: - description: "Go version to use" + description: "The Go version to use" + required: false type: string default: "stable" working-directory: description: "Directory where Go code is located" + required: false type: string default: "." secrets: CHECKER_TOKEN: - description: "Token for API operations" + description: "The GITHUB_TOKEN passed from the caller" required: true outputs: go-summary: - description: "Summary of Go checks" - value: ${{ jobs.check-go-code.outputs.summary }} + description: "A summary of the Go code quality checks." + value: ${{ jobs.go-checks.outputs.go-summary }} go-status: - description: "Status of Go checks" - value: ${{ jobs.check-go-code.outputs.status }} + description: "The overall status of the Go checks." + value: ${{ jobs.go-checks.outputs.go-status }} permissions: contents: read pull-requests: write checks: write -env: - DEFAULT_GOLANGCI_CONFIG: | - linters: - enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused - run: - timeout: 5m - jobs: - check-go-code: + go-checks: + name: "Go Quality Checks" runs-on: ubuntu-latest outputs: - summary: ${{ steps.outcome.outputs.summary }} - status: ${{ steps.outcome.outputs.status }} + go-summary: ${{ steps.run-checks.outputs.go-summary }} + go-status: ${{ steps.run-checks.outputs.go-status }} steps: - name: Checkout Code uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 + - name: Run Go Checks + id: run-checks + uses: ./.github/actions/go-checks with: go-version: ${{ inputs.go-version }} - cache: false - - - name: Set up default golangci-lint config - shell: bash - run: | - WORKDIR="${{ inputs.working-directory }}" - CONFIG_FILES="golangci.yml golangci.yaml golangci.toml" - NO_CONFIG=true - for f in $CONFIG_FILES; do - if [ -f "$WORKDIR/.$f" ]; then - NO_CONFIG=false - break - fi - done - if $NO_CONFIG; then - echo "$DEFAULT_GOLANGCI_CONFIG" > "$WORKDIR/.golangci.yml" - fi - - - name: Check go mod tidy - id: tidy - shell: bash - working-directory: ${{ inputs.working-directory }} - continue-on-error: true - run: | - go mod tidy - if [ -n "$(git status --porcelain)" ]; then - echo "::error::go.mod/go.sum are dirty, please run 'go mod tidy'" - exit 1 - fi - - - name: Install gotestsum - shell: bash - run: go install gotest.tools/gotestsum@latest - - - name: Run golangci-lint - id: lint - uses: golangci/golangci-lint-action@v6 - continue-on-error: true - with: - version: latest working-directory: ${{ inputs.working-directory }} - args: --timeout=5m - - - name: Run Tests (Race Detector) - id: test - shell: bash - working-directory: ${{ inputs.working-directory }} - continue-on-error: true - run: | - GOTESTSUM_BIN="$(go env GOPATH)/bin/gotestsum" - $GOTESTSUM_BIN --junitfile test-results.xml -- -race ./... - - - name: Publish Test Results - uses: mikepenz/action-junit-report@v4 - if: always() - with: - report_paths: "**/test-results.xml" - check_name: "Go Test Results" - - - name: Build Check (Dry Run) - id: build - shell: bash - working-directory: ${{ inputs.working-directory }} - continue-on-error: true - run: go build -v ./... - - - name: Set Go Checks Outcome - id: outcome - if: always() - shell: bash - run: | - .github/scripts/go-check-outcome.sh \ - "${{ steps.tidy.outcome }}" \ - "${{ steps.lint.outcome }}" \ - "${{ steps.test.outcome }}" \ - "${{ steps.build.outcome }}" + token: ${{ secrets.CHECKER_TOKEN }} diff --git a/.github/workflows/reusables-python.yml b/.github/workflows/reusables-python.yml index ae85c12..0fc8e2a 100644 --- a/.github/workflows/reusables-python.yml +++ b/.github/workflows/reusables-python.yml @@ -1,144 +1,44 @@ --- -name: 'Python Code Quality Checks' +name: "Python Code Quality Checks" on: # yamllint disable-line rule:truthy workflow_call: inputs: python-version: - description: 'The Python version to use' + description: "The Python version to use" required: false type: string - default: '3.11' + default: "3.11" secrets: CHECKER_TOKEN: - description: 'The GITHUB_TOKEN passed from the caller' + description: "The GITHUB_TOKEN passed from the caller" required: true outputs: python-summary: description: "A summary of the Python code quality checks." - value: ${{ jobs.check-python-code.outputs.summary }} + value: ${{ jobs.python-checks.outputs.python-summary }} python-status: description: "The overall status of the Python checks." - value: ${{ jobs.check-python-code.outputs.status }} + value: ${{ jobs.python-checks.outputs.python-status }} permissions: contents: read pull-requests: write checks: write -env: - DEFAULT_PROJECT_CONFIG: | - [build-system] - requires = ["setuptools>=61.0", "wheel"] - build-backend = "setuptools.build_meta" - - [project] - name = "project" - version = "0.1.0" - description = "Project" - requires-python = ">=3.11" - dependencies = [] - - DEFAULT_RUFF_CONFIG: | - [tool.ruff.format] - quote-style = "double" - indent-style = "space" - - [tool.ruff] - line-length = 88 - - [tool.ruff.lint] - select = ["E", "F", "N", "I"] - - - DEFAULT_MYPY_CONFIG: | - [tool.mypy] - strict = true - namespace_packages = true - warn_unused_ignores = false - ignore_missing_imports = true jobs: - check-python-code: - name: 'Python Quality Checks' + python-checks: + name: "Python Quality Checks" runs-on: ubuntu-latest outputs: - summary: ${{ steps.outcome.outputs.summary }} - status: ${{ steps.outcome.outputs.status }} + python-summary: ${{ steps.run-checks.outputs.python-summary }} + python-status: ${{ steps.run-checks.outputs.python-status }} steps: - - name: Check out repository + - name: Checkout Code uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - name: Create default pyproject.toml if missing - env: - DEFAULT_PROJECT_CONFIG: ${{ env.DEFAULT_PROJECT_CONFIG }} - DEFAULT_RUFF_CONFIG: ${{ env.DEFAULT_RUFF_CONFIG }} - DEFAULT_MYPY_CONFIG: ${{ env.DEFAULT_MYPY_CONFIG }} - run: .github/scripts/setup-pyproject.sh - - - name: Set up Python - id: setup_python - uses: actions/setup-python@v5 + - name: Run Python Checks + id: run-checks + uses: ./.github/actions/python-checks with: python-version: ${{ inputs.python-version }} - - name: Install uv - uses: astral-sh/setup-uv@v7 - - # - name: Add uv to PATH - # run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Install Dependencies and Linters - run: | - uv sync - - name: Ensure Ruff and Mypy are installed - run: | - uv pip install ruff mypy - - name: Check Ruff Formatter - id: ruff_format - continue-on-error: true - run: | - echo "Running ruff format check..." - uv run ruff format . --check --output-format=github - - - name: Check Ruff Linter (with Annotations) - id: ruff_lint - continue-on-error: true - run: | - echo "Running ruff check with GitHub annotations..." - uv run ruff check . --output-format=github - - - name: Check Mypy (with Annotations) - id: mypy_check - continue-on-error: true - run: | - echo "Running mypy with GitHub annotations..." - set +e # Don't exit on mypy failure - uv run mypy . > mypy_output.txt 2>&1 - MYPY_EXIT_CODE=$? - - if [ -f mypy_output.txt ]; then - sed -i -E \ - 's/^([^:]+:[0-9]+):[[:space:]]+(error|warning|note):/\1:: \2:/' \ - mypy_output.txt - fi - # Parse mypy output and generate GitHub annotations - .github/scripts/parse-mypy-output.sh - # Exit with mypy's original exit code - exit $MYPY_EXIT_CODE - - - name: Set Python Checks Outcome - id: outcome - if: always() - run: | - if [[ "${{ steps.ruff_format.outcome }}" == "failure" || \ - "${{ steps.ruff_lint.outcome }}" == "failure" || \ - "${{ steps.mypy_check.outcome }}" == "failure" ]]; then - echo "status=failure" >> $GITHUB_OUTPUT - echo "summary=❌ **Python Quality:** Checks failed." \ - >> $GITHUB_OUTPUT - else - echo "status=success" >> $GITHUB_OUTPUT - echo "summary=✅ **Python Quality:** All checks passed." \ - >> $GITHUB_OUTPUT - fi From 0fd3ac8f3b61f828e1ef1d3fd0d61a01aa4d50aa Mon Sep 17 00:00:00 2001 From: G36maid Date: Mon, 2 Feb 2026 06:52:47 +0800 Subject: [PATCH 5/7] refactor(main): improve comment deletion logic and enable config checks Update `entrypoint.yml` to correctly find and delete all previous bot comments before posting a new one. Also, enable configuration file checks by default in `main.yml`. --- .github/workflows/entrypoint.yml | 16 ++++++++++------ .github/workflows/main.yml | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/entrypoint.yml b/.github/workflows/entrypoint.yml index f555145..7de9b4b 100644 --- a/.github/workflows/entrypoint.yml +++ b/.github/workflows/entrypoint.yml @@ -272,27 +272,31 @@ jobs: // --- Update or Create Comment --- try { + // 1. 獲取留言 (增加 per_page 避免被舊留言淹沒) const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, + per_page: 100 }); - const botComment = comments.data.find(comment => + + // 2. 找出所有舊的 Bot 留言 (使用 filter 而非 find) + const botComments = comments.data.filter(comment => comment.user.type === 'Bot' && comment.body.includes('🛡️ PR Quality Check Summary') ); - // 1. 如果找到舊留言,就刪除它 - if (botComment) { + // 3. 刪除所有找到的舊留言 + for (const comment of botComments) { await github.rest.issues.deleteComment({ - comment_id: botComment.id, + comment_id: comment.id, owner: context.repo.owner, repo: context.repo.repo, }); - console.log(`✅ Deleted existing comment #${botComment.id}`); + console.log(`✅ Deleted existing comment #${comment.id}`); } - // 2. 無論如何,都在最底下建立一則新留言 + // 4. 建立新留言 await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be84453..4b83ce8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ permissions: jobs: quality-checks: name: Run Organization Quality Checks - uses: sessatakuma/org-workflows/.github/workflows/entrypoint.yml@main + uses: ./.github/workflows/entrypoint.yml secrets: CHECKER_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -33,7 +33,7 @@ jobs: # Configuration Files Quality Checks (optional) # Checks YAML, JSON, and TOML files for syntax errors - run-config-checks: false + run-config-checks: true # Frontend Code Quality Checks (optional) # Requires: Node.js project with Prettier configured From 8e840e12560b32ea76c194e1cddd25b5c6b10417 Mon Sep 17 00:00:00 2001 From: G36maid Date: Mon, 2 Feb 2026 06:53:06 +0800 Subject: [PATCH 6/7] chore: remove unused check scripts This commit removes several shell scripts that were previously used for various checks within the GitHub Actions workflows. These scripts are no longer needed as their functionality has either been replaced or integrated elsewhere. --- .github/scripts/check-branch-name.sh | 46 ------------- .github/scripts/check-commit-messages.sh | 88 ------------------------ .github/scripts/check-conflicts.sh | 40 ----------- .github/scripts/check-json-files.sh | 59 ---------------- .github/scripts/check-pr-title.sh | 61 ---------------- .github/scripts/check-prettier.sh | 23 ------- .github/scripts/check-toml-files.sh | 63 ----------------- .github/scripts/check-yaml-files.sh | 57 --------------- .github/scripts/parse-eslint-json.sh | 39 ----------- .github/scripts/parse-mypy-output.sh | 35 ---------- .github/scripts/setup-pyproject.sh | 33 --------- 11 files changed, 544 deletions(-) delete mode 100755 .github/scripts/check-branch-name.sh delete mode 100755 .github/scripts/check-commit-messages.sh delete mode 100755 .github/scripts/check-conflicts.sh delete mode 100755 .github/scripts/check-json-files.sh delete mode 100755 .github/scripts/check-pr-title.sh delete mode 100755 .github/scripts/check-prettier.sh delete mode 100755 .github/scripts/check-toml-files.sh delete mode 100755 .github/scripts/check-yaml-files.sh delete mode 100755 .github/scripts/parse-eslint-json.sh delete mode 100755 .github/scripts/parse-mypy-output.sh delete mode 100755 .github/scripts/setup-pyproject.sh diff --git a/.github/scripts/check-branch-name.sh b/.github/scripts/check-branch-name.sh deleted file mode 100755 index 661e16e..0000000 --- a/.github/scripts/check-branch-name.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# 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 -if [[ "$BRANCH_NAME" = "dev" ]]; then - MESSAGE="✅ **Branch Name:** Valid special branch (\`$BRANCH_NAME\`)" - STATUS="success" -# Otherwise, validate against the standard format -else - 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 - MESSAGE="❌ **Branch Name:** Invalid type prefix. Current: \`$BRANCH_NAME\`. Must start with one of: $TYPES\`" - STATUS="failure" - elif [[ ! "$BRANCH_NAME" =~ $FULL_REGEX ]]; then - MESSAGE="❌ **Branch Name:** Invalid format after '/'. Must be lowercase alphanumeric and hyphens. Current: \`$BRANCH_NAME\`" - STATUS="failure" - fi -fi - -# Set final success message if no failure occurred -if [ "$STATUS" == "success" ] && [[ "$BRANCH_NAME" != "dev" ]]; then - MESSAGE="✅ **Branch Name:** Follows naming convention (\`$BRANCH_NAME\`)" -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 job will handle the overall failure. -if [ "$STATUS" == "failure" ]; then - exit 0 -fi diff --git a/.github/scripts/check-commit-messages.sh b/.github/scripts/check-commit-messages.sh deleted file mode 100755 index a00d0ee..0000000 --- a/.github/scripts/check-commit-messages.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -# Check Commit Messages Format -# Validates all commit messages in PR against conventional commit format - -# 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.+" -# Get all commits between the base and head of the PR -COMMITS=$(git log --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 - if [ -z "$line" ]; then - continue - fi - - TOTAL_COMMITS=$((TOTAL_COMMITS + 1)) - COMMIT_SHA=$(echo "$line" | cut -d':' -f1 | cut -c1-7) - # Handle cases where the commit message itself contains colons - COMMIT_MSG=$(echo "$line" | cut -d':' -f4-) - - ERRORS="" - - # 1. Length Check - if [ "${#COMMIT_MSG}" -gt "$MAX_LENGTH" ]; then - ERRORS="${ERRORS} ↳ Title is too long (is **${#COMMIT_MSG}** chars, max is **$MAX_LENGTH**)\n" - fi - - # 2. Format Check (Conventional Commit) - if [[ ! "$COMMIT_MSG" =~ $REGEX ]]; then - if [[ ! "$COMMIT_MSG" =~ : ]]; then - ERRORS="${ERRORS} ↳ Missing ':' separator\n" - elif [[ ! "$COMMIT_MSG" =~ ^($TYPES) ]]; then - ERRORS="${ERRORS} ↳ Invalid type prefix\n" - 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" - 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" - -# Determine the final status and construct the output summary -if [ "$FAILED_COUNT" -eq 0 ]; then - MESSAGE="✅ **Commit Messages:** All $TOTAL_COMMITS commit(s) passed (Length, Format, Case)" - STATUS="success" - REPORT="" -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 '<>"$GITHUB_OUTPUT" diff --git a/.github/scripts/check-conflicts.sh b/.github/scripts/check-conflicts.sh deleted file mode 100755 index 2040236..0000000 --- a/.github/scripts/check-conflicts.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -# Check for Merge Conflict Markers -# Searches for unresolved conflict markers in the codebase - -# Search for conflict markers recursively in the current directory. -# Exclude common directories like .git and node_modules. -# The '|| true' prevents the script from exiting if grep finds no matches. -CONFLICT_FILES=$(grep -r -n -E '^(<<<<<<<|=======|>>>>>>>)' \ - --exclude-dir=.git \ - --exclude-dir=node_modules \ - --exclude-dir=dist \ - --exclude-dir=build \ - --exclude=reusables-basic.yml \ - . 2>/dev/null || true) - -# If conflict markers were found... -if [ -n "$CONFLICT_FILES" ]; then - # Get the name of the first file containing conflicts for the summary message. - FIRST_FILE=$(echo "$CONFLICT_FILES" | head -n 1 | cut -d: -f1) - # Count the total number of unique files with conflicts. - FILE_COUNT=$(echo "$CONFLICT_FILES" | cut -d: -f1 | sort -u | wc -l) - - MESSAGE="❌ **Conflicts:** Found unresolved merge markers in $FILE_COUNT file(s) (e.g., \`$FIRST_FILE\`)" - STATUS="failure" -# If no conflicts were found... -else - MESSAGE="✅ **Conflicts:** No merge conflict markers found" - STATUS="success" -fi - -# Write the outputs for the workflow. -{ - echo "status=$STATUS" - echo "summary=$MESSAGE" -} >>"$GITHUB_OUTPUT" - -# If the check failed, exit with 0. The orchestrator will handle the failure. -if [ "$STATUS" == "failure" ]; then - exit 0 -fi diff --git a/.github/scripts/check-json-files.sh b/.github/scripts/check-json-files.sh deleted file mode 100755 index e9a449c..0000000 --- a/.github/scripts/check-json-files.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# Check JSON Files -# Validates JSON syntax using jq and generates GitHub annotations - -# Find all JSON files excluding node_modules and .git -JSON_FILES=$(find . -type f -name "*.json" \ - ! -path "*/node_modules/*" \ - ! -path "*/.git/*" \ - ! -path "*/venv/*" \ - ! -path "*/.venv/*" \ - ! -path "*/.next/*" \ - ! -path "*/dist/*" \ - ! -path "*/build/*" \ - 2>/dev/null || true) - -if [ -z "$JSON_FILES" ]; then - MESSAGE="✅ **JSON Files:** No JSON files found to check" - STATUS="success" -else - FILE_COUNT=$(echo "$JSON_FILES" | wc -l) - FAILED_FILES="" - FAILED_COUNT=0 - - # Check each JSON file - while IFS= read -r file; do - if ! jq empty "$file" >/dev/null 2>&1; then - FAILED_COUNT=$((FAILED_COUNT + 1)) - if [ -z "$FAILED_FILES" ]; then - FAILED_FILES="$file" - fi - # Get error details - ERROR_OUTPUT=$(jq empty "$file" 2>&1 || true) - - # Extract line and column if possible - # Example: parse error: ... at line 10, column 20 - if echo "$ERROR_OUTPUT" | grep -q "at line [0-9]\+, column [0-9]\+"; then - line=$(echo "$ERROR_OUTPUT" | grep -o "at line [0-9]\+" | grep -o "[0-9]\+") - col=$(echo "$ERROR_OUTPUT" | grep -o "column [0-9]\+" | grep -o "[0-9]\+") - msg=$(echo "$ERROR_OUTPUT" | sed -E 's/.*: (.*) at line.*/\1/') - echo "::error file=${file},line=${line},col=${col}::${msg}" - else - echo "::error file=${file}::JSON validation failed: ${ERROR_OUTPUT}" - fi - fi - done <<<"$JSON_FILES" - - if [ $FAILED_COUNT -eq 0 ]; then - MESSAGE="✅ **JSON Files:** All $FILE_COUNT file(s) are valid" - STATUS="success" - else - MESSAGE="❌ **JSON Files:** $FAILED_COUNT of $FILE_COUNT file(s) failed validation (e.g., \`$FAILED_FILES\`)" - STATUS="failure" - fi -fi - -{ - echo "status=$STATUS" - echo "summary=$MESSAGE" -} >>"$GITHUB_OUTPUT" diff --git a/.github/scripts/check-pr-title.sh b/.github/scripts/check-pr-title.sh deleted file mode 100755 index 10328a5..0000000 --- a/.github/scripts/check-pr-title.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# Check PR Title Format -# Validates PR title with cascading checks: length, format, lowercase - -# Script reads from environment variables: -# - TYPES: Valid commit type prefixes -# - MAX_LENGTH: Maximum title length -# - PR_TITLE: The PR title to validate - -TITLE_LENGTH=${#PR_TITLE} -STATUS="success" # Default to success - -# 1. Length Check -if [ "$TITLE_LENGTH" -gt "$MAX_LENGTH" ]; then - MESSAGE="❌ **PR Title:** Title is too long (is **$TITLE_LENGTH** chars, max is **$MAX_LENGTH**). Current: \`$PR_TITLE\`" - STATUS="failure" -fi - -# 2. Format Check (only if length check passed) -if [ "$STATUS" == "success" ]; then - REGEX="^($TYPES)(\(.+\))?:\s.+" - 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 - ERROR_DETAIL="Missing description after ':'" - else - ERROR_DETAIL="Invalid format" - fi - MESSAGE="❌ **PR Title:** $ERROR_DETAIL. Current: \`$PR_TITLE\`. Expected: \`type: description\` (e.g., \`feat: add new feature\`)" - fi -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 -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 diff --git a/.github/scripts/check-prettier.sh b/.github/scripts/check-prettier.sh deleted file mode 100755 index e464c8e..0000000 --- a/.github/scripts/check-prettier.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# Check Prettier Formatting -# Runs prettier --check and generates annotations for files needing formatting - -echo "Running prettier format check..." -set +e # Don't exit on prettier failure -npx prettier --check ./src 2>&1 | tee prettier_output.txt -PRETTIER_EXIT_CODE=$? -set -e - -if [ $PRETTIER_EXIT_CODE -ne 0 ]; then - # Extract filenames from prettier output and generate annotations - grep -E "^\S+\.(js|jsx|ts|tsx|json|css|scss|md|yaml|yml)$" prettier_output.txt | while read -r file; do - if [ -f "$file" ]; then - echo "::error title=Prettier Formatting,file=${file},line=1,col=1::File needs formatting. Run 'npm run format' to fix." - fi - done - echo "status=failure" >>"$GITHUB_OUTPUT" - echo "summary=❌ **Prettier:** Formatting issues found. Please review the annotations above." >>"$GITHUB_OUTPUT" -else - echo "status=success" >>"$GITHUB_OUTPUT" - echo "summary=✅ **Prettier:** All files are properly formatted." >>"$GITHUB_OUTPUT" -fi diff --git a/.github/scripts/check-toml-files.sh b/.github/scripts/check-toml-files.sh deleted file mode 100755 index c7510ad..0000000 --- a/.github/scripts/check-toml-files.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# Check TOML Files -# Validates TOML syntax using taplo and generates GitHub annotations - -# Find all TOML files excluding node_modules and .git -TOML_FILES=$(find . -type f -name "*.toml" \ - ! -path "*/node_modules/*" \ - ! -path "*/.git/*" \ - ! -path "*/venv/*" \ - ! -path "*/.venv/*" \ - 2>/dev/null || true) - -if [ -z "$TOML_FILES" ]; then - MESSAGE="✅ **TOML Files:** No TOML files found to check" - STATUS="success" -else - FILE_COUNT=$(echo "$TOML_FILES" | wc -l) - - # Run taplo check - set +e - TAPLO_OUTPUT=$(echo "$TOML_FILES" | xargs taplo check 2>&1) - TAPLO_EXIT=$? - set -e - - if [ $TAPLO_EXIT -eq 0 ]; then - MESSAGE="✅ **TOML Files:** All $FILE_COUNT file(s) are valid" - STATUS="success" - else - # Extract failed files from taplo output - FAILED_FILES=$(echo "$TAPLO_OUTPUT" | grep -oP '(?<=")[^"]+\.toml(?=")' | head -n 1 || echo "unknown") - FAILED_COUNT=$(echo "$TAPLO_OUTPUT" | grep -c "error" || echo "1") - - MESSAGE="❌ **TOML Files:** Found syntax errors in TOML files (e.g., \`$FAILED_FILES\`)" - STATUS="failure" - - # Show errors as GitHub annotations - # Taplo output is visual, e.g.: - # ┌─ file.toml:1:15 - echo "$TAPLO_OUTPUT" | while IFS= read -r line; do - # Check for location line: ┌─ file:line:col - if echo "$line" | grep -q "┌─ "; then - # Extract info using sed - # Remove leading whitespace and ┌─ - clean_line=$(echo "$line" | sed 's/^[[:space:]]*┌─ //') - file=$(echo "$clean_line" | cut -d':' -f1) - line_num=$(echo "$clean_line" | cut -d':' -f2) - col_num=$(echo "$clean_line" | cut -d':' -f3) - - if [ -n "$file" ] && [ -n "$line_num" ]; then - echo "::error file=${file},line=${line_num},col=${col_num}::TOML syntax error" - fi - elif echo "$line" | grep -q "error:"; then - # Fallback for general errors - echo "::error::TOML: ${line}" - fi - done - fi -fi - -{ - echo "status=$STATUS" - echo "summary=$MESSAGE" -} >>"$GITHUB_OUTPUT" diff --git a/.github/scripts/check-yaml-files.sh b/.github/scripts/check-yaml-files.sh deleted file mode 100755 index be0eeac..0000000 --- a/.github/scripts/check-yaml-files.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -# Check YAML Files -# Runs yamllint on all YAML files and generates GitHub annotations - -# Find all YAML files (*.yml, *.yaml) excluding node_modules and .git -YAML_FILES=$(find . -type f \( -name "*.yml" -o -name "*.yaml" \) \ - ! -path "*/node_modules/*" \ - ! -path "*/.git/*" \ - ! -path "*/venv/*" \ - ! -path "*/.venv/*" \ - 2>/dev/null || true) - -if [ -z "$YAML_FILES" ]; then - MESSAGE="✅ **YAML Files:** No YAML files found to check" - STATUS="success" -else - FILE_COUNT=$(echo "$YAML_FILES" | wc -l) - - # Run yamllint with relaxed configuration - set +e - YAMLLINT_OUTPUT=$(echo "$YAML_FILES" | xargs uv run yamllint -f parsable 2>&1) - YAMLLINT_EXIT=$? - set -e - - if [ $YAMLLINT_EXIT -eq 0 ]; then - MESSAGE="✅ **YAML Files:** All $FILE_COUNT file(s) passed validation" - STATUS="success" - else - # Count failed files - FAILED_COUNT=$(echo "$YAMLLINT_OUTPUT" | cut -d':' -f1 | sort -u | wc -l) - # Get first failed file for summary - FIRST_FAILED=$(echo "$YAMLLINT_OUTPUT" | head -n 1 | cut -d':' -f1) - - MESSAGE="❌ **YAML Files:** Found issues in $FAILED_COUNT of $FILE_COUNT file(s) (e.g., \`$FIRST_FAILED\`)" - STATUS="failure" - - # Show errors in GitHub annotations - echo "$YAMLLINT_OUTPUT" | while IFS=: read -r file line col level msg; do - if [ -n "$file" ] && [ -n "$line" ]; then - # Clean up level (remove brackets) - clean_level=$(echo "$level" | tr -d '[]' | xargs) - # Map yamllint level to GitHub annotation level - if [ "$clean_level" = "warning" ]; then - gh_level="warning" - else - gh_level="error" - fi - echo "::${gh_level} file=${file},line=${line},col=${col}::${msg}" - fi - done - fi -fi - -{ - echo "status=$STATUS" - echo "summary=$MESSAGE" -} >>"$GITHUB_OUTPUT" diff --git a/.github/scripts/parse-eslint-json.sh b/.github/scripts/parse-eslint-json.sh deleted file mode 100755 index 96380f8..0000000 --- a/.github/scripts/parse-eslint-json.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# Parse ESLint JSON Output -# Converts ESLint JSON to GitHub annotations - -echo "Running ESLint with GitHub annotations..." -set +e -npx eslint ./src --format json --output-file eslint_output.json -ESLINT_EXIT_CODE=$? - -if [ -f eslint_output.json ]; then - node -e " -const fs = require('fs'); -const results = JSON.parse(fs.readFileSync('eslint_output.json', 'utf8')); - -let hasErrors = false; -results.forEach(result => { - result.messages.forEach(message => { - const file = result.filePath.replace(process.cwd() + '/', ''); - const line = message.line || 1; - const column = message.column || 1; - const severity = message.severity === 2 ? 'error' : 'warning'; - const ruleId = message.ruleId ? \` (\${message.ruleId})\` : ''; - const messageText = message.message; - - if (message.severity === 2) hasErrors = true; - - console.log(\`::\${severity} title=ESLint\${ruleId},file=\${file},line=\${line},col=\${column}::\${file}:\${line}:\${column}: \${messageText}\`); - }); -}); -" -fi - -if [ $ESLINT_EXIT_CODE -ne 0 ]; then - echo "status=failure" >>"$GITHUB_OUTPUT" - echo "summary=❌ **ESLint:** Issues found. Please review the annotations above." >>"$GITHUB_OUTPUT" -else - echo "status=success" >>"$GITHUB_OUTPUT" - echo "summary=✅ **ESLint:** No issues found." >>"$GITHUB_OUTPUT" -fi diff --git a/.github/scripts/parse-mypy-output.sh b/.github/scripts/parse-mypy-output.sh deleted file mode 100755 index cca2d85..0000000 --- a/.github/scripts/parse-mypy-output.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# Parse Mypy Output -# Reads mypy_output.txt and generates GitHub annotations - -if [ -f mypy_output.txt ]; then - while IFS= read -r line; do - # Match pattern: file.py:line:column: error_type: message - if echo "$line" | grep -qE '^[^:]+:[0-9]+:[0-9]*:'; then - # Extract file, line, column, error type, and message - file=$(echo "$line" | cut -d':' -f1) - line_num=$(echo "$line" | cut -d':' -f2) - col_num=$(echo "$line" | cut -d':' -f3) - rest=$(echo "$line" | cut -d':' -f4-) - - # Extract error type (error, warning, note) and message - error_type=$(echo "$rest" | sed -E 's/^[[:space:]]*(error|warning|note):.*/\1/') - message=$(echo "$rest" | sed -E 's/^[[:space:]]*(error|warning|note):[[:space:]]*//') - - # Determine annotation level - case "$error_type" in - error) level="error" ;; - warning) level="warning" ;; - note) level="notice" ;; - *) level="error" ;; - esac - - # Generate GitHub annotation - if [ -n "$col_num" ] && [ "$col_num" != " " ]; then - echo "::${level} title=Mypy (${error_type}),file=${file},line=${line_num},col=${col_num}::${file}:${line_num}:${col_num}: ${error_type}: ${message}" - else - echo "::${level} title=Mypy (${error_type}),file=${file},line=${line_num}::${file}:${line_num}: ${error_type}: ${message}" - fi - fi - done pyproject.toml -else - # Check and add ruff config if missing - if ! grep -q "\[tool.ruff\]" pyproject.toml; then - echo "Adding default ruff configuration to pyproject.toml..." - echo "" >>pyproject.toml - echo "${DEFAULT_RUFF_CONFIG}" >>pyproject.toml - fi - - # Check and add mypy config if missing - if ! grep -q "\[tool.mypy\]" pyproject.toml; then - echo "Adding default mypy configuration to pyproject.toml..." - echo "" >>pyproject.toml - echo "${DEFAULT_MYPY_CONFIG}" >>pyproject.toml - fi -fi From b41a9a07bb91b19f06ee2b0e74951a79e0ad80ab Mon Sep 17 00:00:00 2001 From: G36maid Date: Mon, 2 Feb 2026 07:05:04 +0800 Subject: [PATCH 7/7] docs: update docs to reflect architecture refactor - Refactor HOW_TO_ADD_WORKFLOWS.md: clearer step-by-step workflow architecture - Rewrite README.md in Traditional Chinese with architecture diagrams - Document output contracts and naming conventions - Add local testing instructions with act and yamllint --- HOW_TO_ADD_WORKFLOWS.md | 314 +++++++++++++++++++--------------------- README.md | 272 ++++++++++++++++++++-------------- 2 files changed, 306 insertions(+), 280 deletions(-) diff --git a/HOW_TO_ADD_WORKFLOWS.md b/HOW_TO_ADD_WORKFLOWS.md index aaed3c1..708e340 100644 --- a/HOW_TO_ADD_WORKFLOWS.md +++ b/HOW_TO_ADD_WORKFLOWS.md @@ -1,225 +1,203 @@ -## 📄 如何新增一個全新的檢查類別 +# 🛠️ 如何新增工作流程 (How to Add Workflows) -> [!WARNING] -> Half written by claude-opus-4-5-thinking. +本專案採用 **Caller (呼叫者) → Orchestrator (協調者) → Reusable Workflow (可重複使用工作流程) → Composite Action (複合動作)** 的分層架構。新增檢查機制時,請依循此模式以保持架構整潔與一致性。 -假設您想新增一個「文件拼寫檢查」(Doc Linting)類別,並希望它像 `run-python-checks` 一樣可以被開啟或關閉。 +## 架構概觀 -這需要三個部分的修改: +1. **Entrypoint (`entrypoint.yml`)**: 協調者。定義所有可用的檢查開關,並彙整最終報告。 +2. **Reusable Workflow (`reusables-*.yml`)**: 中介層。負責呼叫具體的 Action,處理權限與 Secrets。 +3. **Composite Action (`actions/*/action.yml`)**: 實作層。包含實際的檢查邏輯、工具安裝與腳本執行。 -1. **建立**新的可重用工作流程(`.github/workflows/reusables-docs.yml`)並設定其輸出。 -2. **修改**這個協同調度器檔案(`.github/workflows/entrypoint.yml`)來呼叫它並讀取其結果。 -3. **更新**說明文件 `README.md` 讓其它人可以知道如何使用。 +--- -### 第 1 步:建立可重用的工作流程 +## 步驟 1:建立 Composite Action -這就是您「如何設定輸出」問題的答案。輸出是子工作流程回報給此協同調度器的方式。 +這是實際執行檢查的地方。 -您必須遵循 `report-summary` 腳本中設定的**命名慣例**: +1. 在 `.github/actions/` 下建立新的目錄,例如 `my-new-checks`。 +2. 建立 `action.yml` 和 `scripts/` 目錄。 - * `[check-name]-summary`: (必要) 一段人類可讀的摘要字串,將會被貼到 PR 留言中。 - * `[check-name]-status`: (必要) 一個狀態字串,通常是 `success` 或 `failure`。 +**`.github/actions/my-new-checks/action.yml` 範例:** -**範例: `.github/workflows/reusables-docs.yml`** +```yaml +name: "My New Checks" +description: "Run my new custom checks" + +inputs: + my-option: + description: "An option for the check" + required: false + default: "default-value" + +outputs: + # 統一輸出命名格式:-status 和 -summary + new-check-status: + description: "Status of the checks (success or failure)" + value: ${{ steps.outcome.outputs.status }} + new-check-summary: + description: "Summary for the PR comment" + value: ${{ steps.outcome.outputs.summary }} + +runs: + using: "composite" + steps: + - name: Run Check + id: check_step + shell: bash + # 重要:使用 continue-on-error 避免單一檢查失敗導致整個 Job 中斷 + continue-on-error: true + run: | + # 執行您的檢查腳本 + ${{ github.action_path }}/scripts/run-check.sh + + - name: Set Outcome + id: outcome + if: always() # 確保即使檢查失敗也會執行此步驟 + shell: bash + run: | + if [[ "${{ steps.check_step.outcome }}" == "failure" ]]; then + echo "status=failure" >> "$GITHUB_OUTPUT" + echo "summary=❌ **My Check:** Failed." >> "$GITHUB_OUTPUT" + else + echo "status=success" >> "$GITHUB_OUTPUT" + echo "summary=✅ **My Check:** Passed." >> "$GITHUB_OUTPUT" + fi +``` + +### 腳本規範 (`scripts/*.sh`) + +- 使用 `#!/usr/bin/env bash` +- 設定 `export LC_ALL=C` +- 腳本失敗時不要直接 `exit 1`(除非是致命錯誤),應輸出錯誤並由 `action.yml` 判斷 `outcome`。 + +--- + +## 步驟 2:建立 Reusable Workflow + +這個 Workflow 負責包裝 Composite Action,讓 `entrypoint.yml` 可以呼叫。 + +**`.github/workflows/reusables-new-check.yml` 範例:** ```yaml -name: 'Reusable Docs Linting' +name: "My New Quality Checks" -on: +on: # yamllint disable-line rule:truthy workflow_call: - # 1. 在這裡定義工作流程的「輸出」, - # 這樣協同調度器才能接收它們。 - outputs: - docs-lint-summary: - description: 'Summary of the docs linting check.' - value: ${{ jobs.lint-docs.outputs.summary }} - docs-lint-status: - description: 'Status (success/failure) of the docs linting check.' - value: ${{ jobs.lint-docs.outputs.status }} + inputs: + my-option: + description: "Option passed from entrypoint" + required: false + type: string + default: "default" secrets: CHECKER_TOKEN: required: true + outputs: + # 對應 Composite Action 的輸出 + new-check-status: + value: ${{ jobs.new-check-job.outputs.new-check-status }} + new-check-summary: + value: ${{ jobs.new-check-job.outputs.new-check-summary }} jobs: - lint-docs: - name: Run Docs Linter + new-check-job: + name: "Run New Checks" runs-on: ubuntu-latest - - # 2. 讓任務(Job)也定義「輸出」 outputs: - summary: ${{ steps.run-linter.outputs.summary }} - status: ${{ steps.run-linter.outcome }} # 'outcome' 會是 'success' 或 'failure' - + new-check-status: ${{ steps.run.outputs.new-check-status }} + new-check-summary: ${{ steps.run.outputs.new-check-summary }} steps: - - name: Checkout code + - name: Checkout Code uses: actions/checkout@v4 - # --- 這是您實際的檢查邏輯 --- - - name: Run Spell Check - id: run-linter # 3. 給這個步驟一個 ID - # 假設 linter 成功時退出 0,失敗時退出 1 - # 'continue-on-error: true' 很重要,這樣才能繼續執行並報告失敗 - continue-on-error: true - run: | - echo "Running spell check..." - # 這裡放您的實際 linter 指令 - # spell-check-command --report-file spell-report.txt - - # 假設檢查失敗 - echo "Spell check failed" - exit 1 - - # --- 這是設定輸出的關鍵步驟 --- - - name: Set Linter Output - # 4. 根據 'run-linter' 步驟的結果來設定「步驟輸出」 - id: set-output - if: always() - run: | - if: ${{ steps.run-linter.outcome == 'success' }} - then - echo "summary=✅ **Docs Linting:** All files look good!" >> $GITHUB_OUTPUT - else - echo "summary=❌ **Docs Linting:** Found spelling errors." >> $GITHUB_OUTPUT - fi -``` + - name: Run Composite Action + id: run + uses: ./.github/actions/my-new-checks + with: + my-option: ${{ inputs.my-option }} -#### 開發規範 +--- -##### yamllint 合規 -所有 workflow 檔案必須通過 `yamllint` 檢查。常見注意事項: -- 檔案開頭加 `---` -- 避免行尾空白 -- 確保檔案結尾有換行符 +## 步驟 3:更新 Entrypoint (`entrypoint.yml`) -> [!TIP] -> `on:` 關鍵字會觸發 yamllint 的 truthy 警告,建議使用: -> ```yaml -> on: # yamllint disable-line rule:truthy -> ``` +這是最關鍵的一步,將新的 Workflow 整合到主流程中。 -##### 外部腳本 -當 shell 指令較為複雜(例如包含迴圈、條件判斷、多行邏輯)時,應提取至 `.github/scripts/` 目錄: -- 腳本需為可執行檔(`chmod +x`) -- 透過 `env:` 區塊傳遞 workflow expressions 給腳本 -- 輸出寫入 `$GITHUB_OUTPUT` +### 1. 新增 Input +在 `on: workflow_call: inputs:` 區段新增開關: ```yaml -- name: Check Something - env: - MY_VAR: ${{ github.event.pull_request.title }} - run: .github/scripts/check-something.sh -``` - ------ - -### 第 2 步:修改協同調度器 - -現在您有了一個 `.github/workflows/reusables-docs.yml`,您需要讓這個協同調度器檔案去呼叫它。 - -**1. 新增一個 `input` 來控制它:** - -在 `on.workflow_call.inputs` 區塊,新增: - -```yaml - run-docs-checks: - description: 'Whether to run the documentation quality checks.' + run-new-checks: + description: 'Whether to run the new checks.' required: false type: boolean default: false ``` -**2. 新增一個 `job` 來呼叫它:** - -在 `jobs` 區塊,新增一個 `call-docs-checks` 任務: +### 2. 新增 Job +呼叫您在步驟 2 建立的 Workflow: ```yaml - call-docs-checks: - name: Run Documentation Quality Checks - if: inputs.run-docs-checks # 使用您剛才新增的 input - uses: ./.github/workflows/reusables-docs.yml + call-new-checks: + name: Run New Quality Checks + if: inputs.run-new-checks + uses: ./.github/workflows/reusables-new-check.yml + with: + my-option: 'some-value' secrets: CHECKER_TOKEN: ${{ secrets.CHECKER_TOKEN }} ``` -**3. 更新 `report-summary` 任務:** - -這是最後且最重要的一步。 - - * **A. 新增 `needs` 依賴:** - 告訴 `report-summary` 任務也要等待 `call-docs-checks` 完成。 - - ```yaml - report-summary: - name: Report Overall Summary - runs-on: ubuntu-latest - needs: - - call-basic-checks - - call-python-checks - - call-config-checks - - call-docs-checks # <-- 新增這一行 - if: always() - ... - ``` - - * **B. 更新 `github-script` 腳本:** - 在腳本中新增一個區塊來讀取 `needs['call-docs-checks']` 的輸出。把它放在 "Config Checks" 區塊後面即可。 - - ```javascript - // ... (Config Checks 區塊結束) ... +### 3. 更新報告 (JavaScript) +在 `report-summary` Job 中: +1. 將 `call-new-checks` 加入 `needs` 列表。 +2. 更新 `github-script` 步驟,解析輸出並產生報告。 - // --- Docs Checks --- - if (wasJobRun(needs['call-docs-checks'])) { +```javascript + // --- New Checks --- + if (wasJobRun(needs['call-new-checks'])) { anyJobRan = true; - const { outputs } = needs['call-docs-checks']; - + const { outputs } = needs['call-new-checks']; + if (outputs) { - // 'docs-lint-summary' 必須符合您在 reusables-docs.yml 中定義的 output 名稱 - comment_body += getOutput(outputs, 'docs-lint-summary', '⚠️ **Docs Linting:** No output received') + "\n"; + // 取得 Summary + comment_body += getOutput(outputs, 'new-check-summary', + '⚠️ **New Check:** No output received') + "\n"; - // 'docs-lint-status' 也是 - if (getOutput(outputs, 'docs-lint-status') === 'failure') { + // 判斷狀態 + if (getOutput(outputs, 'new-check-status') === 'failure') { all_passed = false; } } else { - comment_body += "⚠️ **Docs Checks:** Completed but no outputs received\n"; + comment_body += "⚠️ **New Checks:** Completed but no outputs received\n"; } } - - // --- Detailed Reports --- - // ... - ``` - -### 第 3 步:更新說明文件 - - -在功能開發完成後,最後一步是更新文件,讓組織中的其他成員知道這項新檢查的存在,以及如何在他們的專案中啟用它。 - -您主要需要更新`README.md`的兩個地方: - -1. 新增arguments的說明。 -2. 新增checking details的說明。 +``` +--- -在您的 `README.md` 檔案中,找到說明可用 `inputs` 的部分,並新增 `run-docs-checks` 的條目。 +## 步驟 4:更新文件與測試 -| Input | Description | Type | Default | -| :--- | :--- | :--- | :--- | -| `run-basic-checks` | Whether to run the basic PR quality checks. | `boolean` | `true` | -| `run-python-checks` | Whether to run the Python code quality checks. | `boolean` | `false` | -| `python-version` | The Python version to use for the Python checks. | `string` | `'3.11'` | -| `run-config-checks` | Whether to run the configuration files quality checks. | `boolean` | `false` | -| **`run-docs-checks`** | **Whether to run the documentation quality checks.** | **`boolean`** | **`false`** | +1. **更新 `README.md`**:在 Inputs 列表中加入新的參數說明。 +2. **本地驗證**: + ```bash + # 檢查 YAML 語法 + yamllint .github/ -此外,記得在 `Checking details` 內簡單說明你的workflows檢查了什麼項目。 + # 檢查 Shell 腳本 + shellcheck .github/actions/my-new-checks/scripts/*.sh + # 使用 act 進行本地模擬測試 (需先安裝 act) + # 列出可用工作流程 + act pull_request --list -``` -### Python Code Quality Checks -This checker will validate: -1. **Syntax of the markdown file**: ... -2. ... -``` + # 進行 dry-run (不實際執行) + act pull_request -n + ``` +3. **整合測試**:建立一個測試用的 PR,開啟該檢查,確認 Bot 有正確留言回報狀態。 ------ +## 命名慣例 -完成這三步後,您的新檢查類別就完全整合並準備好供組織內的其他專案使用了。 +- **Workflow 檔案**: `reusables-.yml` +- **Action 目錄**: `.github/actions/-checks/` +- **Job ID**: `call--checks` +- **Outputs**: `-status`, `-summary` \ No newline at end of file diff --git a/README.md b/README.md index 7691b82..045d9ea 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,166 @@ # Sessatakuma Quality Checking Workflows -This is a repository with our shared workflows. +此儲存庫提供 Sessatakuma 組織共用的 GitHub Actions 工作流程。透過集中管理的「品質閘門 (Quality Gate)」,確保所有專案的程式碼品質與風格一致。 -## Arguments -To use this to check code before PR, please refer to [sample caller](.github/workflows/main.yml). +## 🚀 快速開始 (Quick Start) -To apply workflows, please set the following arguments in `with` section: +要在您的儲存庫中使用這些檢查,請建立 `.github/workflows/quality-checks.yml` 檔案並貼上以下內容: -| Input | Description | Type | Default | +```yaml +--- +name: Quality Checks +on: # yamllint disable-line rule:truthy + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + quality-gate: + # 呼叫組織共用的 entrypoint + uses: sessatakuma/org-workflows/.github/workflows/entrypoint.yml@main + secrets: + CHECKER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # 根據您的專案需求開啟對應檢查 + run-basic-checks: true # 檢查 PR 標題、分支名稱 + run-python-checks: true # Python 專案 (Ruff, Mypy) + run-frontend-checks: false # 前端專案 (ESLint, Prettier) + run-go-checks: false # Go 專案 + run-config-checks: true # 設定檔檢查 (YAML, JSON) +``` + +就是這樣!您的 PR 現在會自動執行這些檢查並在留言中回報結果。 + +--- + +## ⚙️ 參數設定 (Configuration) + +### 1. 輸入參數 (Inputs) + +您可以透過 `with` 區塊調整檢查行為: + +| 參數 (Input) | 類型 | 說明 | 預設值 | | :--- | :--- | :--- | :--- | -| `run-basic-checks` | Whether to run the basic PR quality checks. | `boolean` | `true` | -| `run-python-checks` | Whether to run the Python code quality checks. | `boolean` | `false` | -| `python-version` | The Python version to use for the Python checks. | `string` | `'3.11'` | -| `run-config-checks` | Whether to run the configuration files quality checks. | `boolean` | `false` | -| `run-frontend-checks` | Whether to run the frontend code quality checks. | `boolean` | `false` | -| `run-go-checks` | Whether to run the Golang code quality checks. | `boolean` | `false` | -| `go-version` | The Go version to use. | `string` | `'stable'` | -| `go-working-directory` | Working directory for Go checks. | `string` | `'.'` | - -## Checking details -### Basic PR Quality Checks -This checker will validate: -1. **PR Title Validation** - - Checks for conventional commit format (e.g., `feat:`, `fix:`, `docs:`) - - Validates title length (typically 50-72 characters) - - Ensures proper capitalization and formatting - - Generates clear feedback for non-compliant titles -2. **Branch Name Validation** - - Enforces type-based naming conventions (e.g., `feature/`, `bugfix/`, `hotfix/`) - - Validates branch name format and structure - - Provides suggestions for proper naming patterns -3. **Commit Message Analysis** - - Analyzes all commit messages in the PR - - Validates conventional commit format for each commit - - Checks message length and capitalization rules - - Provides detailed reports with specific line-by-line feedback -4. **Merge Conflict Detection** - - Scans all files for unresolved merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) - - Creates annotations pointing to specific conflict locations - - Prevents accidental merging of conflicted code - -**Note:** Basic checks provide comprehensive feedback through PR comments and status checks. - -### Python Code Quality Checks -This checker will validate: -1. **Ruff Code Formatting Check** - - Runs `ruff format . --check --output-format=github` to verify code formatting - - Automatically generates GitHub annotations for formatting violations - - Uses Ruff's fast Python formatter for consistent code style - - Provides clear indication of files that need formatting fixes -2. **Ruff Linting Analysis** - - Executes `ruff check . --output-format=github` for comprehensive linting - - Catches common Python errors, style issues, and code smells - - Generates GitHub annotations with file, line, and column information - - Includes rule codes (e.g., F401, E501) for easy reference and configuration -3. **Mypy Type Checking** - - Runs `mypy .` to validate type hints and catch type-related errors - - Parses mypy output format: `file.py:line:column: error_type: message` - - Generates GitHub annotations with proper error/warning/notice levels - - Creates detailed annotations with file locations and error descriptions - - Distinguishes between errors, warnings, and informational notes - -**Note:** Python checks create comprehensive inline annotations on your PR with precise file locations, making it easy to identify and fix issues directly from the GitHub interface. - -### Go Code Quality Checks -This checker will validate: -1. **Go Mod Tidy Check** - - Verifies that `go.mod` and `go.sum` are in sync - - Detects missing or unused dependencies - - Ensures reproducible builds -2. **golangci-lint Analysis** - - Runs comprehensive static analysis with multiple linters - - Generates GitHub annotations for code issues - - Checks for common Go anti-patterns and bugs -3. **Go Test with Race Detector** - - Executes all tests with `-race` flag enabled - - Detects data races in concurrent code - - Reports test failures with detailed output -4. **Build Verification** - - Verifies that all packages compile successfully - - Catches syntax errors and type mismatches - - Ensures the codebase is in a buildable state - -**Note:** Go checks create inline annotations on your PR for linting issues, making it easy to identify and fix problems directly from the GitHub interface. - -### Configuration Files Quality Checks -This checker will validate: -1. **YAML File Validation (yamllint)** - - Runs `yamllint` to check YAML syntax and style compliance - - Validates indentation, line length, and YAML structure - - Generates GitHub annotations for syntax errors and style violations - - Checks for common YAML pitfalls like incorrect boolean values and quotes -2. **JSON File Validation (jq)** - - Uses `jq` to parse and validate JSON file syntax - - Detects malformed JSON, missing brackets, and trailing commas - - Creates annotations pointing to specific syntax error locations - - Ensures JSON files are properly formatted and parseable -3. **TOML File Validation (taplo-cli)** - - Executes `taplo-cli check` for TOML syntax and formatting validation - - Validates TOML structure, key-value pairs, and data types - - Generates annotations for syntax errors and formatting issues - - Ensures TOML files follow proper formatting standards - -**Note:** Configuration file checks create precise inline annotations for syntax errors, with clear error messages and exact file locations for quick resolution. - -### Frontend Code Quality Checks -This checker will validate: -1. **Prettier Formatting Check** - - Runs prettier to verify code formatting - - Generates GitHub annotations for files that need formatting - - Provides helpful error messages with fix instructions -2. **ESLint Linting Check** - - Runs ESLint with JSON output format - - Parses the JSON results using Node.js - - Creates proper GitHub annotations with file, line, column info - - Distinguishes between errors and warnings - - Includes rule IDs in annotation titles \ No newline at end of file +| `run-basic-checks` | `boolean` | 檢查 PR 標題、分支命名、Commit 訊息格式與衝突。 | `true` | +| `run-python-checks` | `boolean` | 是否執行 Python 品質檢查 (Ruff, Mypy)。 | `false` | +| `python-version` | `string` | 使用的 Python 版本。 | `'3.11'` | +| `run-frontend-checks` | `boolean` | 是否執行前端品質檢查 (Prettier, ESLint)。 | `false` | +| `run-go-checks` | `boolean` | 是否執行 Go 品質檢查 (Lint, Test, Build)。 | `false` | +| `go-version` | `string` | 使用的 Go 版本 (例如 `1.21` 或 `stable`)。 | `'stable'` | +| `go-working-directory` | `string` | Go 專案的工作目錄 (若不在根目錄時使用)。 | `'.'` | +| `run-config-checks` | `boolean` | 是否驗證 YAML, JSON, TOML 檔案語法。 | `false` | + +### 2. 機密資訊 (Secrets) + +| 名稱 (Secret) | 是否必填 | 說明 | +| :--- | :--- | :--- | +| `CHECKER_TOKEN` | **是** | 請傳入 `${{ secrets.GITHUB_TOKEN }}` 以供機器人發表 PR 留言。 | + +--- + +## 🔍 檢查項目詳情 + +### 1. Basic Checks (基本檢查) +* **PR Title**: 必須符合 Conventional Commits (例如 `feat: add new login page`)。 +* **Branch Name**: 必須包含類別前綴 (例如 `feature/`, `bugfix/`, `hotfix/`)。 +* **Merge Conflicts**: 檢查是否包含未解決的衝突標記 (`<<<<<<<`)。 + +### 2. Python Checks +* 使用 `uv` 與 `ruff` 進行極速 Linting 與 Formatting。 +* 使用 `mypy` 進行靜態型別檢查。 +* *Feature*: 若您的專案沒有 `pyproject.toml`,會自動注入預設配置。 + +### 3. Frontend Checks +* **Prettier**: 程式碼格式化。 +* **ESLint**: JavaScript/TypeScript 語法檢查。 +* *Feature*: 支援自動注入預設的 `.prettierrc` 與 `eslint.config.mjs`。 + +### 4. Go Checks +* **Go Mod**: 檢查 `go.mod` 與 `go.sum` 是否同步。 +* **GolangCI-Lint**: 靜態分析。 +* **Race Detector**: 使用 `-race` 執行測試。 + +--- + +## 🔧 新增或修改工作流程 (Adding or Modifying Workflows) + +如果您想要新增或修改檢查工作流程,請參考 **[如何新增工作流程 (How to Add Workflows)](./HOW_TO_ADD_WORKFLOWS.md)** 設計指南,了解完整的架構設計、實作步驟與命名慣例。 + +--- + +## 🏗️ 架構概覽 (Architecture Overview) + +本專案採用分層架構設計,以確保靈活性與可維護性: + +```mermaid +graph TD + UserWorkflow[User Workflow] --> Entrypoint[entrypoint.yml] + + Entrypoint --> R_Basic[reusables-basic.yml] --> A_Basic[basic-checks] + A_Basic --> S_Basic[scripts/*.sh] + + Entrypoint --> R_Python[reusables-python.yml] --> A_Python[python-checks] + A_Python --> S_Python[scripts/*.sh] + + Entrypoint --> R_Config[reusables-config.yml] --> A_Config[config-checks] + A_Config --> S_Config[scripts/*.sh] + + Entrypoint --> R_Frontend[reusables-frontend.yml] --> A_Frontend[frontend-checks] + A_Frontend --> S_Frontend[scripts/*.sh] + + Entrypoint --> R_Go[reusables-go.yml] --> A_Go[go-checks] + A_Go --> S_Go[Inline Bash / Go Tools] +``` + +### 🔄 資料流向 (Data Flow) + +以下時序圖展示了參數如何向下傳遞成為環境變數,以及執行結果如何向上回報: + +```mermaid +sequenceDiagram + participant User as User Repo + participant Entry as Entrypoint + participant Reusable as Reusable Workflow + participant Action as Composite Action + participant Script as Shell Scripts + + User->>Entry: with: run-basic-checks: true + Entry->>Reusable: with: python-version: "3.11" + Reusable->>Action: inputs: python-version + + Action->>Script: env: PYTHON_VERSION + + activate Script + Script->>Script: Validate & Execute + Script-->>Action: echo "status=success" >> $GITHUB_OUTPUT + deactivate Script + + Action-->>Reusable: outputs: python-status + Reusable-->>Entry: outputs: python-status + + Entry->>Entry: Aggregate all outputs + Entry->>User: Post PR Comment +``` + +--- + +## 💻 本地模擬測試 (Local Testing) + +在將程式碼推送到 GitHub 之前,您可以使用 [act](https://github.com/nektos/act) 在本機電腦上模擬執行這些檢查。這能幫助您快速修復錯誤,無需等待 CI 排隊。 + +**先決條件**:需安裝 Docker, `act` 與 `yamllint`。 + +```bash +# 1. 驗證 YAML 語法 (強烈建議) +yamllint .github/ + +# 2. 在您的專案根目錄列出可用的 Actions +act pull_request --list + +# 3. 模擬執行 Pull Request 事件 (執行所有檢查) +act pull_request + +# 4. Dry run (檢查流程結構但不實際執行) +act pull_request -n +``` + +> **注意**:由於 `act` 是模擬環境,某些 GitHub 特有功能(如 OIDC 或快取)可能無法完全運作,但對於驗證程式碼品質檢查通常已足夠。