diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9681ee11..f328a74c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: # Python Backend Tests - All Platforms # -------------------------------------------------------------------------- test-python: - name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + name: test-python (${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: @@ -98,7 +98,7 @@ jobs: # Frontend Tests - All Platforms # -------------------------------------------------------------------------- test-frontend: - name: Frontend on ${{ matrix.os }} + name: test-frontend (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: @@ -151,7 +151,7 @@ jobs: # Platform-Specific Integration Tests # -------------------------------------------------------------------------- test-platform-integration: - name: Platform Integration Tests on ${{ matrix.os }} + name: test-integration (${{ matrix.os }}) runs-on: ${{ matrix.os }} # Only run integration tests after basic tests pass diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 989eaec5..1b09de7b 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -64,9 +64,7 @@ jobs: // Label definitions LABELS: Object.freeze({ SIZE: ['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL'], - AREA: ['area/frontend', 'area/backend', 'area/fullstack', 'area/ci'], - STATUS: ['🔄 Checking', '✅ Ready for Review', '❌ Checks Failed'], - REVIEW: ['Missing AC Approval', 'AC: Approved', 'AC: Changes Requested', 'AC: Needs Re-review'] + AREA: ['area/frontend', 'area/backend', 'area/fullstack', 'area/ci'] }), // Pagination @@ -230,7 +228,6 @@ jobs: const pr = context.payload.pull_request; const prNumber = pr.number; const title = pr.title || ''; - const isNewPR = context.payload.action === 'opened' || context.payload.action === 'reopened'; console.log(`::group::PR #${prNumber} - Auto-labeling`); console.log(`Title: ${title.slice(0, 100)}${title.length > 100 ? '...' : ''}`); @@ -271,25 +268,9 @@ jobs: CONFIG.LABELS.SIZE.filter(l => l !== sizeLabel).forEach(l => labelsToRemove.add(l)); console.log(` 📏 Size: ${sizeLabel} (${totalLines} lines)`); - // 4. Set status label (only on new PRs - let pr-status-gate handle updates on pushes) - // Note: On synchronize events, CI workflows will trigger pr-status-gate when they complete - if (isNewPR) { - labelsToAdd.add('🔄 Checking'); - CONFIG.LABELS.STATUS.filter(l => l !== '🔄 Checking').forEach(l => labelsToRemove.add(l)); - console.log(` 🔄 Status: Checking`); - } else { - console.log(` â„šī¸ Status: Unchanged (will be updated by pr-status-gate)`); - } - - // 5. Add review label for new PRs only - if (isNewPR) { - labelsToAdd.add('Missing AC Approval'); - console.log(` âŗ Review: Missing AC Approval`); - } - console.log('::endgroup::'); - // 6. Apply label changes + // 4. Apply label changes console.log(`::group::Applying labels`); // Remove labels that should be replaced (exclude ones we're adding) @@ -302,7 +283,7 @@ jobs: console.log('::endgroup::'); console.log(`✅ PR #${prNumber} labeled successfully`); - // 7. Write job summary + // 5. Write job summary const summaryType = type ? CONFIG.TYPE_MAP[type] || 'unknown' : 'none'; const summaryArea = areaLabel ? areaLabel.replace('area/', '') : 'other'; @@ -312,9 +293,7 @@ jobs: [{ data: 'Category', header: true }, { data: 'Label', header: true }], ['Type', summaryType], ['Area', summaryArea], - ['Size', sizeLabel], - ['Status', isNewPR ? '🔄 Checking' : '(unchanged)'], - ['Review', isNewPR ? 'Missing AC Approval' : '(unchanged)'] + ['Size', sizeLabel] ]) .addRaw(`\n**Files:** ${files.length} | **Lines:** +${pr.additions || 0} / -${pr.deletions || 0}\n`) .write(); diff --git a/.github/workflows/pr-status-gate.yml b/.github/workflows/pr-status-gate.yml deleted file mode 100644 index 69cb9bd5..00000000 --- a/.github/workflows/pr-status-gate.yml +++ /dev/null @@ -1,585 +0,0 @@ -name: PR Status Gate - -on: - workflow_run: - workflows: [CI, Lint, Quality Security] - types: [completed] - - issue_comment: - types: [created, edited] - - pull_request: - types: [synchronize] - -concurrency: - group: pr-status-gate-${{ github.event.workflow_run.pull_requests[0].number || github.event.issue.number || github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -permissions: - pull-requests: write - checks: read - -env: - # Shared configuration - single source of truth - REQUIRED_CHECKS: | - CI / test-frontend - CI / test-python (3.12) - CI / test-python (3.13) - Lint / python - Quality Security / CodeQL (javascript-typescript) - Quality Security / CodeQL (python) - Quality Security / Python Security (Bandit) - Quality Security / Security Summary - -jobs: - # ═══════════════════════════════════════════════════════════════════════════ - # JOB 1: CI STATUS (triggered by workflow_run) - # Updates CI status labels when monitored workflows complete - # ═══════════════════════════════════════════════════════════════════════════ - update-ci-status: - name: Update CI Status - runs-on: ubuntu-latest - if: github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0] != null - timeout-minutes: 5 - - steps: - - name: Check all required checks and update label - uses: actions/github-script@v7 - env: - REQUIRED_CHECKS: ${{ env.REQUIRED_CHECKS }} - with: - retries: 3 - retry-exempt-status-codes: 400,401,403,404,422 - script: | - // NOTE: STATUS_LABELS is intentionally duplicated across jobs. - // GitHub Actions jobs run in isolated contexts and cannot share runtime constants. - // If label values change, update ALL occurrences: update-ci-status, check-status-command - const STATUS_LABELS = Object.freeze({ - CHECKING: '🔄 Checking', - PASSED: '✅ Ready for Review', - FAILED: '❌ Checks Failed' - }); - - const REQUIRED_CHECKS = process.env.REQUIRED_CHECKS - .split('\n') - .map(s => s.trim()) - .filter(Boolean); - - async function fetchCheckRuns(sha) { - const { owner, repo } = context.repo; - // Let the configured retries (retries: 3) handle transient failures - // Don't catch errors - allow them to propagate for retry logic - const checkRuns = await github.paginate( - github.rest.checks.listForRef, - { owner, repo, ref: sha, per_page: 100 }, - (response) => response.data - ); - return checkRuns; - } - - function analyzeChecks(checkRuns) { - const results = []; - let allComplete = true; - let anyFailed = false; - - for (const checkName of REQUIRED_CHECKS) { - const check = checkRuns.find(c => c.name === checkName); - - if (!check) { - results.push({ name: checkName, status: 'âŗ Pending', complete: false }); - allComplete = false; - } else if (check.status !== 'completed') { - results.push({ name: checkName, status: '🔄 Running', complete: false }); - allComplete = false; - } else if (check.conclusion === 'success') { - results.push({ name: checkName, status: '✅ Passed', complete: true }); - } else if (check.conclusion === 'skipped') { - results.push({ name: checkName, status: 'â­ī¸ Skipped', complete: true, skipped: true }); - } else { - results.push({ name: checkName, status: '❌ Failed', complete: true, failed: true }); - anyFailed = true; - } - } - return { allComplete, anyFailed, results }; - } - - async function updateStatusLabels(prNumber, newLabel) { - const { owner, repo } = context.repo; - const allLabels = Object.values(STATUS_LABELS); - - // Remove all status labels first - throw on non-404 errors to prevent conflicting labels - for (const label of allLabels) { - try { - await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: label }); - } catch (e) { - if (e && e.status !== 404) { - // Throw to prevent adding new label if removal failed (could cause conflicting labels) - throw new Error(`Failed to remove label '${label}': ${e.message}`); - } - } - } - - try { - await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: [newLabel] }); - } catch (e) { - if (e && e.status === 404) { - core.warning(`Label '${newLabel}' does not exist`); - } else { - throw e; - } - } - } - - // Main logic - const prNumber = context.payload.workflow_run.pull_requests[0].number; - const headSha = context.payload.workflow_run.head_sha; - const triggerWorkflow = context.payload.workflow_run.name; - - console.log(`PR #${prNumber} - Triggered by: ${triggerWorkflow}, SHA: ${headSha.slice(0, 8)}`); - - const checkRuns = await fetchCheckRuns(headSha); - console.log(`Found ${checkRuns.length} check runs`); - const { allComplete, anyFailed, results } = analyzeChecks(checkRuns); - - for (const r of results) { - console.log(` ${r.status} ${r.name}`); - } - - if (!allComplete) { - const pending = results.filter(r => !r.complete).length; - console.log(`âŗ ${pending}/${REQUIRED_CHECKS.length} checks pending`); - // Update to CHECKING status if checks are still running (prevents stale Ready/Failed status) - await updateStatusLabels(prNumber, STATUS_LABELS.CHECKING); - return; - } - - const newLabel = anyFailed ? STATUS_LABELS.FAILED : STATUS_LABELS.PASSED; - await updateStatusLabels(prNumber, newLabel); - - const passedCount = results.filter(r => r.status === '✅ Passed').length; - const failedCount = results.filter(r => r.failed).length; - - if (anyFailed) { - console.log(`❌ PR #${prNumber}: ${failedCount} check(s) failed`); - } else { - console.log(`✅ PR #${prNumber}: Ready for review (${passedCount}/${REQUIRED_CHECKS.length} passed)`); - } - - # ═══════════════════════════════════════════════════════════════════════════ - # JOB 2: /check-status COMMAND - # Manual status check - anyone can trigger by commenting /check-status - # ═══════════════════════════════════════════════════════════════════════════ - check-status-command: - name: Check Status Command - runs-on: ubuntu-latest - if: | - github.event_name == 'issue_comment' && - github.event.issue.pull_request && - contains(github.event.comment.body, '/check-status') - timeout-minutes: 5 - - steps: - - name: Run status check and post report - uses: actions/github-script@v7 - env: - REQUIRED_CHECKS: ${{ env.REQUIRED_CHECKS }} - with: - retries: 3 - retry-exempt-status-codes: 400,401,403,404,422 - script: | - // NOTE: STATUS_LABELS is intentionally duplicated across jobs. - // GitHub Actions jobs run in isolated contexts and cannot share runtime constants. - // If label values change, update ALL occurrences: update-ci-status, check-status-command - const STATUS_LABELS = Object.freeze({ - CHECKING: '🔄 Checking', - PASSED: '✅ Ready for Review', - FAILED: '❌ Checks Failed' - }); - - // NOTE: REVIEW_LABELS is intentionally duplicated across jobs. - // If label values change, update ALL occurrences: check-status-command, update-review-status - const REVIEW_LABELS = Object.freeze([ - 'Missing AC Approval', - 'AC: Approved', - 'AC: Changes Requested', - 'AC: Blocked', - 'AC: Needs Re-review', - 'AC: Reviewed' - ]); - - const REQUIRED_CHECKS = process.env.REQUIRED_CHECKS - .split('\n') - .map(s => s.trim()) - .filter(Boolean); - - const { owner, repo } = context.repo; - const prNumber = context.payload.issue.number; - const requestedBy = context.payload.comment.user.login; - - // Get PR details - const { data: pr } = await github.rest.pulls.get({ - owner, repo, pull_number: prNumber - }); - const headSha = pr.head.sha; - - console.log(`PR #${prNumber} - /check-status by @${requestedBy}, SHA: ${headSha.slice(0, 8)}`); - - // Fetch check runs with pagination to handle >100 checks - const checkRuns = await github.paginate( - github.rest.checks.listForRef, - { owner, repo, ref: headSha, per_page: 100 }, - (response) => response.data - ); - console.log(`Found ${checkRuns.length} check runs`); - - // Analyze results - const results = []; - let allComplete = true; - let anyFailed = false; - - for (const checkName of REQUIRED_CHECKS) { - const check = checkRuns.find(c => c.name === checkName); - - if (!check) { - results.push({ name: checkName, emoji: 'âŗ', complete: false }); - allComplete = false; - } else if (check.status !== 'completed') { - results.push({ name: checkName, emoji: '🔄', complete: false }); - allComplete = false; - } else if (check.conclusion === 'success') { - results.push({ name: checkName, emoji: '✅', complete: true }); - } else if (check.conclusion === 'skipped') { - results.push({ name: checkName, emoji: 'â­ī¸', complete: true, skipped: true }); - } else { - results.push({ name: checkName, emoji: '❌', complete: true, failed: true }); - anyFailed = true; - } - } - - // Get current labels - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner, repo, issue_number: prNumber - }); - const labelNames = currentLabels.map(l => l.name); - const currentStatusLabel = Object.values(STATUS_LABELS).find(l => labelNames.includes(l)) || 'None'; - const currentReviewLabel = REVIEW_LABELS.find(l => labelNames.includes(l)) || 'None'; - - // Update label if all checks complete - let newStatusLabel = STATUS_LABELS.CHECKING; - let statusChanged = false; - - if (allComplete) { - newStatusLabel = anyFailed ? STATUS_LABELS.FAILED : STATUS_LABELS.PASSED; - - if (newStatusLabel !== currentStatusLabel) { - statusChanged = true; - // Remove all status labels first - throw on non-404 errors to prevent conflicting labels - for (const label of Object.values(STATUS_LABELS)) { - try { - await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: label }); - } catch (e) { - if (e && e.status !== 404) { - throw new Error(`Failed to remove label '${label}': ${e.message}`); - } - } - } - await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: [newStatusLabel] }); - } - } - - // Build status report - const passedCount = results.filter(r => r.emoji === '✅').length; - let statusEmoji = '🔄'; - if (allComplete && !anyFailed) statusEmoji = '✅'; - else if (allComplete && anyFailed) statusEmoji = '❌'; - - const checksTable = results.map(r => `| ${r.emoji} | ${r.name} |`).join('\n'); - - const lines = [ - `## ${statusEmoji} PR Status Report`, - '', - `| Label | Value |`, - `|-------|-------|`, - `| CI Status | ${newStatusLabel} |`, - `| AC Review | ${currentReviewLabel} |`, - '' - ]; - - if (statusChanged) { - lines.push(`> Status updated: \`${currentStatusLabel}\` → \`${newStatusLabel}\``); - lines.push(''); - } - - lines.push(`### CI Checks (${passedCount}/${REQUIRED_CHECKS.length} passed)`); - lines.push(''); - lines.push('| Status | Check |'); - lines.push('|--------|-------|'); - lines.push(checksTable); - lines.push(''); - lines.push('---'); - lines.push(`Triggered by \`/check-status\` from @${requestedBy}`); - - await github.rest.issues.createComment({ - owner, repo, issue_number: prNumber, body: lines.join('\n') - }); - - console.log(`✅ Posted status report to PR #${prNumber}`); - - # ═══════════════════════════════════════════════════════════════════════════ - # JOB 3: AUTO-CLAUDE REVIEW - # Processes Auto-Claude review comments from trusted sources - # Security: Only bots and collaborators can update labels - # ═══════════════════════════════════════════════════════════════════════════ - update-review-status: - name: Update Review Status - runs-on: ubuntu-latest - if: | - github.event_name == 'issue_comment' && - github.event.issue.pull_request && - !contains(github.event.comment.body, '/check-status') - timeout-minutes: 5 - - steps: - - name: Check for Auto-Claude review - uses: actions/github-script@v7 - with: - retries: 3 - retry-exempt-status-codes: 400,401,403,404,422 - script: | - // Security configuration - // SECURITY: Only [bot] suffixed accounts are protected by GitHub. - // Regular usernames can be registered by anyone and are NOT trusted. - const TRUSTED_BOT_ACCOUNTS = Object.freeze([ - 'github-actions[bot]', - 'auto-claude[bot]' - ]); - - const TRUSTED_AUTHOR_ASSOCIATIONS = Object.freeze([ - 'COLLABORATOR', - 'MEMBER', - 'OWNER' - ]); - - const IDENTIFIER_PATTERNS = Object.freeze([ - '🤖 Auto Claude PR Review', - 'Auto Claude Review', - 'Auto-Claude Review' - ]); - - // SECURITY: Regex patterns are tightened to prevent false matches - // Using \s* instead of .* and requiring specific emoji + verdict format - const VERDICTS = Object.freeze({ - APPROVED: { - patterns: ['Auto Claude Review - APPROVED', '✅ Auto Claude Review - APPROVED'], - // Match: "Merge Verdict:" followed by whitespace/emoji, then ✅, then APPROVED/READY TO MERGE - regex: /Merge Verdict:\s*✅\s*(?:APPROVED|READY TO MERGE)/i, - label: 'AC: Approved' - }, - CHANGES_REQUESTED: { - patterns: ['NEEDS REVISION', 'Needs Revision'], - // Match: "Merge Verdict:" followed by whitespace/emoji, then 🟠 - regex: /Merge Verdict:\s*🟠/, - label: 'AC: Changes Requested' - }, - BLOCKED: { - patterns: ['BLOCKED'], - // Match: "Merge Verdict:" followed by whitespace/emoji, then 🔴 - regex: /Merge Verdict:\s*🔴/, - label: 'AC: Blocked' - } - }); - - // NOTE: REVIEW_LABELS is intentionally duplicated across jobs. - // GitHub Actions jobs run in isolated contexts and cannot share runtime constants. - // If label values change, update ALL occurrences: check-status-command, update-review-status - const REVIEW_LABELS = Object.freeze([ - 'Missing AC Approval', - 'AC: Approved', - 'AC: Changes Requested', - 'AC: Blocked', - 'AC: Needs Re-review', - 'AC: Reviewed' - ]); - - // Helper functions - // SECURITY: Verify both username AND account type to prevent spoofing - function isTrustedBot(username, userType) { - const isKnownBot = TRUSTED_BOT_ACCOUNTS.some(t => username.toLowerCase() === t.toLowerCase()); - // Only trust if it's a known bot account AND GitHub confirms it's a Bot type - return isKnownBot && userType === 'Bot'; - } - - function isTrustedAssociation(assoc) { - return TRUSTED_AUTHOR_ASSOCIATIONS.includes(assoc); - } - - function isAutoClaudeComment(body) { - return IDENTIFIER_PATTERNS.some(p => body.includes(p)); - } - - function parseVerdict(body) { - const safeBody = body.slice(0, 5000); - for (const [key, config] of Object.entries(VERDICTS)) { - const patternMatch = config.patterns.some(p => safeBody.includes(p)); - const regexMatch = config.regex && config.regex.test(safeBody); - if (patternMatch || regexMatch) { - return { verdict: key, label: config.label }; - } - } - return null; - } - - async function updateReviewLabels(prNumber, newLabel) { - const { owner, repo } = context.repo; - - // Remove all review labels first - throw on non-404 errors to prevent conflicting labels - for (const label of REVIEW_LABELS) { - try { - await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: label }); - console.log(` Removed: ${label}`); - } catch (e) { - if (e && e.status !== 404) { - // Throw to prevent adding new label if removal failed (could cause conflicting labels) - throw new Error(`Failed to remove label '${label}': ${e.message}`); - } - } - } - - try { - await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: [newLabel] }); - console.log(` Added: ${newLabel}`); - } catch (e) { - if (e && e.status === 404) { - core.warning(`Label '${newLabel}' does not exist`); - } else { - throw e; - } - } - } - - // Main logic - const prNumber = context.payload.issue.number; - const comment = context.payload.comment; - const commenter = comment.user.login; - const commenterType = comment.user.type; - const authorAssociation = comment.author_association; - const body = comment.body || ''; - - console.log(`PR #${prNumber} - Comment by: ${commenter} (type: ${commenterType}, assoc: ${authorAssociation})`); - - // Security checks - // SECURITY: Bot status requires BOTH username match AND verified Bot type - const isBot = isTrustedBot(commenter, commenterType); - const isCollaborator = isTrustedAssociation(authorAssociation); - const isACComment = isAutoClaudeComment(body); - - console.log(` Trusted bot: ${isBot}, Collaborator: ${isCollaborator}, AC comment: ${isACComment}`); - - if (!isBot && !isCollaborator) { - console.log('Skipping: Not a trusted bot or collaborator'); - return; - } - - if (!isACComment) { - console.log('Skipping: Not an Auto-Claude comment'); - return; - } - - const verdictResult = parseVerdict(body); - if (!verdictResult) { - console.log('Skipping: Could not parse verdict'); - return; - } - - console.log(`Verdict: ${verdictResult.verdict} → ${verdictResult.label}`); - await updateReviewLabels(prNumber, verdictResult.label); - console.log(`✅ PR #${prNumber} review status updated`); - - # ═══════════════════════════════════════════════════════════════════════════ - # JOB 4: RE-REVIEW ON PUSH - # When new commits pushed after AC approval, require re-review - # ═══════════════════════════════════════════════════════════════════════════ - require-re-review: - name: Require Re-review on Push - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.action == 'synchronize' - timeout-minutes: 5 - - steps: - - name: Check and reset AC approval if needed - uses: actions/github-script@v7 - with: - retries: 3 - retry-exempt-status-codes: 400,401,403,404,422 - script: | - const { owner, repo } = context.repo; - const prNumber = context.payload.pull_request.number; - const pusher = context.payload.sender.login; - - console.log(`PR #${prNumber} - New commits by: ${pusher}`); - - // Get current labels - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ - owner, repo, issue_number: prNumber - }); - const labelNames = labels.map(l => l.name); - - // Check if PR was approved - const wasApproved = labelNames.includes('AC: Approved'); - - if (!wasApproved) { - console.log('PR was not AC-approved, no action needed'); - return; - } - - console.log('PR was AC-approved, resetting to require re-review'); - - // Remove AC: Approved - throw on non-404 errors to prevent conflicting labels - try { - await github.rest.issues.removeLabel({ - owner, repo, issue_number: prNumber, name: 'AC: Approved' - }); - console.log(' Removed: AC: Approved'); - } catch (e) { - if (e && e.status !== 404) { - // Throw to prevent adding 'AC: Needs Re-review' if removal failed (could cause conflicting labels) - core.error(`Failed to remove 'AC: Approved' label: ${e.message}`); - throw e; - } - } - - // Add AC: Needs Re-review - try { - await github.rest.issues.addLabels({ - owner, repo, issue_number: prNumber, labels: ['AC: Needs Re-review'] - }); - console.log(' Added: AC: Needs Re-review'); - } catch (e) { - if (e && e.status === 404) { - core.warning("Label 'AC: Needs Re-review' does not exist"); - } else { - throw e; - } - } - - // Post notification comment - const commentLines = [ - '## 🔄 Re-review Required', - '', - 'New commits were pushed after Auto-Claude approval.', - '', - '| Previous | Current |', - '|----------|---------|', - '| `AC: Approved` | `AC: Needs Re-review` |', - '', - 'Please run Auto-Claude review again or request a manual review.', - '', - '---', - `Triggered by push from @${pusher}` - ]; - - await github.rest.issues.createComment({ - owner, repo, issue_number: prNumber, body: commentLines.join('\n') - }); - - console.log(`✅ Posted re-review notification to PR #${prNumber}`); diff --git a/.github/workflows/test-azure-auth.yml b/.github/workflows/test-azure-auth.yml deleted file mode 100644 index f3fb73c2..00000000 --- a/.github/workflows/test-azure-auth.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Test Azure Auth - -on: - workflow_dispatch: - -jobs: - test-auth: - runs-on: windows-latest - permissions: - id-token: write - contents: read - steps: - - name: Azure Login (OIDC) - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Success - run: echo "Azure authentication successful!" diff --git a/.github/workflows/test-on-tag.yml b/.github/workflows/test-on-tag.yml deleted file mode 100644 index f633c868..00000000 --- a/.github/workflows/test-on-tag.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Test on Tag - -on: - push: - tags: - - 'v*' - -jobs: - # Python tests - test-python: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.12', '3.13'] - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Install dependencies - working-directory: apps/backend - run: | - uv venv - uv pip install -r requirements.txt - uv pip install -r ../../tests/requirements-test.txt - - - name: Run tests - working-directory: apps/backend - env: - PYTHONPATH: ${{ github.workspace }}/apps/backend - run: | - source .venv/bin/activate - pytest ../../tests/ -v --tb=short - - # Frontend tests - test-frontend: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - - - name: Install dependencies - working-directory: apps/frontend - run: npm ci --ignore-scripts - - - name: Run tests - working-directory: apps/frontend - run: npm run test diff --git a/.github/workflows/validate-version.yml b/.github/workflows/validate-version.yml deleted file mode 100644 index a076114d..00000000 --- a/.github/workflows/validate-version.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Validate Version - -on: - push: - tags: - - 'v*' - -jobs: - validate-version: - name: Validate package.json version matches tag - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Extract version from tag - id: tag_version - run: | - # Extract version from tag (e.g., v2.5.5 -> 2.5.5) - TAG_VERSION=${GITHUB_REF#refs/tags/v} - echo "version=$TAG_VERSION" >> $GITHUB_OUTPUT - echo "Tag version: $TAG_VERSION" - - - name: Extract version from package.json - id: package_version - run: | - # Read version from package.json - PACKAGE_VERSION=$(node -p "require('./apps/frontend/package.json').version") - echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT - echo "Package.json version: $PACKAGE_VERSION" - - - name: Compare versions - run: | - TAG_VERSION="${{ steps.tag_version.outputs.version }}" - PACKAGE_VERSION="${{ steps.package_version.outputs.version }}" - - echo "==========================================" - echo "Version Validation" - echo "==========================================" - echo "Git tag version: v$TAG_VERSION" - echo "package.json version: $PACKAGE_VERSION" - echo "==========================================" - - if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then - echo "" - echo "❌ ERROR: Version mismatch detected!" - echo "" - echo "The version in package.json ($PACKAGE_VERSION) does not match" - echo "the git tag version ($TAG_VERSION)." - echo "" - echo "To fix this:" - echo " 1. Delete this tag: git tag -d v$TAG_VERSION" - echo " 2. Update package.json version to $TAG_VERSION" - echo " 3. Commit the change" - echo " 4. Recreate the tag: git tag -a v$TAG_VERSION -m 'Release v$TAG_VERSION'" - echo "" - echo "Or use the automated script:" - echo " node scripts/bump-version.js $TAG_VERSION" - echo "" - exit 1 - fi - - echo "" - echo "✅ SUCCESS: Versions match!" - echo "" - - - name: Version validation result - if: success() - run: | - echo "::notice::Version validation passed - package.json version matches tag v${{ steps.tag_version.outputs.version }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bded7f5c..376eeb67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -429,7 +429,6 @@ All pull requests and pushes to `main` trigger automated CI checks via GitHub Ac |----------|---------|----------------| | **CI** | Push to `main`, PRs | Python tests (3.11 & 3.12), Frontend tests | | **Lint** | Push to `main`, PRs | Ruff (Python), ESLint + TypeScript (Frontend) | -| **Test on Tag** | Version tags (`v*`) | Full test suite before release | ### PR Requirements diff --git a/RELEASE.md b/RELEASE.md index 21d0e6b5..4eb9ff02 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -186,7 +186,6 @@ The release workflow **validates** that `CHANGELOG.md` has an entry for the vers |----------|---------|---------| | `prepare-release.yml` | Push to `main` | Detects version bump, **validates CHANGELOG.md**, creates tag | | `release.yml` | Tag `v*` pushed | Builds binaries, extracts changelog, creates release | -| `validate-version.yml` | Tag `v*` pushed | Validates tag matches package.json | | `update-readme` (in release.yml) | After release | Updates README with new version | ## Troubleshooting