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/scripts/check-branch-name.sh b/.github/actions/basic-checks/scripts/check-branch-name.sh similarity index 77% rename from .github/scripts/check-branch-name.sh rename to .github/actions/basic-checks/scripts/check-branch-name.sh index 661e16e..8dcdee4 100755 --- a/.github/scripts/check-branch-name.sh +++ b/.github/actions/basic-checks/scripts/check-branch-name.sh @@ -2,10 +2,6 @@ # Check Branch Name Format # Validates branch naming convention with special case for 'dev' branch -# Script reads from environment variables: -# - TYPES: Valid branch type prefixes -# - BRANCH_NAME: The branch name to validate - STATUS="success" # Default to success # Allow 'dev' as a special case; it's always valid @@ -14,12 +10,12 @@ if [[ "$BRANCH_NAME" = "dev" ]]; then STATUS="success" # Otherwise, validate against the standard format else - FULL_REGEX="^($TYPES)\/([a-z0-9][a-z0-9-]*)$" + FULL_REGEX="^($TYPES)/([a-z0-9][a-z0-9-]*)$" if [[ ! "$BRANCH_NAME" =~ / ]]; then MESSAGE="❌ **Branch Name:** Missing '/' separator. Current: \`$BRANCH_NAME\`. Expected: \`type/description\`" STATUS="failure" - elif [[ ! "$BRANCH_NAME" =~ ^($TYPES)\/ ]]; then + elif [[ ! "$BRANCH_NAME" =~ ^($TYPES)/ ]]; then MESSAGE="❌ **Branch Name:** Invalid type prefix. Current: \`$BRANCH_NAME\`. Must start with one of: $TYPES\`" STATUS="failure" elif [[ ! "$BRANCH_NAME" =~ $FULL_REGEX ]]; then @@ -39,8 +35,6 @@ fi echo "summary=$MESSAGE" } >>"$GITHUB_OUTPUT" -# If the check failed, exit with 0 to prevent the script from stopping the workflow. -# The orchestrator job will handle the overall failure. if [ "$STATUS" == "failure" ]; then exit 0 fi diff --git a/.github/scripts/check-commit-messages.sh b/.github/actions/basic-checks/scripts/check-commit-messages.sh similarity index 77% rename from .github/scripts/check-commit-messages.sh rename to .github/actions/basic-checks/scripts/check-commit-messages.sh index a00d0ee..f47826f 100755 --- a/.github/scripts/check-commit-messages.sh +++ b/.github/actions/basic-checks/scripts/check-commit-messages.sh @@ -2,15 +2,21 @@ # Check Commit Messages Format # Validates all commit messages in PR against conventional commit format +# Force standard locale to ensure [a-z] ranges work as expected +export LC_ALL=C + # Script reads from environment variables: # - TYPES: Valid commit types # - MAX_LENGTH: Maximum commit message length # - BASE_SHA: Base commit for comparison # - HEAD_SHA: Head commit for comparison -REGEX="^($TYPES)(\(.+\))?(!?):\s.+" +# Use [[:space:]] for POSIX compatibility +REGEX="^($TYPES)(\(.+\))?(!?):[[:space:]].+" + # Get all commits between the base and head of the PR -COMMITS=$(git log --format="%H:::%s" "$BASE_SHA".."$HEAD_SHA") +# Added --no-color to prevent ANSI codes from breaking regex +COMMITS=$(git log --no-color --format="%H:::%s" "$BASE_SHA".."$HEAD_SHA") FAILED_COMMITS_REPORT="" TOTAL_COMMITS=0 @@ -18,7 +24,7 @@ FAILED_COUNT=0 # Use a while loop to read each commit line by line while IFS= read -r line; do - # Skip empty lines, which can happen with git log + # Skip empty lines if [ -z "$line" ]; then continue fi @@ -41,7 +47,7 @@ while IFS= read -r line; do ERRORS="${ERRORS} ↳ Missing ':' separator\n" elif [[ ! "$COMMIT_MSG" =~ ^($TYPES) ]]; then ERRORS="${ERRORS} ↳ Invalid type prefix\n" - elif [[ "$COMMIT_MSG" =~ ^($TYPES)(\(.+\))?:[[:space:]]*$ ]]; then + elif [[ "$COMMIT_MSG" =~ ^($TYPES)(\(.+\))?(!?)?:[[:space:]]*$ ]]; then ERRORS="${ERRORS} ↳ Missing description after ':'\n" else ERRORS="${ERRORS} ↳ Invalid format\n" @@ -49,15 +55,21 @@ while IFS= read -r line; do fi # 3. Lowercase Description Check - DESC=$(echo "$COMMIT_MSG" | sed -E "s/^($TYPES)(\(.+\))?:\s+//") - if [[ ! "$DESC" =~ ^[a-z] ]]; then - ERRORS="${ERRORS} ↳ Description must start with a lowercase letter\n" + CAPTURE_REGEX="^($TYPES)(\(.+\))?(!?)?:[[:space:]]+(.+)" + + if [[ "$COMMIT_MSG" =~ $CAPTURE_REGEX ]]; then + DESC="${BASH_REMATCH[4]}" + + if [[ ! "$DESC" =~ ^[a-z] ]]; then + ERRORS="${ERRORS} ↳ Description must start with a lowercase letter\n" + fi + else + : fi # If any errors were found, add them to the report if [ -n "$ERRORS" ]; then FAILED_COUNT=$((FAILED_COUNT + 1)) - # Append the formatted error message for this commit FAILED_COMMITS_REPORT="${FAILED_COMMITS_REPORT}- [\`${COMMIT_SHA}\`] \`${COMMIT_MSG}\`\n${ERRORS}" fi done <<<"$COMMITS" @@ -70,15 +82,12 @@ if [ "$FAILED_COUNT" -eq 0 ]; then else MESSAGE="❌ **Commit Messages:** $FAILED_COUNT of $TOTAL_COMMITS commit(s) failed validation" STATUS="failure" - # Construct a detailed report for failed commits REPORT="Expected format: \`type(scope): description\` (max $MAX_LENGTH chars)\n" REPORT+="Valid types: $TYPES\n\n" REPORT+="Failed commits:\n" REPORT+="$FAILED_COMMITS_REPORT" fi -# Write the outputs to the GITHUB_OUTPUT file for the workflow to use -# The '<>"$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/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/scripts/check-json-files.sh b/.github/actions/config-checks/scripts/check-json-files.sh similarity index 100% rename from .github/scripts/check-json-files.sh rename to .github/actions/config-checks/scripts/check-json-files.sh diff --git a/.github/scripts/check-toml-files.sh b/.github/actions/config-checks/scripts/check-toml-files.sh similarity index 100% rename from .github/scripts/check-toml-files.sh rename to .github/actions/config-checks/scripts/check-toml-files.sh diff --git a/.github/scripts/check-yaml-files.sh b/.github/actions/config-checks/scripts/check-yaml-files.sh similarity index 100% rename from .github/scripts/check-yaml-files.sh rename to .github/actions/config-checks/scripts/check-yaml-files.sh 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/scripts/check-prettier.sh b/.github/actions/frontend-checks/scripts/check-prettier.sh similarity index 100% rename from .github/scripts/check-prettier.sh rename to .github/actions/frontend-checks/scripts/check-prettier.sh diff --git a/.github/scripts/parse-eslint-json.sh b/.github/actions/frontend-checks/scripts/parse-eslint-json.sh similarity index 100% rename from .github/scripts/parse-eslint-json.sh rename to .github/actions/frontend-checks/scripts/parse-eslint-json.sh 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/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/scripts/parse-mypy-output.sh b/.github/actions/python-checks/scripts/parse-mypy-output.sh similarity index 100% rename from .github/scripts/parse-mypy-output.sh rename to .github/actions/python-checks/scripts/parse-mypy-output.sh diff --git a/.github/scripts/setup-pyproject.sh b/.github/actions/python-checks/scripts/setup-pyproject.sh similarity index 100% rename from .github/scripts/setup-pyproject.sh rename to .github/actions/python-checks/scripts/setup-pyproject.sh 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/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 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 }} 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 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 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 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 或快取)可能無法完全運作,但對於驗證程式碼品質檢查通常已足夠。