diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..b4b9fbf41 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,62 @@ +name: Build + +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: + test-build: + name: Build Tauri App + runs-on: ubuntu-22.04 + 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: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu + + - name: Install Tauri dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Cache node modules + id: yarn-cache + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install dependencies + if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + + - name: Build frontend + run: yarn build + + - name: Build Tauri app + run: yarn tauri build -- --target x86_64-unknown-linux-gnu + env: + NODE_ENV: production diff --git a/.github/workflows/pr-protection.yml b/.github/workflows/pr-protection.yml new file mode 100644 index 000000000..aaabe6ef3 --- /dev/null +++ b/.github/workflows/pr-protection.yml @@ -0,0 +1,176 @@ +name: PR Protection - Main Branch + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + pull-requests: write + contents: read + +jobs: + check-pr-protection: + runs-on: ubuntu-latest + + steps: + - name: Check PR target branch + id: check-target + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const baseBranch = pr.base.ref; + const headBranch = pr.head.ref; + const prAuthor = pr.user.login; + const prTitle = pr.title; + const prBody = pr.body || ''; + + // Check if PR is targeting main or master + const isTargetingMain = baseBranch === 'main' || baseBranch === 'master'; + + if (!isTargetingMain) { + console.log(`PR is targeting ${baseBranch}, not main/master. Skipping protection check.`); + core.setOutput('needs_check', 'false'); + return; + } + + console.log(`PR #${pr.number} is targeting ${baseBranch}`); + console.log(`PR author: ${prAuthor}`); + console.log(`PR title: ${prTitle}`); + console.log(`Head branch: ${headBranch}`); + + // Check if PR title follows conventional commit format + // Allowed prefixes: feat, fix, chore, refactor, docs, test, style, perf, revert + const conventionalCommitPattern = /^(feat|fix|chore|refactor|docs|test|style|perf|revert)(\(.+\))?: .+/; + const followsConventionalFormat = conventionalCommitPattern.test(prTitle); + + console.log(`Conventional commit format check: ${followsConventionalFormat ? 'PASSED' : 'FAILED'}`); + if (!followsConventionalFormat) { + console.log(`PR title must follow conventional commit format: : `); + console.log(`Allowed types: feat, fix, chore, refactor, docs, test, style, perf, revert`); + } + + // Check if this is the automated PR from create-develop-to-main-pr workflow + // The automated PR has these characteristics: + // 1. Created by github-actions[bot] + // 2. From develop branch to main + // 3. Title matches pattern: "chore: merge develop into main (vX.Y.Z)" + // 4. Body contains "This PR automatically merges changes from develop into main" + + // Validate PR title matches expected pattern: "chore: merge develop into main" followed by optional version + const expectedTitlePattern = /^chore: merge develop into main( \(v\d+\.\d+\.\d+\))?$/; + const titleMatches = expectedTitlePattern.test(prTitle); + + console.log(`Automated PR title validation: ${titleMatches ? 'PASSED' : 'FAILED'}`); + console.log(`Expected pattern: "chore: merge develop into main" or "chore: merge develop into main (vX.Y.Z)"`); + + const isAutomatedPR = + prAuthor === 'github-actions[bot]' && + headBranch === 'develop' && + titleMatches && + prBody.includes('This PR automatically merges changes from develop into main'); + + if (isAutomatedPR) { + console.log('This PR is from the automated workflow. Allowing it.'); + core.setOutput('needs_check', 'false'); + core.setOutput('is_allowed', 'true'); + } else { + console.log('This PR is NOT from the automated workflow.'); + + // Even if not automated, check if title follows conventional commit format + if (!followsConventionalFormat) { + console.log('PR title does not follow conventional commit format. Blocking it.'); + core.setOutput('needs_check', 'true'); + core.setOutput('is_allowed', 'false'); + core.setOutput('conventional_format_failed', 'true'); + } else { + console.log('PR title follows conventional commit format, but PR is not from automated workflow. Blocking it.'); + core.setOutput('needs_check', 'true'); + core.setOutput('is_allowed', 'false'); + core.setOutput('conventional_format_failed', 'false'); + } + } + + // Store the conventional format check result + core.setOutput('conventional_format', followsConventionalFormat ? 'true' : 'false'); + + - name: Block unauthorized PR to main + if: steps.check-target.outputs.needs_check == 'true' && steps.check-target.outputs.is_allowed == 'false' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const baseBranch = pr.base.ref; + const prAuthor = pr.user.login; + const prTitle = pr.title; + const headBranch = pr.head.ref; + const prBody = pr.body || ''; + + // Check which validation criteria failed + const conventionalCommitPattern = /^(feat|fix|chore|refactor|docs|test|style|perf|revert)(\(.+\))?: .+/; + const followsConventionalFormat = conventionalCommitPattern.test(prTitle); + + const checks = { + author: prAuthor === 'github-actions[bot]', + branch: headBranch === 'develop', + automatedTitle: /^chore: merge develop into main( \(v\d+\.\d+\.\d+\))?$/.test(prTitle), + body: prBody.includes('This PR automatically merges changes from develop into main'), + conventionalFormat: followsConventionalFormat + }; + + const failedChecks = Object.entries(checks) + .filter(([_, passed]) => !passed) + .map(([check]) => check); + + console.log('Validation checks:', JSON.stringify(checks, null, 2)); + console.log('Failed checks:', failedChecks.join(', ')); + + // Build detailed error message + let errorDetails = ''; + if (failedChecks.length > 0) { + errorDetails = `\n\n**Failed validation checks:**\n`; + if (!checks.author) errorDetails += `- ❌ Author must be \`github-actions[bot]\` (found: \`${prAuthor}\`)\n`; + if (!checks.branch) errorDetails += `- ❌ Head branch must be \`develop\` (found: \`${headBranch}\`)\n`; + if (!checks.automatedTitle) errorDetails += `- ❌ Title must match pattern: \`chore: merge develop into main\` or \`chore: merge develop into main (vX.Y.Z)\` (found: \`${prTitle}\`)\n`; + if (!checks.body) errorDetails += `- ❌ Body must contain: "This PR automatically merges changes from develop into main"\n`; + if (!checks.conventionalFormat) errorDetails += `- ❌ PR title must follow conventional commit format: \`: \`\n`; + if (!checks.conventionalFormat) errorDetails += ` Allowed types: \`feat\`, \`fix\`, \`chore\`, \`refactor\`, \`docs\`, \`test\`, \`style\`, \`perf\`, \`revert\`\n`; + if (!checks.conventionalFormat) errorDetails += ` Example: \`feat: add new authentication endpoint\`\n`; + } + + // Comment on the PR explaining why it was blocked + const codeBlock = '`'; + const commentBody = [ + '## PR Protection Rule Violation', + '', + `This PR targets the ${codeBlock}${baseBranch}${codeBlock} branch, which is protected.`, + '', + `**Only automated PRs from the ${codeBlock}create-develop-to-main-pr.yml${codeBlock} workflow are allowed to target ${codeBlock}${baseBranch}${codeBlock}.**${errorDetails}`, + '', + `**To merge changes into ${codeBlock}${baseBranch}${codeBlock}:**`, + `1. Create your PR targeting the ${codeBlock}develop${codeBlock} branch instead`, + `2. Once merged to ${codeBlock}develop${codeBlock}, the automated workflow will create a PR from ${codeBlock}develop${codeBlock} to ${codeBlock}${baseBranch}${codeBlock}`, + '', + `Please update this PR to target ${codeBlock}develop${codeBlock} instead, or close it and create a new PR targeting ${codeBlock}develop${codeBlock}.` + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: commentBody + }); + + // Fail the workflow to block the PR + let failureMessage = `PR #${pr.number} cannot target ${baseBranch} branch. `; + + if (!checks.conventionalFormat) { + failureMessage += `PR title does not follow conventional commit format. `; + } + + failureMessage += `Failed checks: ${failedChecks.join(', ')}. `; + failureMessage += `Only automated PRs from create-develop-to-main-pr.yml workflow are allowed.`; + + core.setFailed(failureMessage); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..e82113793 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Test + +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: + test: + name: Run Unit Tests + 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 }}-test-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-test- + ${{ runner.os }}-build- + + - name: Install dependencies + if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + + - name: Run tests with coverage + run: yarn test:coverage + env: + NODE_ENV: test + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage + retention-days: 7 diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 000000000..82e70d5db --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,71 @@ +name: Version Bump + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + version-bump: + runs-on: ubuntu-latest + environment: Production + # Skip if this is a version bump commit to avoid infinite loop + if: "!contains(github.event.head_commit.message, 'chore: bump version') && !contains(github.event.head_commit.message, '[skip ci]')" + + steps: + - name: Generate GitHub App token + id: app-token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.XGITHUB_APP_ID }} + private_key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }} + + - name: Checkout code + uses: actions/checkout@v4 + with: + # Use GitHub App token for checkout and push operations + # This token has bypass permissions if the GitHub App is configured with them + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Configure Git + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + # Update remote URL to use GitHub App token for all git operations + git remote set-url origin https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git + + - name: Pull latest changes + run: | + # Ensure we're on main and pull latest changes to avoid push conflicts + git checkout main + git pull origin main --ff-only + + - name: Bump version + id: version + run: | + # Get current version + CURRENT_VERSION=$(node -p "require('./package.json').version") + echo "Current version: $CURRENT_VERSION" + + # Bump minor version (creates commit and tag) + # The commit message includes [skip ci] to prevent triggering this workflow again + NEW_VERSION=$(npm version minor -m "chore: bump version to %s [skip ci]" | sed 's/v//') + echo "New version: $NEW_VERSION" + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Push changes + run: | + # Push to main and tags using GitHub App token (remote URL already configured) + git push origin main + git push origin --tags diff --git a/skills b/skills index 87bb3adab..1c3082aca 160000 --- a/skills +++ b/skills @@ -1 +1 @@ -Subproject commit 87bb3adab1286e3923b26ba9407c0a2af6ad4069 +Subproject commit 1c3082aca47e19e89b19bb58a8ec122597d7c269