diff --git a/.forgejo/workflows/README.md b/.forgejo/workflows/README.md new file mode 100644 index 0000000..8d48a16 --- /dev/null +++ b/.forgejo/workflows/README.md @@ -0,0 +1,85 @@ +# Forgejo CI/CD Workflows + +This directory contains automated workflows for the Great Lakes Ice Report project. + +## Workflows + +### CI (ci.yml) +Runs on every push to main and on all pull requests. Includes: +- **Lint**: Checks code style with ESLint +- **Type Check**: Validates TypeScript types +- **Test**: Runs Jest tests on Node.js 18 and 20 +- **Build**: Verifies all build outputs (backend, frontend, CSS) +- **Security**: Checks for hardcoded secrets and vulnerabilities +- **i18n Validation**: Ensures translation files are valid and complete + +### Code Quality (code-quality.yml) +Runs on pull requests to analyze code quality: +- Complexity analysis +- Detection of console.log statements +- TODO/FIXME comment tracking +- Large file detection +- Import analysis and circular dependency checks + +### Dependency Review (dependency-review.yml) +Triggered when package.json or package-lock.json changes: +- Identifies major version updates +- Security vulnerability scanning +- Bundle size impact analysis + +### PR Labeler (pr-labeler.yml) +Automatically suggests labels based on: +- Changed file paths +- PR title and description keywords +- Type of changes (bug, feature, security, etc.) + +### Release (release.yml) +Triggered on version tags (v*): +- Runs full test suite +- Builds the project +- Generates changelog +- Creates release archive + +## Running Workflows Locally + +You can test workflows locally using [act](https://github.com/nektos/act): + +```bash +# Run all workflows +act + +# Run specific workflow +act -W .forgejo/workflows/ci.yml + +# Run specific job +act -j lint -W .forgejo/workflows/ci.yml +``` + +## Workflow Status Badges + +Add these to your README: + +```markdown +[![CI](https://git.deco.sh/deco/ice/actions/workflows/ci.yml/badge.svg)](https://git.deco.sh/deco/ice/actions/workflows/ci.yml) +[![Code Quality](https://git.deco.sh/deco/ice/actions/workflows/code-quality.yml/badge.svg)](https://git.deco.sh/deco/ice/actions/workflows/code-quality.yml) +``` + +## Best Practices + +1. **Keep workflows fast**: Use caching and parallel jobs +2. **Fail fast**: Put quick checks (lint, type-check) before slow ones (tests) +3. **Be specific**: Use path filters to avoid unnecessary runs +4. **Cache dependencies**: Always use `actions/setup-node` with cache +5. **Security first**: Never commit secrets, always use repository secrets + +## Troubleshooting + +### Workflow not running? +- Check if Forgejo Actions is enabled in repository settings +- Verify workflow syntax with online YAML validators +- Check runner availability + +### Tests failing in CI but passing locally? +- Ensure Node.js versions match +- Check for missing environment variables +- Verify database initialization in CI environment \ No newline at end of file diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..087f9f5 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,214 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + name: Lint Code + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + type-check: + runs-on: ubuntu-latest + name: TypeScript Type Check + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run TypeScript compiler + run: npx tsc --noEmit + + test: + runs-on: ubuntu-latest + name: Run Tests + + strategy: + matrix: + node-version: [18, 20] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests with coverage + run: npm run test:coverage + + - name: Upload coverage reports + if: matrix.node-version == '20' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + + build: + runs-on: ubuntu-latest + name: Build Project + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build TypeScript + run: npm run build:ts + + - name: Build Frontend + run: npm run build:frontend + + - name: Build CSS + run: npm run build-css + + - name: Verify build outputs + run: | + echo "Checking backend build..." + test -f dist/server.js || exit 1 + + echo "Checking frontend build..." + test -f public/dist/app-main.js || exit 1 + test -f public/dist/app-admin.js || exit 1 + test -f public/dist/app-privacy.js || exit 1 + + echo "Checking CSS build..." + test -f public/style.css || exit 1 + + echo "✅ All build outputs verified!" + + security: + runs-on: ubuntu-latest + name: Security Checks + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run npm audit + run: npm audit --audit-level=high + continue-on-error: true + + - name: Check for secrets + run: | + echo "Checking for potential secrets..." + ! grep -r "MAPBOX_ACCESS_TOKEN" --include="*.js" --include="*.ts" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git . || \ + (echo "❌ Found hardcoded Mapbox token!" && exit 1) + + ! grep -r "ADMIN_PASSWORD" --include="*.js" --include="*.ts" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git . || \ + (echo "❌ Found hardcoded admin password!" && exit 1) + + echo "✅ No hardcoded secrets found" + + validate-i18n: + runs-on: ubuntu-latest + name: Validate i18n Files + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Validate JSON files + run: | + echo "Validating i18n JSON files..." + for file in src/i18n/locales/*.json; do + echo "Checking $file..." + node -e "JSON.parse(require('fs').readFileSync('$file', 'utf8'))" || exit 1 + done + echo "✅ All i18n files are valid JSON" + + - name: Check translation keys match + run: | + echo "Comparing translation keys..." + node -e " + const fs = require('fs'); + const en = JSON.parse(fs.readFileSync('src/i18n/locales/en.json', 'utf8')); + const esMX = JSON.parse(fs.readFileSync('src/i18n/locales/es-MX.json', 'utf8')); + + function getKeys(obj, prefix = '') { + let keys = []; + for (const key in obj) { + const fullKey = prefix ? prefix + '.' + key : key; + if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + keys = keys.concat(getKeys(obj[key], fullKey)); + } else { + keys.push(fullKey); + } + } + return keys.sort(); + } + + const enKeys = getKeys(en); + const esMXKeys = getKeys(esMX); + + const missingInEs = enKeys.filter(k => !esMXKeys.includes(k)); + const missingInEn = esMXKeys.filter(k => !enKeys.includes(k)); + + if (missingInEs.length > 0) { + console.error('❌ Keys in en.json missing from es-MX.json:', missingInEs); + process.exit(1); + } + + if (missingInEn.length > 0) { + console.error('❌ Keys in es-MX.json missing from en.json:', missingInEn); + process.exit(1); + } + + console.log('✅ All translation keys match between locales'); + " \ No newline at end of file diff --git a/.forgejo/workflows/code-quality.yml b/.forgejo/workflows/code-quality.yml new file mode 100644 index 0000000..6bf530d --- /dev/null +++ b/.forgejo/workflows/code-quality.yml @@ -0,0 +1,141 @@ +name: Code Quality + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + code-quality: + runs-on: ubuntu-latest + name: Code Quality Checks + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for better analysis + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check code complexity + run: | + echo "Analyzing code complexity..." + npx -y complexity-report src/**/*.ts src/**/*.js --format json > complexity.json || true + + node -e " + try { + const report = JSON.parse(require('fs').readFileSync('complexity.json', 'utf8')); + console.log('\\n📊 Code Complexity Report:'); + + const files = report.reports || []; + const complex = files.filter(f => f.aggregate?.cyclomatic > 10); + + if (complex.length > 0) { + console.log('\\n⚠️ Files with high complexity (>10):'); + complex.forEach(f => { + console.log(\` - \${f.path}: Cyclomatic complexity = \${f.aggregate.cyclomatic}\`); + }); + } else { + console.log('✅ All files have acceptable complexity'); + } + } catch (e) { + console.log('ℹ️ Complexity analysis not available'); + } + " + + - name: Check for console.log statements + run: | + echo "Checking for console.log statements..." + FILES=$(grep -r "console\.log" --include="*.ts" --include="*.js" \ + --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=public/dist \ + --exclude-dir=tests --exclude-dir=scripts \ + src/ || true) + + if [ -n "$FILES" ]; then + echo "⚠️ Found console.log statements (consider using proper logging):" + echo "$FILES" + else + echo "✅ No console.log statements in source code" + fi + + - name: Check for TODO/FIXME comments + run: | + echo "Checking for TODO/FIXME comments..." + TODOS=$(grep -r "TODO\|FIXME\|HACK\|XXX" --include="*.ts" --include="*.js" \ + --exclude-dir=node_modules --exclude-dir=dist \ + . || true) + + if [ -n "$TODOS" ]; then + echo "📝 Found TODO/FIXME comments:" + echo "$TODOS" + echo "" + echo "ℹ️ Consider creating issues for these items" + else + echo "✅ No TODO/FIXME comments found" + fi + + - name: Check for large files + run: | + echo "Checking for large files..." + LARGE_FILES=$(find . -type f -size +1M \ + -not -path "./node_modules/*" \ + -not -path "./.git/*" \ + -not -path "./dist/*" \ + -not -path "./coverage/*" \ + -not -name "*.db" \ + -not -name "package-lock.json") + + if [ -n "$LARGE_FILES" ]; then + echo "⚠️ Found large files (>1MB):" + echo "$LARGE_FILES" | xargs -I {} sh -c 'echo " - {} ($(du -h {} | cut -f1))"' + echo "" + echo "Consider if these files should be in the repository" + else + echo "✅ No large files detected" + fi + + - name: Check TypeScript strict mode + run: | + echo "Verifying TypeScript strict mode..." + STRICT=$(grep -E '"strict":\s*true' tsconfig.json) + + if [ -n "$STRICT" ]; then + echo "✅ TypeScript strict mode is enabled" + else + echo "⚠️ Consider enabling TypeScript strict mode for better type safety" + fi + + - name: Analyze import statements + run: | + echo "Analyzing imports..." + + # Check for circular dependencies + npx -y madge --circular --extensions ts,js src/ || true + + # Check for unused exports + echo "" + echo "Checking for potentially unused exports..." + npx -y ts-unused-exports tsconfig.json --excludePathsFromReport=src/types || true + + - name: Generate PR comment + if: always() + run: | + echo "## 🔍 Code Quality Report" > pr-comment.md + echo "" >> pr-comment.md + echo "All automated code quality checks have been run. Please review the logs above for details." >> pr-comment.md + echo "" >> pr-comment.md + echo "### Checklist" >> pr-comment.md + echo "- [ ] ESLint passes" >> pr-comment.md + echo "- [ ] TypeScript compiles without errors" >> pr-comment.md + echo "- [ ] Tests pass" >> pr-comment.md + echo "- [ ] No high complexity code" >> pr-comment.md + echo "- [ ] No hardcoded secrets" >> pr-comment.md + echo "" >> pr-comment.md + echo "_This comment was generated automatically by the Code Quality workflow._" >> pr-comment.md \ No newline at end of file diff --git a/.forgejo/workflows/dependency-review.yml b/.forgejo/workflows/dependency-review.yml new file mode 100644 index 0000000..ab5d5e0 --- /dev/null +++ b/.forgejo/workflows/dependency-review.yml @@ -0,0 +1,112 @@ +name: Dependency Review + +on: + pull_request: + paths: + - 'package.json' + - 'package-lock.json' + +jobs: + dependency-review: + runs-on: ubuntu-latest + name: Review Dependencies + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Check for major version changes + run: | + echo "Checking for major dependency updates..." + git fetch origin main + + # Get the package.json from main branch + git show origin/main:package.json > package-main.json + + # Compare dependencies + node -e " + const fs = require('fs'); + const mainPkg = JSON.parse(fs.readFileSync('package-main.json', 'utf8')); + const currentPkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + + function compareDeps(mainDeps = {}, currentDeps = {}, type) { + console.log(\`\\nChecking \${type}:\`); + let hasChanges = false; + + for (const [pkg, currentVer] of Object.entries(currentDeps)) { + const mainVer = mainDeps[pkg]; + if (!mainVer) { + console.log(\` ✅ Added: \${pkg}@\${currentVer}\`); + hasChanges = true; + } else if (mainVer !== currentVer) { + const mainMajor = mainVer.match(/\\d+/)?.[0]; + const currentMajor = currentVer.match(/\\d+/)?.[0]; + + if (mainMajor && currentMajor && mainMajor !== currentMajor) { + console.log(\` ⚠️ Major update: \${pkg} \${mainVer} → \${currentVer}\`); + } else { + console.log(\` 📦 Updated: \${pkg} \${mainVer} → \${currentVer}\`); + } + hasChanges = true; + } + } + + for (const [pkg, mainVer] of Object.entries(mainDeps)) { + if (!currentDeps[pkg]) { + console.log(\` ❌ Removed: \${pkg}@\${mainVer}\`); + hasChanges = true; + } + } + + if (!hasChanges) { + console.log(\` No changes\`); + } + } + + compareDeps(mainPkg.dependencies, currentPkg.dependencies, 'dependencies'); + compareDeps(mainPkg.devDependencies, currentPkg.devDependencies, 'devDependencies'); + " + + - name: Check for security advisories + run: | + npm audit --json > audit.json || true + node -e " + const audit = JSON.parse(require('fs').readFileSync('audit.json', 'utf8')); + const vulns = audit.metadata?.vulnerabilities || {}; + + console.log('\\nSecurity Audit Summary:'); + console.log(' Critical:', vulns.critical || 0); + console.log(' High:', vulns.high || 0); + console.log(' Moderate:', vulns.moderate || 0); + console.log(' Low:', vulns.low || 0); + + if (vulns.critical > 0 || vulns.high > 0) { + console.error('\\n❌ Found critical or high severity vulnerabilities!'); + process.exit(1); + } + " + + - name: Check bundle size impact + run: | + echo "Analyzing bundle size impact..." + + # Install dependencies from main + git show origin/main:package-lock.json > package-lock-main.json + npm ci --package-lock-only --package-lock=package-lock-main.json + npm run build:frontend || true + du -sh public/dist > size-main.txt + + # Install current dependencies + npm ci + npm run build:frontend + du -sh public/dist > size-current.txt + + echo "Bundle size comparison:" + echo "Main branch: $(cat size-main.txt)" + echo "This branch: $(cat size-current.txt)" \ No newline at end of file diff --git a/.forgejo/workflows/pr-labeler.yml b/.forgejo/workflows/pr-labeler.yml new file mode 100644 index 0000000..f88ebdc --- /dev/null +++ b/.forgejo/workflows/pr-labeler.yml @@ -0,0 +1,99 @@ +name: PR Labeler + +on: + pull_request: + types: [opened, edited, synchronize] + +jobs: + label: + runs-on: ubuntu-latest + name: Label Pull Request + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Analyze and label PR + run: | + echo "Analyzing PR for automatic labeling..." + + # Get changed files + git fetch origin main + CHANGED_FILES=$(git diff --name-only origin/main...HEAD) + + # Initialize labels array + LABELS="" + + # Check file types and paths + if echo "$CHANGED_FILES" | grep -q "^src/.*\.ts$"; then + LABELS="$LABELS,backend" + fi + + if echo "$CHANGED_FILES" | grep -q "^src/frontend/.*\.ts$"; then + LABELS="$LABELS,frontend" + fi + + if echo "$CHANGED_FILES" | grep -q "^public/.*\.\(js\|html\)$"; then + LABELS="$LABELS,frontend" + fi + + if echo "$CHANGED_FILES" | grep -q "^src/scss/.*\.scss$"; then + LABELS="$LABELS,styles" + fi + + if echo "$CHANGED_FILES" | grep -q "^tests/.*\.test\.ts$"; then + LABELS="$LABELS,tests" + fi + + if echo "$CHANGED_FILES" | grep -q "^\.forgejo/workflows/"; then + LABELS="$LABELS,ci/cd" + fi + + if echo "$CHANGED_FILES" | grep -q "package.*\.json$"; then + LABELS="$LABELS,dependencies" + fi + + if echo "$CHANGED_FILES" | grep -q "^docs/\|README\.md\|CLAUDE\.md"; then + LABELS="$LABELS,documentation" + fi + + if echo "$CHANGED_FILES" | grep -q "^src/i18n/"; then + LABELS="$LABELS,i18n" + fi + + if echo "$CHANGED_FILES" | grep -q "^scripts/"; then + LABELS="$LABELS,tooling" + fi + + # Check PR title/body for keywords + PR_TITLE="${{ github.event.pull_request.title }}" + PR_BODY="${{ github.event.pull_request.body }}" + + if echo "$PR_TITLE $PR_BODY" | grep -qi "security\|vulnerability\|CVE"; then + LABELS="$LABELS,security" + fi + + if echo "$PR_TITLE $PR_BODY" | grep -qi "performance\|optimize\|speed"; then + LABELS="$LABELS,performance" + fi + + if echo "$PR_TITLE $PR_BODY" | grep -qi "bug\|fix\|issue"; then + LABELS="$LABELS,bug" + fi + + if echo "$PR_TITLE $PR_BODY" | grep -qi "feature\|enhancement\|add"; then + LABELS="$LABELS,enhancement" + fi + + if echo "$PR_TITLE $PR_BODY" | grep -qi "breaking change\|BREAKING"; then + LABELS="$LABELS,breaking-change" + fi + + # Remove leading comma and duplicates + LABELS=$(echo "$LABELS" | sed 's/^,//' | tr ',' '\n' | sort -u | tr '\n' ',' | sed 's/,$//') + + echo "Suggested labels: $LABELS" + + # Note: In actual Forgejo/Gitea, you would use the API to apply labels + # This is just for demonstration + echo "To apply labels, use: tea pr edit ${{ github.event.pull_request.number }} --add-label \"$LABELS\"" \ No newline at end of file diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..2f5dc38 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,99 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + name: Create Release + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build project + run: npm run build + + - name: Generate changelog + run: | + echo "# Changelog" > CHANGELOG_CURRENT.md + echo "" >> CHANGELOG_CURRENT.md + + # Get the previous tag + PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + CURRENT_TAG="${{ github.ref_name }}" + + if [ -n "$PREV_TAG" ]; then + echo "## Changes since $PREV_TAG" >> CHANGELOG_CURRENT.md + echo "" >> CHANGELOG_CURRENT.md + + # Group commits by type + echo "### Features" >> CHANGELOG_CURRENT.md + git log $PREV_TAG..HEAD --grep="feat:" --pretty=format:"- %s" >> CHANGELOG_CURRENT.md || true + echo "" >> CHANGELOG_CURRENT.md + + echo "### Bug Fixes" >> CHANGELOG_CURRENT.md + git log $PREV_TAG..HEAD --grep="fix:" --pretty=format:"- %s" >> CHANGELOG_CURRENT.md || true + echo "" >> CHANGELOG_CURRENT.md + + echo "### Other Changes" >> CHANGELOG_CURRENT.md + git log $PREV_TAG..HEAD --grep -v "feat:\|fix:" --pretty=format:"- %s" >> CHANGELOG_CURRENT.md || true + else + echo "## Initial Release" >> CHANGELOG_CURRENT.md + git log --pretty=format:"- %s" >> CHANGELOG_CURRENT.md + fi + + - name: Create release archive + run: | + # Create a release archive excluding unnecessary files + tar -czf "ice-report-${{ github.ref_name }}.tar.gz" \ + --exclude=node_modules \ + --exclude=.git \ + --exclude=.env \ + --exclude=*.db \ + --exclude=coverage \ + --exclude=.forgejo \ + . + + - name: Create release notes + run: | + echo "# Release ${{ github.ref_name }}" > RELEASE_NOTES.md + echo "" >> RELEASE_NOTES.md + echo "## Installation" >> RELEASE_NOTES.md + echo "" >> RELEASE_NOTES.md + echo '```bash' >> RELEASE_NOTES.md + echo "wget https://git.deco.sh/deco/ice/releases/download/${{ github.ref_name }}/ice-report-${{ github.ref_name }}.tar.gz" >> RELEASE_NOTES.md + echo "tar -xzf ice-report-${{ github.ref_name }}.tar.gz" >> RELEASE_NOTES.md + echo "cd ice-report" >> RELEASE_NOTES.md + echo "npm install" >> RELEASE_NOTES.md + echo "npm run build" >> RELEASE_NOTES.md + echo '```' >> RELEASE_NOTES.md + echo "" >> RELEASE_NOTES.md + cat CHANGELOG_CURRENT.md >> RELEASE_NOTES.md + + # Note: In actual Forgejo/Gitea, you would use their release API + # This is a placeholder showing what would be done + - name: Display release information + run: | + echo "Release ${{ github.ref_name }} is ready!" + echo "Archive: ice-report-${{ github.ref_name }}.tar.gz" + echo "" + echo "Release notes:" + cat RELEASE_NOTES.md \ No newline at end of file