Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions .github/ISSUE_TEMPLATE.md

This file was deleted.

111 changes: 43 additions & 68 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ jobs:
- name: Format Check
run: dotnet format Honua.sln --verify-no-changes --verbosity diagnostic

test-unit:
name: Unit Tests
test-all:
name: All Tests (Unit + Integration)
runs-on: ubuntu-latest
needs: build
needs: build # Only wait for build, run in parallel with architecture tests
timeout-minutes: 12
steps:
- uses: actions/checkout@v4

Expand All @@ -47,89 +48,56 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore
run: dotnet restore Honua.sln

- name: Run Unit Tests
run: |
dotnet test Honua.sln \
--no-restore \
--configuration Release \
--filter "Category!=Integration" \
--logger "trx;LogFileName=unit-results.trx" \
--results-directory ./tests/TestResults

- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
timeout-minutes: 5
with:
name: unit-test-results
path: tests/TestResults/**/*.trx
retention-days: 7

test-integration:
name: Integration Tests (Testcontainers + PostGIS)
runs-on: ubuntu-latest
needs: [build, test-unit, test-architecture, aot-build] # Run after all other tests except LLM
outputs:
needs_integration: ${{ steps.changes.outputs.integration }}
services:
# Testcontainers will manage containers, but we need Docker available
docker:
image: docker:dind
options: --privileged
steps:
- uses: actions/checkout@v4

- name: Detect Integration Test Scope
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
integration:
- 'src/**'
- 'tests/**'
- 'docker/**'
- 'Dockerfile'
- 'docker-compose.yml'
- '**/*.csproj'
- 'Directory.Build.props'
- 'Honua.sln'
- '.github/workflows/**'
- 'scripts/**'

- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Cache NuGet packages
uses: actions/cache@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Restore
run: dotnet restore Honua.sln

- name: Run Integration Tests
if: steps.changes.outputs.integration == 'true'
- name: Create Test Results Directory
run: mkdir -p ./tests/TestResults

- name: Pre-pull Docker images for faster tests
run: |
docker pull postgis/postgis:16-3.4-alpine || true

- name: Run All Tests (Unit + Integration)
timeout-minutes: 10
run: |
dotnet test Honua.sln \
--no-restore \
--configuration Release \
--filter "Category=Integration" \
--logger "trx;LogFileName=integration-results.trx" \
--results-directory ./tests/TestResults
--filter "Category=Unit|Category=Integration" \
--logger "trx;LogFileName=all-test-results.trx" \
--logger "console;verbosity=minimal" \
--results-directory ./tests/TestResults \
--blame-hang-timeout 8m \
--collect:"XPlat Code Coverage" \
--settings tests/coverlet.runsettings \
-- RunConfiguration.MaxCpuCount=0
env:
TESTCONTAINERS_RYUK_DISABLED: false
TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX: ""

- name: Upload Test Results
uses: actions/upload-artifact@v4
if: steps.changes.outputs.integration == 'true'
if: always()
timeout-minutes: 3
with:
name: integration-test-results
name: test-results
path: tests/TestResults/
retention-days: 7

test-architecture:
name: Architecture Tests
runs-on: ubuntu-latest
needs: build
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

Expand All @@ -141,14 +109,20 @@ jobs:
- name: Restore
run: dotnet restore Honua.sln

- name: Create Test Results Directory
run: mkdir -p ./tests/TestResults

- name: Run Architecture Tests
timeout-minutes: 8
run: |
dotnet test Honua.sln \
--no-restore \
--configuration Release \
--filter "Category=Architecture" \
--logger "trx;LogFileName=architecture-results.trx" \
--results-directory ./tests/TestResults
--logger "console;verbosity=normal" \
--results-directory ./tests/TestResults \
--blame-hang-timeout 5m

- name: Upload Architecture Test Results
uses: actions/upload-artifact@v4
Expand All @@ -162,7 +136,7 @@ jobs:
llm-architecture-review:
name: LLM Architecture Review
runs-on: ubuntu-latest
needs: [build, test-unit, test-architecture, aot-build] # Run in parallel with integration tests
needs: [build, test-all, test-architecture, aot-build] # Run after core tests complete
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
outputs:
assessment: ${{ steps.llm-analysis.outputs.assessment }}
Expand Down Expand Up @@ -299,7 +273,8 @@ jobs:
aot-build:
name: AOT Build Verification
runs-on: ubuntu-latest
needs: build
needs: build # Run in parallel with tests
timeout-minutes: 8
steps:
- uses: actions/checkout@v4

Expand Down
140 changes: 140 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: PR Validation

on:
pull_request:
types: [opened, edited, synchronize, reopened]

jobs:
validate-pr:
name: Validate PR Template Compliance
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: read
steps:
- name: Check PR Template Compliance
uses: actions/github-script@v8
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});

const body = pr.body || '';
const title = pr.title || '';

console.log('Validating PR:', title);
console.log('PR body length:', body.length);

const issues = [];
const warnings = [];

// Check 1: Issue link requirement
const hasIssueLink = /(?:Fixes|Closes|Resolves|Related to)\s+#\d+/i.test(body);
if (!hasIssueLink) {
issues.push('❌ **Missing issue link** - Please include "Fixes #123" or similar');
}

// Check 2: Summary section
const summaryMatch = body.match(/## Summary\s*(?:\s*<!--.*?-->\s*)?\n\s*(.*?)\n/s);
if (!summaryMatch || !summaryMatch[1].trim()) {
issues.push('❌ **Empty summary** - Please describe what this PR does');
}

// Check 3: Changes Made section
const changesMatch = body.match(/## Changes Made\s*(?:\s*<!--.*?-->\s*)?\n(.*?)(?=\n##|\n---|\n$)/s);
if (!changesMatch || !changesMatch[1].includes('-') || changesMatch[1].trim().length < 10) {
issues.push('❌ **Changes Made incomplete** - Please list key changes with bullet points');
}

// Check 4: Testing checklist
const testingChecked = body.match(/- \[x\]/i);
if (!testingChecked) {
warnings.push('⚠️ **No testing boxes checked** - Please indicate testing completed');
}

// Check 5: Pre-PR checklist
const preChecklistComplete = body.match(/## Pre-PR Checklist[\s\S]*?- \[x\].*scripts\/pre-pr-check\.sh/i);
if (!preChecklistComplete) {
issues.push('❌ **Pre-PR validation not confirmed** - Please run `scripts/pre-pr-check.sh` and check the box');
}

// Check 6: Breaking changes addressed
const hasBreakingSection = body.includes('## Breaking Changes');
if (!hasBreakingSection) {
warnings.push('⚠️ **Missing Breaking Changes section** - Template may be outdated');
}

// Check 7: Commit message format (check title)
const titleFormat = /^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\([^)]+\))?: .+( \(#\d+\))?$/i;
if (!titleFormat.test(title)) {
issues.push('❌ **PR title format** - Should be "type: description (#issue)" (e.g., "feat: add login endpoint (#123)")');
}

// Generate comment
let comment = '## 🤖 PR Template Validation\n\n';

if (issues.length === 0 && warnings.length === 0) {
comment += '✅ **All checks passed!** Your PR follows the template correctly.\n\n';
comment += '**Next steps:**\n';
comment += '- Wait for CI to complete\n';
comment += '- Address any feedback from LLM architecture review\n';
comment += '- Request review from team members\n';
} else {
if (issues.length > 0) {
comment += '### ❌ Issues (must fix)\n\n';
issues.forEach(issue => comment += `${issue}\n\n`);
}

if (warnings.length > 0) {
comment += '### ⚠️ Warnings (recommended fixes)\n\n';
warnings.forEach(warning => comment += `${warning}\n\n`);
}

comment += '### 📚 Helpful Links\n';
comment += '- [PR Template Guide](../blob/trunk/.github/pull_request_template.md)\n';
comment += '- [Pre-PR Validation](../blob/trunk/scripts/pre-pr-check.sh)\n';
comment += '- [Commit Message Format](../blob/trunk/CLAUDE.md#commit-guidelines)\n\n';

comment += '💡 **Tip:** Edit your PR description to address these issues and this check will re-run automatically.\n';
}

comment += '\n---\n*Automated validation powered by GitHub Actions*';

// Check if we already have a validation comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});

const existingComment = comments.find(comment =>
comment.body.includes('🤖 PR Template Validation')
);

if (existingComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: comment
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}

// Set status
if (issues.length > 0) {
core.setFailed(`PR template validation failed: ${issues.length} issues found`);
} else {
console.log(`✅ PR template validation passed (${warnings.length} warnings)`);
}
Loading
Loading