From 3dcf6d61102b2159609040da690b34920d4089f3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 2 Feb 2026 05:28:11 +0530 Subject: [PATCH] Refactor GitHub Actions build workflow to streamline concurrency handling - Removed the push trigger from the build workflow, focusing on pull requests for CI/CD processes. - Simplified concurrency group definition by eliminating fallback options, ensuring clearer management of in-progress builds. These changes enhance the clarity and efficiency of the CI/CD pipeline, aligning it more closely with project needs. --- .github/workflows/build.yml | 4 +- .../workflows/create-develop-to-main-pr.yml | 190 ++++++++++++++++ .github/workflows/typecheck.yml | 51 +++++ eslint.config.js | 206 ++++++++++++++++++ 4 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/create-develop-to-main-pr.yml create mode 100644 .github/workflows/typecheck.yml create mode 100644 eslint.config.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4b9fbf41..c9d3c6d5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,6 @@ name: Build on: - push: - branches: ["*"] pull_request: branches: ["*"] @@ -11,7 +9,7 @@ permissions: pull-requests: read concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: diff --git a/.github/workflows/create-develop-to-main-pr.yml b/.github/workflows/create-develop-to-main-pr.yml new file mode 100644 index 000000000..c3eefc21d --- /dev/null +++ b/.github/workflows/create-develop-to-main-pr.yml @@ -0,0 +1,190 @@ +name: Create PR from Develop to Main + +on: + push: + branches: + - develop + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: develop-to-main-pr + cancel-in-progress: false + +jobs: + create-pr: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch tags and branches + run: | + git fetch origin --tags + git fetch origin main develop + + - name: Check if push is from sync workflow + id: check-sync + run: | + # Check if the latest commit on develop is the same as main (indicating a sync operation) + # When sync-main-to-develop runs, it fast-forwards develop to match main's HEAD + DEVELOP_HEAD=$(git rev-parse origin/develop 2>/dev/null || echo "") + MAIN_HEAD=$(git rev-parse origin/main 2>/dev/null || echo "") + + if [ -z "$DEVELOP_HEAD" ] || [ -z "$MAIN_HEAD" ]; then + echo "Could not determine branch heads. Proceeding with PR check." + echo "is_sync=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Develop HEAD: $DEVELOP_HEAD" + echo "Main HEAD: $MAIN_HEAD" + + # If develop HEAD is the same as main HEAD, this is a sync operation and we should skip + if [ "$DEVELOP_HEAD" = "$MAIN_HEAD" ]; then + echo "Latest commit on develop is the same as main. This is a sync operation from sync-main-to-develop workflow. Skipping PR creation." + echo "is_sync=true" >> $GITHUB_OUTPUT + else + echo "This is not a sync operation. Proceeding with PR check." + echo "is_sync=false" >> $GITHUB_OUTPUT + fi + + - name: Check for existing PR + if: steps.check-sync.outputs.is_sync != 'true' + id: check-pr + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pulls } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + base: 'main', + head: `${context.repo.owner}:develop`, + state: 'open' + }); + + if (pulls.length > 0) { + core.setOutput('exists', 'true'); + core.setOutput('number', pulls[0].number.toString()); + console.log(`Existing PR found: #${pulls[0].number}. Skipping PR creation.`); + } else { + core.setOutput('exists', 'false'); + console.log('No existing PR found'); + } + + - name: Check if develop is ahead of main + id: check-diff + if: steps.check-sync.outputs.is_sync != 'true' && steps.check-pr.outputs.exists == 'false' + run: | + # Get the commit counts + DEVELOP_COMMITS=$(git rev-list --count origin/main..origin/develop) + MAIN_COMMITS=$(git rev-list --count origin/develop..origin/main) + + echo "Commits in develop ahead of main: $DEVELOP_COMMITS" + echo "Commits in main ahead of develop: $MAIN_COMMITS" + + if [ "$DEVELOP_COMMITS" -eq 0 ]; then + echo "develop is not ahead of main. No PR needed." + echo "ahead=false" >> $GITHUB_OUTPUT + else + echo "develop is ahead of main by $DEVELOP_COMMITS commits. Creating PR..." + echo "ahead=true" >> $GITHUB_OUTPUT + fi + + - name: Calculate next version + id: next-version + if: steps.check-sync.outputs.is_sync != 'true' && steps.check-pr.outputs.exists == 'false' && steps.check-diff.outputs.ahead == 'true' + run: | + # Get the latest version tag (format: v1.2.3) + LATEST_TAG=$(git tag -l "v*" | sort -V | tail -n 1 || echo "") + + if [ -z "$LATEST_TAG" ]; then + # No tags found, start with v1.0.0 + NEXT_VERSION="v1.0.0" + LATEST_TAG="(none)" + else + # Extract version number (remove 'v' prefix) + VERSION="${LATEST_TAG#v}" + + # Split into major.minor.patch + IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" + + # Ensure we have valid numbers (default to 0 if empty) + MAJOR=${MAJOR:-0} + MINOR=${MINOR:-0} + PATCH=${PATCH:-0} + + # Increment minor version and reset patch to 0 + MINOR=$((MINOR + 1)) + PATCH=0 + + NEXT_VERSION="v${MAJOR}.${MINOR}.${PATCH}" + fi + + echo "Latest tag: $LATEST_TAG" + echo "Predicted next version: $NEXT_VERSION" + echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT + + - name: Generate PR description + id: pr-description + if: steps.check-sync.outputs.is_sync != 'true' && steps.check-pr.outputs.exists == 'false' && steps.check-diff.outputs.ahead == 'true' + run: | + cat < pr_body.txt + ## Overview + + This PR automatically merges changes from develop into main. This merge will trigger a tag action that will bump the minor version to ${{ steps.next-version.outputs.version }}. + + ## Notes + + This PR was automatically created by GitHub Actions. Please review the changes before merging. + EOF + + - name: Create pull request + if: steps.check-sync.outputs.is_sync != 'true' && steps.check-pr.outputs.exists == 'false' && steps.check-diff.outputs.ahead == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const prBody = fs.readFileSync('pr_body.txt', 'utf8'); + const nextVersion = '${{ steps.next-version.outputs.version }}'; + + const { data: pr } = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `chore: merge develop into main (${nextVersion})`, + head: 'develop', + base: 'main', + body: prBody + }); + + // Add labels + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['chore', 'type: chore'] + }); + + // Request review from senamakel + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + reviewers: ['senamakel'] + }); + + console.log(`Created PR #${pr.number} with version ${nextVersion} and requested review from senamakel`); diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 000000000..219a8fd67 --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,51 @@ +name: Type Check + +on: + push: + branches: ['*'] + pull_request: + branches: ['*'] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + typecheck: + name: Type Check TypeScript + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Node.js 24.x + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: 'yarn' + + - name: Cache node modules + id: yarn-cache + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-typecheck-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-typecheck- + ${{ runner.os }}-test- + ${{ runner.os }}-build- + + - name: Install dependencies + if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + + - name: Type check TypeScript files + run: npx tsc + env: + NODE_ENV: test diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..4e0cf1d0e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,206 @@ +// ESLint flat config for ESLint 9+ +// This config is compatible with Prettier and won't conflict with formatting rules + +const js = require('@eslint/js'); +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsparser = require('@typescript-eslint/parser'); +const reactPlugin = require('eslint-plugin-react'); +const reactHooksPlugin = require('eslint-plugin-react-hooks'); +const importPlugin = require('eslint-plugin-import'); +const prettierConfig = require('eslint-config-prettier'); + +module.exports = [ + // Base recommended rules + js.configs.recommended, + + // Ignore patterns + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'coverage/**', + 'src-tauri/**', + 'skills/**', + '*.config.js', + '*.config.ts', + 'vitest.config.ts', + 'tsconfig.tsbuildinfo', + ], + }, + + // Browser environment globals + { + files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx'], + languageOptions: { + globals: { + // Browser globals + window: 'readonly', + document: 'readonly', + navigator: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + fetch: 'readonly', + AbortSignal: 'readonly', + // Node.js globals (for Vite/node polyfills) + require: 'readonly', + process: 'readonly', + Buffer: 'readonly', + global: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + module: 'readonly', + exports: 'readonly', + }, + }, + }, + + // TypeScript files configuration + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + plugins: { + '@typescript-eslint': tseslint, + import: importPlugin, + }, + rules: { + // Disable base no-unused-vars in favor of TypeScript version + 'no-unused-vars': 'off', + // TypeScript recommended rules (disable base JS rules that TypeScript handles) + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_|^[A-Z_]+$', // Ignore _prefixed vars and ALL_CAPS (enum members) + caughtErrorsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + + // Import/export rules + // Note: import/order is disabled to let Prettier handle import sorting + // ESLint still checks for other import issues + 'import/order': 'off', // Prettier plugin handles import sorting + 'import/no-unresolved': 'off', // TypeScript handles this + 'import/no-cycle': 'warn', + 'import/no-duplicates': 'error', // Prevent duplicate imports + + // General JavaScript/TypeScript rules + 'no-console': 'off', // Allow console in frontend code + 'no-debugger': 'error', + 'no-duplicate-imports': 'error', + 'no-unused-expressions': 'off', // Covered by @typescript-eslint version + '@typescript-eslint/no-unused-expressions': 'error', + + // Code quality + 'prefer-const': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-arrow-callback': 'error', + + // Style: Enforce single-line statements on same line without braces when possible + curly: ['error', 'multi', 'consistent'], // Allow single-line without braces, require braces only for multi-statement blocks + 'nonblock-statement-body-position': ['error', 'beside'], // Enforce single-line statements on same line (prevents braces on single-line) + }, + }, + + // React files configuration + { + files: ['**/*.jsx', '**/*.tsx'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + plugins: { + react: reactPlugin, + 'react-hooks': reactHooksPlugin, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + ...reactPlugin.configs.recommended.rules, + ...reactHooksPlugin.configs.recommended.rules, + 'react/react-in-jsx-scope': 'off', // Not needed in React 17+ + 'react/prop-types': 'off', // TypeScript handles prop validation + 'react/display-name': 'off', // Not needed with TypeScript + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, + + // Vitest test files and test setup files (must come after TypeScript config to override rules) + { + files: [ + '**/*.test.ts', + '**/*.test.tsx', + '**/*.spec.ts', + '**/*.spec.tsx', + '**/__tests__/**/*.ts', + '**/__tests__/**/*.tsx', + ], + languageOptions: { + globals: { + // Vitest globals + describe: 'readonly', + it: 'readonly', + test: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + vi: 'readonly', + vitest: 'readonly', + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', // Allow any in tests + '@typescript-eslint/no-non-null-assertion': 'off', // Allow non-null assertions in tests + 'no-undef': 'off', // Vitest provides globals + }, + }, + + // JavaScript files configuration + { + files: ['**/*.js', '**/*.jsx'], + languageOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + rules: { + 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + 'no-console': 'off', + 'no-debugger': 'error', + 'prefer-const': 'error', + 'no-var': 'error', + }, + }, + + // Disable all Prettier-conflicting rules (must be last) + prettierConfig, +];