diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f55afd4..a517cc7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,193 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: +# Dependabot configuration for automated dependency updates # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 + +registries: + # Define any private registries here if needed + # npm-registry: + # type: npm-registry + # url: https://npm.pkg.github.com + # token: ${{ secrets.NPM_TOKEN }} + updates: - - package-ecosystem: "" # See documentation for possible values - directory: "/" # Location of package manifests + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + commit-message: + prefix: "ci" + include: "scope" + labels: + - "dependencies" + - "github-actions" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 5 + + # npm/Node.js (if package.json exists) + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "tuesday" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "javascript" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + versioning-strategy: "increase" + groups: + dev-dependencies: + dependency-type: "development" + update-types: + - "minor" + - "patch" + production-dependencies: + dependency-type: "production" + update-types: + - "patch" + + # pip/Python (if requirements.txt or pyproject.toml exists) + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "wednesday" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "python" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + + # Bundler/Ruby (if Gemfile exists) + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "weekly" + day: "thursday" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "ruby" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + + # Go modules (if go.mod exists) + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + day: "friday" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "go" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + + # Cargo/Rust (if Cargo.toml exists) + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "rust" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + + # Docker (if Dockerfile exists) + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "docker" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 5 + + # Composer/PHP (if composer.json exists) + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "php" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + + # NuGet/.NET (if *.csproj exists) + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "dotnet" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + + # Maven/Java (if pom.xml exists) + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "java" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 + + # Gradle/Java (if build.gradle exists) + - package-ecosystem: "gradle" + directory: "/" schedule: - interval: "daily" + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "java" + reviewers: + - "hyperpolymath" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..98ea855 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,331 @@ +name: CI + +on: + push: + branches: [ main, master, develop ] + tags: [ 'v*' ] + pull_request: + branches: [ main, master, develop ] + workflow_dispatch: + inputs: + run_full_test: + description: 'Run full test suite' + required: false + default: 'false' + type: boolean + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CI: true + COVERAGE: true + +jobs: + # Detect project type and configuration + detect: + name: Detect Project Configuration + runs-on: ubuntu-latest + outputs: + languages: ${{ steps.detect.outputs.languages }} + primary_language: ${{ steps.detect.outputs.primary_language }} + package_managers: ${{ steps.detect.outputs.package_managers }} + test_frameworks: ${{ steps.detect.outputs.test_frameworks }} + has_nodejs: ${{ steps.detect.outputs.has_nodejs }} + has_python: ${{ steps.detect.outputs.has_python }} + has_go: ${{ steps.detect.outputs.has_go }} + has_rust: ${{ steps.detect.outputs.has_rust }} + has_docker: ${{ steps.detect.outputs.has_docker }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Make scripts executable + run: chmod +x ci-scripts/*.sh + + - name: Run detection + id: detect + run: | + source ci-scripts/detect.sh + detect_languages + detect_package_managers + detect_test_frameworks + detect_build_systems + + echo "languages=$DETECTED_LANGUAGES" >> $GITHUB_OUTPUT + echo "primary_language=$PRIMARY_LANGUAGE" >> $GITHUB_OUTPUT + echo "package_managers=$DETECTED_PACKAGE_MANAGERS" >> $GITHUB_OUTPUT + echo "test_frameworks=$DETECTED_TEST_FRAMEWORKS" >> $GITHUB_OUTPUT + + # Set boolean flags for conditional jobs + [[ "$DETECTED_LANGUAGES" == *"javascript"* || "$DETECTED_LANGUAGES" == *"typescript"* ]] && echo "has_nodejs=true" >> $GITHUB_OUTPUT || echo "has_nodejs=false" >> $GITHUB_OUTPUT + [[ "$DETECTED_LANGUAGES" == *"python"* ]] && echo "has_python=true" >> $GITHUB_OUTPUT || echo "has_python=false" >> $GITHUB_OUTPUT + [[ "$DETECTED_LANGUAGES" == *"go"* ]] && echo "has_go=true" >> $GITHUB_OUTPUT || echo "has_go=false" >> $GITHUB_OUTPUT + [[ "$DETECTED_LANGUAGES" == *"rust"* ]] && echo "has_rust=true" >> $GITHUB_OUTPUT || echo "has_rust=false" >> $GITHUB_OUTPUT + [[ -f "Dockerfile" ]] && echo "has_docker=true" >> $GITHUB_OUTPUT || echo "has_docker=false" >> $GITHUB_OUTPUT + + # Lint job + lint: + name: Lint + runs-on: ubuntu-latest + needs: detect + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + if: needs.detect.outputs.has_nodejs == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + - name: Setup Python + if: needs.detect.outputs.has_python == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Setup Go + if: needs.detect.outputs.has_go == 'true' + uses: actions/setup-go@v5 + with: + go-version: '1.22' + cache: true + + - name: Setup Rust + if: needs.detect.outputs.has_rust == 'true' + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Install dependencies + run: | + chmod +x ci-scripts/*.sh + ci-scripts/setup.sh + + - name: Run linters + run: ci-scripts/lint.sh + + # Test job with matrix for multiple versions + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: detect + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + include: + - os: ubuntu-latest + node-version: '20' + python-version: '3.12' + go-version: '1.22' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + if: needs.detect.outputs.has_nodejs == 'true' + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + - name: Setup Python + if: needs.detect.outputs.has_python == 'true' + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Setup Go + if: needs.detect.outputs.has_go == 'true' + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Setup Rust + if: needs.detect.outputs.has_rust == 'true' + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + ~/.npm + ~/.cache/pip + ~/go/pkg/mod + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json', '**/requirements*.txt', '**/go.sum', '**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-deps- + + - name: Install dependencies + run: | + chmod +x ci-scripts/*.sh + ci-scripts/setup.sh + + - name: Run tests + run: ci-scripts/test.sh --coverage --ci + + - name: Upload coverage + if: success() && github.event_name == 'push' + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + + # Build job + build: + name: Build + runs-on: ubuntu-latest + needs: [detect, lint, test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + if: needs.detect.outputs.has_nodejs == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + - name: Setup Python + if: needs.detect.outputs.has_python == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup Go + if: needs.detect.outputs.has_go == 'true' + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Setup Rust + if: needs.detect.outputs.has_rust == 'true' + uses: dtolnay/rust-toolchain@stable + + - name: Install dependencies + run: | + chmod +x ci-scripts/*.sh + ci-scripts/setup.sh + + - name: Build + run: ci-scripts/build.sh --release + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + build/ + target/release/ + retention-days: 7 + if-no-files-found: ignore + + # Mirror to GitLab (event-driven push mirroring) + mirror: + name: Mirror to GitLab + runs-on: ubuntu-latest + needs: [build] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup SSH for GitLab + env: + GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + run: | + mkdir -p ~/.ssh + echo "$GITLAB_SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan gitlab.com >> ~/.ssh/known_hosts + + - name: Mirror to GitLab + env: + MIRROR_URL: ${{ secrets.GITLAB_MIRROR_URL }} + run: | + chmod +x ci-scripts/*.sh + ci-scripts/sync-mirror.sh --mirror-url "$MIRROR_URL" --push + + # Docker build (if Dockerfile exists) + docker: + name: Docker Build + runs-on: ubuntu-latest + needs: [detect, build] + if: needs.detect.outputs.has_docker == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Release job (on tags) + release: + name: Release + runs-on: ubuntu-latest + needs: [build] + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: dist/ + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + files: dist/* diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6021e63..07d4d1a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,99 +1,211 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL Advanced" +# CodeQL Security Analysis +# Scans for security vulnerabilities and coding errors +# https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning + +name: "CodeQL Security Analysis" on: push: - branches: [ "main" ] + branches: [ "main", "master", "develop" ] pull_request: - branches: [ "main" ] + branches: [ "main", "master", "develop" ] schedule: - - cron: '23 15 * * 0' + # Run weekly on Sunday at 3:23 AM UTC + - cron: '23 3 * * 0' + workflow_dispatch: + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true jobs: + # First, detect which languages are in the repository + detect-languages: + name: Detect Languages + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.detect.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect languages + id: detect + run: | + LANGUAGES=() + + # Check for JavaScript/TypeScript + if find . -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" | grep -q .; then + LANGUAGES+=('{"language": "javascript-typescript", "build-mode": "none"}') + fi + + # Check for Python + if find . -name "*.py" | grep -q .; then + LANGUAGES+=('{"language": "python", "build-mode": "none"}') + fi + + # Check for Go + if find . -name "*.go" | grep -q .; then + LANGUAGES+=('{"language": "go", "build-mode": "autobuild"}') + fi + + # Check for Ruby + if find . -name "*.rb" | grep -q .; then + LANGUAGES+=('{"language": "ruby", "build-mode": "none"}') + fi + + # Check for Java/Kotlin + if find . -name "*.java" -o -name "*.kt" | grep -q .; then + LANGUAGES+=('{"language": "java-kotlin", "build-mode": "autobuild"}') + fi + + # Check for C/C++ + if find . -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" | grep -q .; then + LANGUAGES+=('{"language": "c-cpp", "build-mode": "autobuild"}') + fi + + # Check for C# + if find . -name "*.cs" | grep -q .; then + LANGUAGES+=('{"language": "csharp", "build-mode": "autobuild"}') + fi + + # Check for Swift + if find . -name "*.swift" | grep -q .; then + LANGUAGES+=('{"language": "swift", "build-mode": "autobuild"}') + fi + + # Always include actions analysis for GitHub workflows + LANGUAGES+=('{"language": "actions", "build-mode": "none"}') + + # Build JSON matrix + if [ ${#LANGUAGES[@]} -gt 0 ]; then + MATRIX=$(printf '%s\n' "${LANGUAGES[@]}" | jq -s '{include: .}') + else + MATRIX='{"include": [{"language": "actions", "build-mode": "none"}]}' + fi + + echo "matrix=$MATRIX" >> $GITHUB_OUTPUT + echo "Detected languages: $MATRIX" + analyze: name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. + needs: detect-languages runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: 360 permissions: - # required for all workflows security-events: write - - # required to fetch internal or private CodeQL packs packages: read - - # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false - matrix: - include: - - language: actions - build-mode: none - # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + matrix: ${{ fromJson(needs.detect-languages.outputs.matrix) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Setup for different languages + - name: Setup Node.js + if: matrix.language == 'javascript-typescript' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Python + if: matrix.language == 'python' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup Go + if: matrix.language == 'go' + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Setup Java + if: matrix.language == 'java-kotlin' + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Setup .NET + if: matrix.language == 'csharp' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + # Initialize CodeQL + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # Enable security-extended for more thorough analysis + queries: security-extended,security-and-quality + + # Manual build for compiled languages if autobuild fails + - name: Build (Manual) + if: matrix.build-mode == 'manual' + shell: bash + run: | + chmod +x ci-scripts/*.sh 2>/dev/null || true + if [ -f "ci-scripts/build.sh" ]; then + ci-scripts/build.sh + else + echo "No build script found, attempting standard builds..." + # Go + if [ -f "go.mod" ]; then + go build ./... + fi + # Java/Maven + if [ -f "pom.xml" ]; then + mvn package -DskipTests -B + fi + # Java/Gradle + if [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then + ./gradlew build -x test 2>/dev/null || gradle build -x test + fi + # .NET + if ls *.csproj 1>/dev/null 2>&1; then + dotnet build + fi + # C/C++ + if [ -f "CMakeLists.txt" ]; then + mkdir -p build && cd build && cmake .. && make + elif [ -f "Makefile" ]; then + make + fi + fi + + # Perform CodeQL Analysis + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{ matrix.language }}" + # Upload SARIF results + upload: always + + # Summary job + security-summary: + name: Security Summary + needs: analyze + runs-on: ubuntu-latest + if: always() steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # πŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - name: Run manual build steps - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + - name: Check analysis results + run: | + echo "## CodeQL Security Analysis Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Security scanning completed. Review the Security tab for detailed findings." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Analyzed Components" >> $GITHUB_STEP_SUMMARY + echo "- GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY + echo "- Source code (detected languages)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "For more details, see:" >> $GITHUB_STEP_SUMMARY + echo "- [Security Advisories](../../security/advisories)" >> $GITHUB_STEP_SUMMARY + echo "- [Code Scanning Alerts](../../security/code-scanning)" >> $GITHUB_STEP_SUMMARY diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..99cb0ae --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,335 @@ +# GitLab CI/CD Configuration +# Uses shared scripts from ci-scripts/ for consistency with GitHub Actions + +stages: + - detect + - lint + - test + - build + - deploy + +# Global settings +default: + image: ubuntu:22.04 + before_script: + - apt-get update -qq + - apt-get install -y -qq git curl + - chmod +x ci-scripts/*.sh + +# Cache configuration +.cache_config: &cache_config + cache: + key: "${CI_COMMIT_REF_SLUG}" + paths: + - node_modules/ + - .npm/ + - .venv/ + - ~/.cache/pip/ + - vendor/ + - target/ + policy: pull-push + +# Detect job - determines project configuration +detect: + stage: detect + script: + - | + source ci-scripts/detect.sh + detect_languages + detect_package_managers + detect_test_frameworks + detect_build_systems + + echo "DETECTED_LANGUAGES=$DETECTED_LANGUAGES" > detect.env + echo "PRIMARY_LANGUAGE=$PRIMARY_LANGUAGE" >> detect.env + echo "DETECTED_PACKAGE_MANAGERS=$DETECTED_PACKAGE_MANAGERS" >> detect.env + echo "DETECTED_TEST_FRAMEWORKS=$DETECTED_TEST_FRAMEWORKS" >> detect.env + + cat detect.env + artifacts: + reports: + dotenv: detect.env + expire_in: 1 hour + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_COMMIT_TAG + +# Node.js lint +lint:nodejs: + stage: lint + image: node:20-slim + <<: *cache_config + needs: + - detect + script: + - npm ci --prefer-offline || npm install + - ci-scripts/lint.sh + rules: + - if: $DETECTED_LANGUAGES =~ /javascript|typescript/ + when: on_success + - when: never + +# Python lint +lint:python: + stage: lint + image: python:3.12-slim + <<: *cache_config + needs: + - detect + script: + - pip install ruff black mypy + - ci-scripts/lint.sh + rules: + - if: $DETECTED_LANGUAGES =~ /python/ + when: on_success + - when: never + +# Go lint +lint:go: + stage: lint + image: golang:1.22 + <<: *cache_config + needs: + - detect + script: + - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + - ci-scripts/lint.sh + rules: + - if: $DETECTED_LANGUAGES =~ /go/ + when: on_success + - when: never + +# Rust lint +lint:rust: + stage: lint + image: rust:latest + <<: *cache_config + needs: + - detect + script: + - rustup component add clippy rustfmt + - ci-scripts/lint.sh + rules: + - if: $DETECTED_LANGUAGES =~ /rust/ + when: on_success + - when: never + +# Shell lint +lint:shell: + stage: lint + image: koalaman/shellcheck-alpine:stable + script: + - shellcheck ci-scripts/*.sh + rules: + - changes: + - ci-scripts/*.sh + when: on_success + - when: never + +# Generic lint (fallback) +lint:generic: + stage: lint + <<: *cache_config + needs: + - detect + script: + - ci-scripts/setup.sh || true + - ci-scripts/lint.sh || echo "No linters configured" + rules: + - if: $DETECTED_LANGUAGES == "" + when: on_success + +# Node.js tests +test:nodejs: + stage: test + image: node:20 + <<: *cache_config + needs: + - detect + - lint:nodejs + script: + - npm ci --prefer-offline || npm install + - ci-scripts/test.sh --coverage --ci + coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' + artifacts: + reports: + junit: junit.xml + coverage_report: + coverage_format: cobertura + path: coverage/cobertura-coverage.xml + expire_in: 1 week + rules: + - if: $DETECTED_LANGUAGES =~ /javascript|typescript/ + when: on_success + - when: never + +# Python tests +test:python: + stage: test + image: python:3.12 + <<: *cache_config + needs: + - detect + - lint:python + script: + - python -m venv .venv + - source .venv/bin/activate + - pip install pytest pytest-cov + - ci-scripts/setup.sh + - ci-scripts/test.sh --coverage --ci + coverage: '/TOTAL.*\s+(\d+%)/' + artifacts: + reports: + junit: pytest-report.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml + expire_in: 1 week + rules: + - if: $DETECTED_LANGUAGES =~ /python/ + when: on_success + - when: never + +# Go tests +test:go: + stage: test + image: golang:1.22 + <<: *cache_config + needs: + - detect + - lint:go + script: + - ci-scripts/test.sh --coverage + coverage: '/coverage: \d+\.\d+% of statements/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + expire_in: 1 week + rules: + - if: $DETECTED_LANGUAGES =~ /go/ + when: on_success + - when: never + +# Rust tests +test:rust: + stage: test + image: rust:latest + <<: *cache_config + needs: + - detect + - lint:rust + script: + - ci-scripts/test.sh + rules: + - if: $DETECTED_LANGUAGES =~ /rust/ + when: on_success + - when: never + +# Generic test (fallback) +test:generic: + stage: test + <<: *cache_config + needs: + - detect + - lint:generic + script: + - ci-scripts/setup.sh || true + - ci-scripts/test.sh || echo "No tests configured" + rules: + - if: $DETECTED_LANGUAGES == "" + when: on_success + +# Build job +build: + stage: build + <<: *cache_config + needs: + - job: test:nodejs + optional: true + - job: test:python + optional: true + - job: test:go + optional: true + - job: test:rust + optional: true + - job: test:generic + optional: true + script: + - ci-scripts/setup.sh || true + - ci-scripts/build.sh --release + artifacts: + paths: + - dist/ + - build/ + - target/release/ + expire_in: 1 week + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_COMMIT_TAG + +# Docker build +build:docker: + stage: build + image: docker:24 + services: + - docker:24-dind + variables: + DOCKER_TLS_CERTDIR: "/certs" + needs: + - build + script: + - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . + - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA + - docker push $CI_REGISTRY_IMAGE:latest + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + exists: + - Dockerfile + +# Mirror sync (event-driven - triggers on push from GitHub) +mirror:verify: + stage: deploy + script: + - ci-scripts/verify-mirror.sh --source origin --mirror gitlab || true + rules: + - if: $CI_PIPELINE_SOURCE == "push" + when: manual + - when: never + +# Release job +release: + stage: deploy + image: registry.gitlab.com/gitlab-org/release-cli:latest + needs: + - build + script: + - echo "Creating release for $CI_COMMIT_TAG" + release: + tag_name: $CI_COMMIT_TAG + name: 'Release $CI_COMMIT_TAG' + description: 'Release created automatically from CI/CD pipeline' + rules: + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ + +# Pages deployment (documentation) +pages: + stage: deploy + image: node:20 + script: + - npm ci --prefer-offline || npm install || true + - npm run docs:build || mkdir -p public + - cp -r docs/.vitepress/dist/* public/ 2>/dev/null || cp -r docs/* public/ 2>/dev/null || echo "No docs to publish" + artifacts: + paths: + - public + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + changes: + - docs/**/* + when: on_success + - when: never diff --git a/README.md b/README.md index 3ad6143..4f29ad5 100644 --- a/README.md +++ b/README.md @@ -1 +1,246 @@ -# universal-project-manager +# Universal Project Manager + +[![CI](https://github.com/hyperpolymath/Universal-Project-Manager/actions/workflows/ci.yml/badge.svg)](https://github.com/hyperpolymath/Universal-Project-Manager/actions/workflows/ci.yml) +[![CodeQL](https://github.com/hyperpolymath/Universal-Project-Manager/actions/workflows/codeql.yml/badge.svg)](https://github.com/hyperpolymath/Universal-Project-Manager/actions/workflows/codeql.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A comprehensive, language-agnostic CI/CD framework that automatically detects project configurations and provides unified scripts for building, testing, and deploying across multiple platforms. + +## Features + +- **Auto-Detection**: Automatically detects programming languages, package managers, test frameworks, and build systems +- **Multi-Language Support**: JavaScript/TypeScript, Python, Ruby, Go, Rust, Java/Kotlin, C/C++, C#, PHP, and more +- **Platform Agnostic**: Works with GitHub Actions and GitLab CI out of the box +- **Mirror Sync**: Event-driven repository mirroring between platforms +- **Unified Scripts**: Single set of scripts that work across all CI platforms + +## Quick Start + +### 1. Copy CI Scripts + +Copy the `ci-scripts/` directory to your project: + +```bash +cp -r ci-scripts/ /path/to/your/project/ +chmod +x /path/to/your/project/ci-scripts/*.sh +``` + +### 2. Run Detection + +```bash +./ci-scripts/detect.sh +``` + +Output: +``` +[INFO] Detecting project configuration in: /your/project +[SUCCESS] Detection complete! + +Languages detected: javascript,typescript +Primary language: javascript +Package managers: npm +Test frameworks: jest +Build systems: npm-scripts,webpack +``` + +### 3. Setup Dependencies + +```bash +./ci-scripts/setup.sh +``` + +### 4. Run Tests + +```bash +./ci-scripts/test.sh --coverage +``` + +### 5. Lint Code + +```bash +./ci-scripts/lint.sh +# Or with auto-fix: +./ci-scripts/lint.sh --fix +``` + +### 6. Build + +```bash +./ci-scripts/build.sh --release +``` + +## CI Scripts Reference + +| Script | Description | Options | +|--------|-------------|---------| +| `detect.sh` | Detects project configuration | `json` - Output as JSON | +| `setup.sh` | Installs project dependencies | - | +| `test.sh` | Runs test suites | `--coverage`, `--verbose`, `--ci` | +| `lint.sh` | Runs code linters | `--fix`, `--verbose` | +| `build.sh` | Builds the project | `--release`, `--debug` | +| `sync-mirror.sh` | Syncs to mirror repository | `--mirror-url`, `--dry-run` | +| `verify-mirror.sh` | Verifies mirror sync | `--source`, `--mirror` | + +## Supported Languages + +| Language | Package Manager | Test Framework | Linter | +|----------|----------------|----------------|--------| +| JavaScript/TypeScript | npm, yarn, pnpm, bun | Jest, Mocha, Vitest, AVA | ESLint, Prettier | +| Python | pip, pipenv, poetry | pytest, unittest, tox | Ruff, Flake8, Black, mypy | +| Ruby | Bundler | RSpec, Minitest | RuboCop | +| Go | Go Modules | go test | golangci-lint, go vet | +| Rust | Cargo | cargo test | clippy, rustfmt | +| Java/Kotlin | Maven, Gradle | JUnit | Checkstyle, SpotBugs | +| C/C++ | CMake, Make | - | clang-format | +| C#/.NET | NuGet | xUnit, NUnit | dotnet format | +| PHP | Composer | PHPUnit | PHP-CS-Fixer, PHPStan | + +## CI/CD Integration + +### GitHub Actions + +The included `.github/workflows/ci.yml` provides: + +- Automatic language detection +- Matrix testing across OS and language versions +- Dependency caching +- Code coverage reporting +- Build artifact uploads +- Docker image builds +- Automatic releases on tags +- Event-driven mirror sync + +### GitLab CI + +The included `.gitlab-ci.yml` provides: + +- Language-specific job templates +- Parallel test execution +- Coverage reporting +- Container Registry integration +- GitLab Pages deployment +- Release automation + +## Repository Mirroring + +### Setup (GitHub to GitLab) + +1. Generate an SSH key: + ```bash + ssh-keygen -t ed25519 -C "github-to-gitlab-mirror" -f gitlab_mirror_key + ``` + +2. Add the public key to GitLab as a deploy key with write access + +3. Add secrets to GitHub: + - `GITLAB_SSH_PRIVATE_KEY`: Contents of `gitlab_mirror_key` + - `GITLAB_MIRROR_URL`: `git@gitlab.com:your/repo.git` + +4. Push to main/master to trigger sync + +See [SECRETS.md](SECRETS.md) for detailed instructions. + +## Project Structure + +``` +. +β”œβ”€β”€ ci-scripts/ +β”‚ β”œβ”€β”€ detect.sh # Project detection +β”‚ β”œβ”€β”€ setup.sh # Dependency setup +β”‚ β”œβ”€β”€ test.sh # Test runner +β”‚ β”œβ”€β”€ lint.sh # Linter runner +β”‚ β”œβ”€β”€ build.sh # Build script +β”‚ β”œβ”€β”€ sync-mirror.sh # Mirror sync +β”‚ └── verify-mirror.sh # Mirror verification +β”œβ”€β”€ tests/ +β”‚ β”œβ”€β”€ test_detect.bats # BATS tests +β”‚ └── run_tests.sh # Test runner +β”œβ”€β”€ .github/ +β”‚ β”œβ”€β”€ workflows/ +β”‚ β”‚ β”œβ”€β”€ ci.yml # Main CI workflow +β”‚ β”‚ └── codeql.yml # Security scanning +β”‚ └── dependabot.yml # Dependency updates +β”œβ”€β”€ .gitlab-ci.yml # GitLab CI config +β”œβ”€β”€ TODO.md # Project TODO list +β”œβ”€β”€ SECRETS.md # Secrets documentation +β”œβ”€β”€ ROADMAP.adoc # Project roadmap +└── README.md # This file +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `PROJECT_ROOT` | Project root directory | Script's parent dir | +| `CI` | CI environment flag | `false` | +| `COVERAGE` | Enable coverage reporting | `false` | +| `VERBOSE` | Enable verbose output | `false` | +| `FIX` | Auto-fix lint issues | `false` | +| `BUILD_MODE` | Build mode (`release`/`debug`) | `release` | + +### Future: Configuration File + +Support for a `upm.yml` configuration file is planned: + +```yaml +version: 1 + +project: + name: my-project + type: nodejs # Override auto-detection + +ci: + test: + coverage: true + parallel: true + lint: + fix: false +``` + +## Security + +- **CodeQL**: Automated security scanning +- **Dependabot**: Automated dependency updates +- **SAST**: Static Application Security Testing + +See [SECURITY.md](SECURITY.md) for security policy. + +## Contributing + +Contributions are welcome! Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) first. + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run tests: `./tests/run_tests.sh` +5. Submit a pull request + +## Roadmap + +See [ROADMAP.adoc](ROADMAP.adoc) for the full roadmap. + +### MVP v1.0 Goals + +- [x] Auto-detection for 10+ languages +- [x] GitHub Actions workflow +- [x] GitLab CI configuration +- [x] Mirror sync support +- [ ] Configuration file support +- [ ] CLI wrapper tool + +## License + +This project is licensed under the MIT License - see [LICENSE](LICENSE) for details. + +## Acknowledgments + +- Inspired by the need for unified CI/CD across multiple platforms +- Thanks to all contributors and users + +--- + +**Note**: This project is mirrored between: +- GitHub: https://github.com/hyperpolymath/Universal-Project-Manager +- GitLab: https://gitlab.com/overarch-underpin/managers/universal-project-manager diff --git a/ROADMAP.adoc b/ROADMAP.adoc new file mode 100644 index 0000000..336d727 --- /dev/null +++ b/ROADMAP.adoc @@ -0,0 +1,396 @@ += Universal Project Manager Roadmap +:toc: macro +:toc-title: Table of Contents +:toclevels: 3 +:icons: font +:source-highlighter: rouge + +[abstract] +This document outlines the development roadmap for the Universal Project Manager, +from the current CI/CD infrastructure through MVP v1.0 deployment and beyond. + +toc::[] + +== Vision + +The Universal Project Manager (UPM) aims to be a comprehensive, +language-agnostic project management and CI/CD solution that: + +* Automatically detects project types and configurations +* Provides unified CI/CD scripts across platforms (GitHub, GitLab, Bitbucket) +* Enables seamless repository mirroring +* Reduces setup time for new projects to minutes + +== Current State + +=== Completed Infrastructure + +[cols="1,3,1"] +|=== +|Component |Description |Status + +|`ci-scripts/detect.sh` +|Auto-detects languages, package managers, test frameworks, and build systems +|βœ… Complete + +|`ci-scripts/setup.sh` +|Installs dependencies based on detected configuration +|βœ… Complete + +|`ci-scripts/test.sh` +|Runs tests with coverage support +|βœ… Complete + +|`ci-scripts/lint.sh` +|Runs linters with auto-fix support +|βœ… Complete + +|`ci-scripts/build.sh` +|Builds projects in release or debug mode +|βœ… Complete + +|`ci-scripts/sync-mirror.sh` +|Event-driven repository mirroring +|βœ… Complete + +|`ci-scripts/verify-mirror.sh` +|Verifies sync between remotes +|βœ… Complete + +|`.github/workflows/ci.yml` +|GitHub Actions CI/CD pipeline +|βœ… Complete + +|`.gitlab-ci.yml` +|GitLab CI/CD pipeline +|βœ… Complete +|=== + +== Route to MVP v1.0 + +=== Phase 1: Foundation (Current) + +*Status: In Progress* + +==== Goals +* [x] Establish CI/CD infrastructure +* [x] Create platform-agnostic scripts +* [x] Set up dual-platform CI (GitHub + GitLab) +* [ ] Complete documentation +* [ ] Pass all CI checks + +==== Deliverables +* Fully functional CI scripts +* Working GitHub Actions workflow +* Working GitLab CI configuration +* Comprehensive documentation + +==== Success Criteria +* All scripts pass shellcheck +* CI runs successfully on both platforms +* Mirror sync works reliably + +=== Phase 2: Testing & Validation + +*Target: Sprint 2* + +==== Goals +* [ ] Achieve 80%+ test coverage for CI scripts +* [ ] Validate all language detections +* [ ] Test on real-world repositories +* [ ] Gather user feedback + +==== Tasks + +[source,asciidoc] +---- +1. Expand BATS test suite + - Test all detection functions + - Test error handling + - Test edge cases (empty repos, mixed languages) + +2. Integration testing + - Create test fixture repositories + - Test each language ecosystem + - Verify multi-language detection + +3. User acceptance testing + - Recruit 3-5 beta testers + - Document feedback + - Prioritize improvements +---- + +==== Success Criteria +* All tests pass +* No critical bugs reported +* Positive user feedback + +=== Phase 3: Feature Completion + +*Target: Sprint 3* + +==== Goals +* [ ] Add configuration file support (upm.yml) +* [ ] Implement wiki mirroring +* [ ] Add deployment scripts +* [ ] Create CLI wrapper + +==== New Features + +.Configuration File Support +[source,yaml] +---- +# upm.yml - Example configuration +version: 1 + +project: + name: my-project + type: nodejs # Override auto-detection + +ci: + test: + coverage: true + parallel: true + lint: + fix: false + +mirror: + enabled: true + target: gitlab + branches: + - main + - develop +---- + +.CLI Wrapper +[source,bash] +---- +# Proposed CLI interface +upm detect # Run detection +upm setup # Install dependencies +upm test # Run tests +upm lint [--fix] # Run linters +upm build # Build project +upm mirror # Sync to mirror +upm init # Initialize UPM in a project +---- + +=== Phase 4: MVP v1.0 Release + +*Target: Sprint 4* + +==== Release Checklist + +* [ ] All Phase 1-3 features complete +* [ ] Documentation complete and reviewed +* [ ] Security audit passed +* [ ] Performance benchmarks meet targets +* [ ] Release notes prepared +* [ ] Changelog generated +* [ ] Version tagged + +==== MVP Features + +[cols="1,2,1"] +|=== +|Feature |Description |Priority + +|Auto-Detection +|Detect 10+ languages, 15+ package managers, 20+ test frameworks +|P0 + +|CI Scripts +|Platform-agnostic scripts for setup, test, lint, build +|P0 + +|GitHub Actions +|Full CI/CD workflow with matrix testing +|P0 + +|GitLab CI +|Full CI/CD pipeline with comparable features +|P0 + +|Mirror Sync +|Event-driven push mirroring between platforms +|P1 + +|Configuration +|YAML configuration file support +|P1 + +|CLI Tool +|Simple command-line interface +|P2 + +|Documentation +|Complete user and developer documentation +|P0 +|=== + +== Post-MVP Roadmap + +=== v1.1 - Extended Platform Support + +* [ ] Bitbucket Pipelines support +* [ ] Azure DevOps support +* [ ] CircleCI support +* [ ] Jenkins pipeline generation + +=== v1.2 - Advanced Features + +* [ ] Monorepo support +* [ ] Workspace/Lerna/Turborepo detection +* [ ] Custom script hooks +* [ ] Plugin system + +=== v1.3 - Enterprise Features + +* [ ] Private registry support +* [ ] Custom language definitions +* [ ] Team configuration sharing +* [ ] Audit logging + +=== v2.0 - Platform Evolution + +* [ ] Web dashboard +* [ ] Project analytics +* [ ] AI-powered suggestions +* [ ] Self-hosted option + +== Deployment Strategy + +=== MVP Deployment Architecture + +[source,asciidoc] +---- +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Developer Workflow β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Code │────▢│ GitHub │────▢│ GitLab β”‚ β”‚ +β”‚ β”‚ Push β”‚ β”‚ (Primary) β”‚ β”‚ (Mirror) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ GitHub β”‚ β”‚ +β”‚ β”‚ Actions β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Lint β”‚ β”‚ Test β”‚ β”‚ Build β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Deploy β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +---- + +=== Deployment Environments + +[cols="1,2,2"] +|=== +|Environment |Trigger |Purpose + +|Development +|Every push to feature branches +|Validate changes, run tests + +|Staging +|Push to `develop` branch +|Integration testing, preview + +|Production +|Push to `main` or version tag +|Live deployment +|=== + +=== Release Process + +. **Version Bump** ++ +[source,bash] +---- +# Update version in relevant files +npm version patch # or minor/major +---- + +. **Changelog Generation** ++ +[source,bash] +---- +# Generate changelog from commits +npx conventional-changelog -p angular -i CHANGELOG.md -s +---- + +. **Tag Release** ++ +[source,bash] +---- +git tag -a v1.0.0 -m "MVP Release" +git push origin v1.0.0 +---- + +. **Automated Release** ++ +* GitHub Actions creates release +* Artifacts uploaded +* Release notes generated + +== Success Metrics + +=== MVP Success Criteria + +[cols="1,2,1"] +|=== +|Metric |Target |Measurement + +|CI Script Reliability +|99% success rate on supported languages +|CI logs analysis + +|Detection Accuracy +|95% correct language detection +|Test suite results + +|Setup Time +|< 2 minutes for any project +|Benchmark tests + +|Documentation Coverage +|100% of public APIs documented +|Manual review + +|User Satisfaction +|4+ stars average rating +|User feedback +|=== + +=== Long-term Goals + +* 1000+ repositories using UPM +* Support for 20+ programming languages +* Active community with contributors +* Integration with major IDEs + +== Contributing + +See link:CONTRIBUTING.md[CONTRIBUTING.md] for guidelines on: + +* Reporting bugs +* Suggesting features +* Submitting pull requests +* Development setup + +== Changelog + +See link:CHANGELOG.md[CHANGELOG.md] for version history. + +== License + +This project is licensed under the MIT License - see link:LICENSE[LICENSE] for details. diff --git a/SECRETS.md b/SECRETS.md new file mode 100644 index 0000000..c6c86b0 --- /dev/null +++ b/SECRETS.md @@ -0,0 +1,179 @@ +# Secrets Configuration Guide + +This document lists all secrets required for the CI/CD pipelines to function properly. + +## GitHub Repository Secrets + +Configure these secrets in your GitHub repository: +**Settings** > **Secrets and variables** > **Actions** > **New repository secret** + +### Required Secrets + +| Secret Name | Description | Where to Get | Used By | +|-------------|-------------|--------------|---------| +| `GITLAB_SSH_PRIVATE_KEY` | SSH private key for GitLab push mirroring | Generate with `ssh-keygen -t ed25519 -C "github-mirror"` | `.github/workflows/ci.yml` (mirror job) | +| `GITLAB_MIRROR_URL` | GitLab repository URL for mirroring | `git@gitlab.com:overarch-underpin/managers/universal-project-manager.git` | `.github/workflows/ci.yml` (mirror job) | + +### Optional Secrets + +| Secret Name | Description | Where to Get | Used By | +|-------------|-------------|--------------|---------| +| `CODECOV_TOKEN` | Token for uploading coverage reports | [codecov.io](https://codecov.io) - Get from repo settings | `.github/workflows/ci.yml` (test job) | +| `SNYK_TOKEN` | Token for Snyk security scanning | [snyk.io](https://snyk.io) - Account settings | Security scanning workflows | +| `SONAR_TOKEN` | Token for SonarCloud analysis | [sonarcloud.io](https://sonarcloud.io) - Security tab | Code quality workflows | + +### Built-in Secrets (No Configuration Needed) + +| Secret Name | Description | Availability | +|-------------|-------------|--------------| +| `GITHUB_TOKEN` | Automatic GitHub token | Automatically available in all workflows | + +--- + +## GitLab CI/CD Variables + +Configure these variables in your GitLab project: +**Settings** > **CI/CD** > **Variables** > **Add variable** + +### Required Variables + +| Variable Name | Description | Where to Get | Protected | Masked | +|---------------|-------------|--------------|-----------|--------| +| `CI_REGISTRY_USER` | GitLab registry username | Your GitLab username | No | No | +| `CI_REGISTRY_PASSWORD` | GitLab registry password or token | GitLab Personal Access Token with `read_registry`, `write_registry` | Yes | Yes | + +### Optional Variables + +| Variable Name | Description | Where to Get | Protected | Masked | +|---------------|-------------|--------------|-----------|--------| +| `GITHUB_MIRROR_URL` | GitHub repository URL (if reverse mirroring) | `git@github.com:hyperpolymath/Universal-Project-Manager.git` | No | No | +| `GITHUB_SSH_PRIVATE_KEY` | SSH key for GitHub push | Generate with `ssh-keygen` | Yes | Yes | + +### Predefined Variables (No Configuration Needed) + +GitLab provides many predefined CI/CD variables automatically. Key ones include: + +- `CI_COMMIT_REF_SLUG` - Slug of the branch or tag +- `CI_COMMIT_SHA` - Full commit SHA +- `CI_DEFAULT_BRANCH` - Default branch name +- `CI_PROJECT_PATH` - Project path with namespace +- `CI_REGISTRY` - GitLab Container Registry URL +- `CI_REGISTRY_IMAGE` - Registry image path + +--- + +## Setup Instructions + +### 1. Generate SSH Key for Mirroring + +```bash +# Generate a new ED25519 SSH key pair +ssh-keygen -t ed25519 -C "github-to-gitlab-mirror" -f gitlab_mirror_key -N "" + +# The private key (add to GitHub secrets as GITLAB_SSH_PRIVATE_KEY): +cat gitlab_mirror_key + +# The public key (add to GitLab as a deploy key with write access): +cat gitlab_mirror_key.pub +``` + +### 2. Add Deploy Key to GitLab + +1. Go to your GitLab project: **Settings** > **Repository** > **Deploy keys** +2. Click **Add deploy key** +3. Title: `GitHub Mirror` +4. Key: Paste the public key from `gitlab_mirror_key.pub` +5. **Enable** "Grant write permissions to this key" +6. Click **Add key** + +### 3. Add SSH Key to GitHub + +1. Go to your GitHub repository: **Settings** > **Secrets and variables** > **Actions** +2. Click **New repository secret** +3. Name: `GITLAB_SSH_PRIVATE_KEY` +4. Secret: Paste the entire contents of `gitlab_mirror_key` (including BEGIN/END lines) +5. Click **Add secret** + +### 4. Add Mirror URL to GitHub + +1. In the same GitHub Secrets page, click **New repository secret** +2. Name: `GITLAB_MIRROR_URL` +3. Secret: `git@gitlab.com:overarch-underpin/managers/universal-project-manager.git` +4. Click **Add secret** + +### 5. Configure Codecov (Optional) + +1. Go to [codecov.io](https://codecov.io) and sign in with GitHub +2. Add your repository +3. Copy the upload token from the repository settings +4. Add to GitHub Secrets as `CODECOV_TOKEN` + +--- + +## Security Best Practices + +### Do's + +- Use **protected** variables for production secrets +- Use **masked** variables for sensitive values +- Rotate keys periodically (recommended: every 90 days) +- Use environment-specific secrets when possible +- Audit secret access regularly + +### Don'ts + +- Never commit secrets to the repository +- Never log secret values in CI output +- Never share secrets between unrelated projects +- Never use personal access tokens for CI (use deploy keys/tokens) +- Never disable masking for sensitive values + +--- + +## Verification + +### Test GitHub Mirror Setup + +```bash +# From your local machine, verify the connection works +ssh -T git@gitlab.com -i ~/.ssh/gitlab_mirror_key +``` + +Expected output: `Welcome to GitLab, @username!` + +### Test CI Pipeline + +1. Push a small change to trigger the CI +2. Check the Actions tab for workflow runs +3. Verify the mirror job succeeds +4. Check GitLab to confirm the push arrived + +--- + +## Troubleshooting + +### Mirror Push Fails with "Permission denied" + +- Verify the deploy key has **write permissions** on GitLab +- Check the SSH key format (should include BEGIN/END lines) +- Ensure the key isn't password-protected + +### Codecov Upload Fails + +- Verify the token is correct +- Check if the repository is properly activated on codecov.io +- Ensure coverage files are being generated + +### GitLab Container Registry Auth Fails + +- Use a Personal Access Token, not your password +- Token needs `read_registry` and `write_registry` scopes +- Variable should be **masked** and **protected** + +--- + +## Contact + +For issues with secret configuration: +- GitHub: Open an issue in this repository +- GitLab: Contact the project maintainers diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e9257bd --- /dev/null +++ b/TODO.md @@ -0,0 +1,289 @@ +# Universal Project Manager - TODO + +> Automatically generated analysis of codebase improvements needed. +> Last updated: 2025-12-08 + +## Priority Legend + +- **P0 - Critical**: Must be done immediately for the project to function +- **P1 - High**: Should be done soon, affects core functionality +- **P2 - Medium**: Important for production readiness +- **P3 - Low**: Nice to have, improves developer experience + +--- + +## Missing Tests + +### P1 - High Priority + +- [ ] **ci-scripts/setup.sh** - No unit tests for dependency installation + - Test each package manager detection and installation flow + - Mock package managers for isolated testing + - Verify virtual environment creation for Python + +- [ ] **ci-scripts/lint.sh** - No tests for linter execution + - Test linter detection logic + - Verify fix mode operates correctly + - Test failure handling + +- [ ] **ci-scripts/build.sh** - No build verification tests + - Test build command generation for each language + - Verify artifact creation + - Test release vs debug mode + +- [ ] **ci-scripts/test.sh** - Limited test coverage for test runner + - Test framework detection accuracy + - Verify coverage flag propagation + - Test summary generation + +### P2 - Medium Priority + +- [ ] **ci-scripts/sync-mirror.sh** - No mirror sync tests + - Test event-driven push handling + - Verify branch/tag sync logic + - Test error recovery + +- [ ] **ci-scripts/verify-mirror.sh** - No verification tests + - Test branch comparison logic + - Verify tag comparison + - Test history verification + +--- + +## Missing Documentation + +### P0 - Critical + +- [ ] **README.md** - Currently empty, needs comprehensive documentation + - Project overview and purpose + - Installation instructions + - Usage examples for each CI script + - Configuration options + - Contributing guidelines + +### P1 - High Priority + +- [ ] **ci-scripts/README.md** - Document CI scripts + - Purpose of each script + - Environment variables + - Exit codes + - Examples + +- [ ] **CONTRIBUTING.md** - Contribution guidelines + - Development setup + - Code style + - PR process + - Testing requirements + +### P2 - Medium Priority + +- [ ] **API Documentation** - If this becomes a library + - Function signatures + - Usage examples + - Integration guides + +- [ ] **Architecture Decision Records (ADRs)** + - Document key design decisions + - Rationale for language support choices + - CI/CD strategy decisions + +--- + +## Potential Security Issues + +### P0 - Critical + +- [ ] **Secrets Audit** + - Verify no secrets in code or configuration + - Check for hardcoded API keys + - Scan for credential patterns + +- [ ] **Dependency Scanning** + - Enable GitHub Dependabot (partially configured) + - Configure renovate or similar for automated updates + - Set up vulnerability alerts + +### P1 - High Priority + +- [ ] **Input Validation in CI Scripts** + - Sanitize environment variable inputs + - Validate remote URLs before use + - Escape shell arguments properly + +- [ ] **SSH Key Handling** + - Document secure key storage + - Verify key permissions in CI + - Add key rotation reminders + +### P2 - Medium Priority + +- [ ] **Container Security** + - Add Dockerfile best practices + - Use non-root user in containers + - Pin base image versions + +- [ ] **CodeQL Configuration** + - Expand language coverage beyond actions + - Add custom security queries + - Enable security-and-quality ruleset + +--- + +## Code Quality Improvements + +### P1 - High Priority + +- [ ] **Shellcheck Compliance** + - Fix all shellcheck warnings in ci-scripts/ + - Add shellcheck to CI pipeline + - Document any intentional suppressions + +- [ ] **Error Handling** + - Improve error messages with context + - Add retry logic for network operations + - Implement graceful degradation + +- [ ] **Logging Consistency** + - Standardize log formats across scripts + - Add verbosity levels + - Support JSON output for CI parsing + +### P2 - Medium Priority + +- [ ] **Code Deduplication** + - Extract common functions to shared library + - Create utility script for repeated patterns + - Standardize color output functions + +- [ ] **Configuration Management** + - Support configuration file (upm.yml) + - Environment variable documentation + - Default value handling + +### P3 - Low Priority + +- [ ] **Performance Optimization** + - Parallel execution where possible + - Cache detection results + - Minimize subprocess calls + +- [ ] **Cross-Platform Support** + - Test on macOS + - Test on Windows (WSL/Git Bash) + - Document platform-specific quirks + +--- + +## CI/CD Enhancements + +### P1 - High Priority + +- [ ] **Test Matrix Expansion** + - Add more Node.js versions (18, 22) + - Add more Python versions (3.10, 3.11) + - Test on multiple OS (macOS, Windows) + +- [ ] **Caching Optimization** + - Implement proper cache keys + - Add cache warming for dependencies + - Monitor cache hit rates + +- [ ] **Branch Protection Rules** + - Require status checks + - Require reviews + - Protect main branch + +### P2 - Medium Priority + +- [ ] **Deployment Pipeline** + - Add staging environment + - Implement blue-green deployment + - Add rollback capability + +- [ ] **Monitoring Integration** + - Add build time metrics + - Track test coverage trends + - Alert on CI failures + +- [ ] **Release Automation** + - Semantic versioning + - Changelog generation + - Release notes automation + +### P3 - Low Priority + +- [ ] **Advanced CI Features** + - Add visual regression testing + - Implement load testing + - Add accessibility testing + +- [ ] **Developer Experience** + - Pre-commit hooks + - IDE configurations + - Development container support + +--- + +## Infrastructure + +### P2 - Medium Priority + +- [ ] **Wiki Mirroring** + - Set up wiki sync between GitHub and GitLab + - Document wiki workflow + - Automate sync on wiki changes + +- [ ] **Issue/PR Templates** + - Create bug report template + - Create feature request template + - Create PR template with checklist + +### P3 - Low Priority + +- [ ] **Community Files** + - Add FUNDING.yml + - Create SUPPORT.md + - Add discussion templates + +--- + +## Completed Items + +- [x] Create ci-scripts directory structure +- [x] Implement detect.sh for project detection +- [x] Implement setup.sh for dependency installation +- [x] Implement test.sh for running tests +- [x] Implement lint.sh for code quality +- [x] Implement build.sh for project building +- [x] Implement verify-mirror.sh for sync verification +- [x] Implement sync-mirror.sh for event-driven mirroring +- [x] Create GitHub Actions CI workflow +- [x] Create GitLab CI configuration +- [x] Add basic BATS tests for detect.sh +- [x] Configure CodeQL scanning +- [x] Configure Dependabot (partial) + +--- + +## Notes + +### Migration Status + +- **Source Repository**: https://gitlab.com/overarch-underpin/managers/universal-project-manager +- **Destination Repository**: https://github.com/hyperpolymath/Universal-Project-Manager +- **Mirror Direction**: GitHub -> GitLab (event-driven push) +- **Wiki Mirroring**: Not yet configured + +### Dependencies + +The CI scripts have no external dependencies beyond standard Unix tools and +the language-specific package managers they detect. Optional tools like +`shellcheck` and `bats` enhance the experience but are not required. + +### Maintenance + +This TODO list should be reviewed monthly and updated based on: +- New feature requests +- Security advisories +- Dependency updates +- Community feedback diff --git a/ci-scripts/build.sh b/ci-scripts/build.sh new file mode 100755 index 0000000..a9b6d15 --- /dev/null +++ b/ci-scripts/build.sh @@ -0,0 +1,304 @@ +#!/usr/bin/env bash +# Universal Build Script +# Builds the project based on detected configuration +# Exit codes: 0 = success, 1 = build error + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" + +# Configuration +BUILD_MODE="${BUILD_MODE:-release}" # release or debug +VERBOSE="${VERBOSE:-false}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Source detection script +source "$SCRIPT_DIR/detect.sh" 2>/dev/null || true + +# Build Node.js project +build_nodejs() { + log_info "Building Node.js project..." + cd "$PROJECT_ROOT" + + local npm_cmd="npm" + [[ -f "pnpm-lock.yaml" ]] && npm_cmd="pnpm" + [[ -f "yarn.lock" ]] && npm_cmd="yarn" + [[ -f "bun.lockb" ]] && npm_cmd="bun" + + # Check for build script + if [[ -f "package.json" ]] && grep -q '"build"' package.json; then + log_info "Running: $npm_cmd run build" + $npm_cmd run build + log_success "Node.js build completed" + return 0 + fi + + # TypeScript compilation + if [[ -f "tsconfig.json" ]]; then + log_info "Running: npx tsc" + npx tsc + log_success "TypeScript compilation completed" + return 0 + fi + + log_warn "No build configuration found for Node.js" + return 0 +} + +# Build Python project +build_python() { + log_info "Building Python project..." + cd "$PROJECT_ROOT" + + # Activate virtual environment if exists + if [[ -d ".venv" ]]; then + source .venv/bin/activate 2>/dev/null || true + elif [[ -d "venv" ]]; then + source venv/bin/activate 2>/dev/null || true + fi + + # Build wheel + if [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then + log_info "Running: python -m build" + python -m build 2>/dev/null || { + log_info "Running: pip install build && python -m build" + pip install build + python -m build + } + log_success "Python build completed" + return 0 + fi + + log_warn "No build configuration found for Python" + return 0 +} + +# Build Go project +build_go() { + log_info "Building Go project..." + cd "$PROJECT_ROOT" + + if [[ -f "go.mod" ]]; then + local build_flags="" + [[ "$BUILD_MODE" == "release" ]] && build_flags="-ldflags='-s -w'" + + log_info "Running: go build $build_flags ./..." + eval "go build $build_flags ./..." + log_success "Go build completed" + return 0 + fi + + log_warn "No Go module found" + return 0 +} + +# Build Rust project +build_rust() { + log_info "Building Rust project..." + cd "$PROJECT_ROOT" + + if [[ -f "Cargo.toml" ]]; then + local cmd="cargo build" + [[ "$BUILD_MODE" == "release" ]] && cmd="cargo build --release" + + log_info "Running: $cmd" + eval "$cmd" + log_success "Rust build completed" + return 0 + fi + + log_warn "No Cargo.toml found" + return 0 +} + +# Build Java/Maven project +build_maven() { + log_info "Building Maven project..." + cd "$PROJECT_ROOT" + + if [[ -f "pom.xml" ]]; then + log_info "Running: mvn package -DskipTests" + mvn package -DskipTests -B + log_success "Maven build completed" + return 0 + fi + + return 0 +} + +# Build Java/Gradle project +build_gradle() { + log_info "Building Gradle project..." + cd "$PROJECT_ROOT" + + if [[ -f "build.gradle" || -f "build.gradle.kts" ]]; then + local gradle_cmd="gradle" + [[ -f "gradlew" ]] && gradle_cmd="./gradlew" + + log_info "Running: $gradle_cmd build -x test" + $gradle_cmd build -x test + log_success "Gradle build completed" + return 0 + fi + + return 0 +} + +# Build C/C++ project +build_cpp() { + log_info "Building C/C++ project..." + cd "$PROJECT_ROOT" + + # CMake + if [[ -f "CMakeLists.txt" ]]; then + mkdir -p build + cd build + log_info "Running: cmake .. && make" + cmake .. + make -j"$(nproc)" + log_success "CMake build completed" + return 0 + fi + + # Make + if [[ -f "Makefile" ]]; then + log_info "Running: make" + make -j"$(nproc)" + log_success "Make build completed" + return 0 + fi + + log_warn "No build system found for C/C++" + return 0 +} + +# Build .NET project +build_dotnet() { + log_info "Building .NET project..." + cd "$PROJECT_ROOT" + + if find . -name "*.csproj" -o -name "*.sln" 2>/dev/null | grep -q .; then + local config="Release" + [[ "$BUILD_MODE" == "debug" ]] && config="Debug" + + log_info "Running: dotnet build -c $config" + dotnet build -c "$config" + log_success ".NET build completed" + return 0 + fi + + return 0 +} + +# Build Docker image +build_docker() { + log_info "Building Docker image..." + cd "$PROJECT_ROOT" + + if [[ -f "Dockerfile" ]]; then + local image_name + image_name=$(basename "$PROJECT_ROOT" | tr '[:upper:]' '[:lower:]') + + log_info "Running: docker build -t $image_name ." + docker build -t "$image_name" . + log_success "Docker build completed" + return 0 + fi + + return 0 +} + +# Main function +main() { + log_info "Starting build in: $PROJECT_ROOT" + echo "" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --release) BUILD_MODE="release"; shift ;; + --debug) BUILD_MODE="debug"; shift ;; + --verbose|-v) VERBOSE="true"; shift ;; + *) shift ;; + esac + done + + log_info "Build mode: $BUILD_MODE" + + # Run detection if not already done + if [[ -z "${DETECTED_LANGUAGES:-}" ]]; then + source "$SCRIPT_DIR/detect.sh" + detect_languages + detect_package_managers + detect_build_systems + fi + + local builds_run=0 + local exit_code=0 + + # Build based on detected languages/systems + IFS=',' read -ra languages <<< "${DETECTED_LANGUAGES:-}" + + for lang in "${languages[@]}"; do + case "$lang" in + javascript|typescript) + build_nodejs && ((builds_run++)) || exit_code=1 + ;; + python) + build_python && ((builds_run++)) || exit_code=1 + ;; + go) + build_go && ((builds_run++)) || exit_code=1 + ;; + rust) + build_rust && ((builds_run++)) || exit_code=1 + ;; + java|kotlin) + if [[ -f "$PROJECT_ROOT/pom.xml" ]]; then + build_maven && ((builds_run++)) || exit_code=1 + elif [[ -f "$PROJECT_ROOT/build.gradle" || -f "$PROJECT_ROOT/build.gradle.kts" ]]; then + build_gradle && ((builds_run++)) || exit_code=1 + fi + ;; + c-cpp) + build_cpp && ((builds_run++)) || exit_code=1 + ;; + csharp) + build_dotnet && ((builds_run++)) || exit_code=1 + ;; + esac + done + + # Build Docker if Dockerfile exists + if [[ -f "$PROJECT_ROOT/Dockerfile" ]]; then + build_docker && ((builds_run++)) || exit_code=1 + fi + + echo "" + if [[ $builds_run -eq 0 ]]; then + log_warn "No build targets found" + elif [[ $exit_code -eq 0 ]]; then + log_success "All builds completed successfully!" + else + log_error "Some builds failed" + fi + + return $exit_code +} + +# Run if executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/ci-scripts/detect.sh b/ci-scripts/detect.sh new file mode 100755 index 0000000..63ef7c5 --- /dev/null +++ b/ci-scripts/detect.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# Universal Project Detector +# Detects programming languages, package managers, and test frameworks +# Exit codes: 0 = success, 1 = error + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" + +# Detection results (exported as environment variables) +export DETECTED_LANGUAGES="" +export DETECTED_PACKAGE_MANAGERS="" +export DETECTED_TEST_FRAMEWORKS="" +export DETECTED_BUILD_SYSTEMS="" +export PRIMARY_LANGUAGE="" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Detect languages based on file extensions and config files +detect_languages() { + local languages=() + + # JavaScript/TypeScript + if [[ -f "$PROJECT_ROOT/package.json" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.jsx" 2>/dev/null | grep -q .; then + languages+=("javascript") + if find "$PROJECT_ROOT" -maxdepth 3 -name "*.ts" -o -name "*.tsx" 2>/dev/null | grep -q .; then + languages+=("typescript") + fi + fi + + # Python + if [[ -f "$PROJECT_ROOT/requirements.txt" ]] || \ + [[ -f "$PROJECT_ROOT/setup.py" ]] || \ + [[ -f "$PROJECT_ROOT/pyproject.toml" ]] || \ + [[ -f "$PROJECT_ROOT/Pipfile" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.py" 2>/dev/null | grep -q .; then + languages+=("python") + fi + + # Ruby + if [[ -f "$PROJECT_ROOT/Gemfile" ]] || \ + [[ -f "$PROJECT_ROOT/*.gemspec" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.rb" 2>/dev/null | grep -q .; then + languages+=("ruby") + fi + + # Go + if [[ -f "$PROJECT_ROOT/go.mod" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.go" 2>/dev/null | grep -q .; then + languages+=("go") + fi + + # Rust + if [[ -f "$PROJECT_ROOT/Cargo.toml" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.rs" 2>/dev/null | grep -q .; then + languages+=("rust") + fi + + # Java/Kotlin + if [[ -f "$PROJECT_ROOT/pom.xml" ]] || \ + [[ -f "$PROJECT_ROOT/build.gradle" ]] || \ + [[ -f "$PROJECT_ROOT/build.gradle.kts" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.java" 2>/dev/null | grep -q .; then + languages+=("java") + if find "$PROJECT_ROOT" -maxdepth 3 -name "*.kt" 2>/dev/null | grep -q .; then + languages+=("kotlin") + fi + fi + + # C/C++ + if [[ -f "$PROJECT_ROOT/CMakeLists.txt" ]] || \ + [[ -f "$PROJECT_ROOT/Makefile" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" 2>/dev/null | grep -q .; then + languages+=("c-cpp") + fi + + # C#/.NET + if find "$PROJECT_ROOT" -maxdepth 3 -name "*.csproj" -o -name "*.sln" 2>/dev/null | grep -q .; then + languages+=("csharp") + fi + + # PHP + if [[ -f "$PROJECT_ROOT/composer.json" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.php" 2>/dev/null | grep -q .; then + languages+=("php") + fi + + # Swift + if [[ -f "$PROJECT_ROOT/Package.swift" ]] || \ + find "$PROJECT_ROOT" -maxdepth 3 -name "*.swift" 2>/dev/null | grep -q .; then + languages+=("swift") + fi + + # Shell scripts + if find "$PROJECT_ROOT" -maxdepth 3 -name "*.sh" -o -name "*.bash" 2>/dev/null | grep -q .; then + languages+=("shell") + fi + + DETECTED_LANGUAGES=$(IFS=','; echo "${languages[*]}") + + # Set primary language (first detected) + if [[ ${#languages[@]} -gt 0 ]]; then + PRIMARY_LANGUAGE="${languages[0]}" + fi +} + +# Detect package managers +detect_package_managers() { + local managers=() + + # Node.js ecosystem + [[ -f "$PROJECT_ROOT/package-lock.json" ]] && managers+=("npm") + [[ -f "$PROJECT_ROOT/yarn.lock" ]] && managers+=("yarn") + [[ -f "$PROJECT_ROOT/pnpm-lock.yaml" ]] && managers+=("pnpm") + [[ -f "$PROJECT_ROOT/bun.lockb" ]] && managers+=("bun") + + # Python ecosystem + [[ -f "$PROJECT_ROOT/requirements.txt" ]] && managers+=("pip") + [[ -f "$PROJECT_ROOT/Pipfile" ]] && managers+=("pipenv") + [[ -f "$PROJECT_ROOT/poetry.lock" ]] && managers+=("poetry") + [[ -f "$PROJECT_ROOT/pyproject.toml" ]] && managers+=("pip") # could be poetry/pdm/etc + + # Ruby + [[ -f "$PROJECT_ROOT/Gemfile" ]] && managers+=("bundler") + + # Go + [[ -f "$PROJECT_ROOT/go.mod" ]] && managers+=("go-modules") + + # Rust + [[ -f "$PROJECT_ROOT/Cargo.toml" ]] && managers+=("cargo") + + # Java/JVM + [[ -f "$PROJECT_ROOT/pom.xml" ]] && managers+=("maven") + [[ -f "$PROJECT_ROOT/build.gradle" || -f "$PROJECT_ROOT/build.gradle.kts" ]] && managers+=("gradle") + + # PHP + [[ -f "$PROJECT_ROOT/composer.json" ]] && managers+=("composer") + + # .NET + [[ -f "$PROJECT_ROOT/*.csproj" || -f "$PROJECT_ROOT/*.sln" ]] && managers+=("dotnet") + + DETECTED_PACKAGE_MANAGERS=$(IFS=','; echo "${managers[*]}") +} + +# Detect test frameworks +detect_test_frameworks() { + local frameworks=() + + # JavaScript/TypeScript + if [[ -f "$PROJECT_ROOT/package.json" ]]; then + local pkg_content + pkg_content=$(cat "$PROJECT_ROOT/package.json" 2>/dev/null || echo "{}") + + echo "$pkg_content" | grep -q '"jest"' && frameworks+=("jest") + echo "$pkg_content" | grep -q '"mocha"' && frameworks+=("mocha") + echo "$pkg_content" | grep -q '"vitest"' && frameworks+=("vitest") + echo "$pkg_content" | grep -q '"ava"' && frameworks+=("ava") + echo "$pkg_content" | grep -q '"tap"' && frameworks+=("tap") + echo "$pkg_content" | grep -q '"@testing-library"' && frameworks+=("testing-library") + echo "$pkg_content" | grep -q '"cypress"' && frameworks+=("cypress") + echo "$pkg_content" | grep -q '"playwright"' && frameworks+=("playwright") + fi + + # Python + [[ -f "$PROJECT_ROOT/pytest.ini" || -f "$PROJECT_ROOT/pyproject.toml" ]] && frameworks+=("pytest") + [[ -f "$PROJECT_ROOT/setup.cfg" ]] && grep -q "pytest" "$PROJECT_ROOT/setup.cfg" 2>/dev/null && frameworks+=("pytest") + [[ -f "$PROJECT_ROOT/tox.ini" ]] && frameworks+=("tox") + find "$PROJECT_ROOT" -maxdepth 3 -name "test_*.py" 2>/dev/null | grep -q . && frameworks+=("unittest") + + # Ruby + [[ -d "$PROJECT_ROOT/spec" ]] && frameworks+=("rspec") + [[ -d "$PROJECT_ROOT/test" ]] && frameworks+=("minitest") + + # Go + find "$PROJECT_ROOT" -maxdepth 3 -name "*_test.go" 2>/dev/null | grep -q . && frameworks+=("go-test") + + # Rust + [[ -f "$PROJECT_ROOT/Cargo.toml" ]] && frameworks+=("cargo-test") + + # Java + [[ -d "$PROJECT_ROOT/src/test" ]] && frameworks+=("junit") + + DETECTED_TEST_FRAMEWORKS=$(IFS=','; echo "${frameworks[*]}") +} + +# Detect build systems +detect_build_systems() { + local systems=() + + [[ -f "$PROJECT_ROOT/Makefile" ]] && systems+=("make") + [[ -f "$PROJECT_ROOT/CMakeLists.txt" ]] && systems+=("cmake") + [[ -f "$PROJECT_ROOT/package.json" ]] && systems+=("npm-scripts") + [[ -f "$PROJECT_ROOT/webpack.config.js" || -f "$PROJECT_ROOT/webpack.config.ts" ]] && systems+=("webpack") + [[ -f "$PROJECT_ROOT/vite.config.js" || -f "$PROJECT_ROOT/vite.config.ts" ]] && systems+=("vite") + [[ -f "$PROJECT_ROOT/rollup.config.js" ]] && systems+=("rollup") + [[ -f "$PROJECT_ROOT/esbuild.config.js" ]] && systems+=("esbuild") + [[ -f "$PROJECT_ROOT/tsconfig.json" ]] && systems+=("tsc") + [[ -f "$PROJECT_ROOT/Dockerfile" ]] && systems+=("docker") + [[ -f "$PROJECT_ROOT/docker-compose.yml" || -f "$PROJECT_ROOT/docker-compose.yaml" ]] && systems+=("docker-compose") + + DETECTED_BUILD_SYSTEMS=$(IFS=','; echo "${systems[*]}") +} + +# Generate JSON output +generate_json_output() { + cat </dev/null || true + +# Track lint results +LINTS_RUN=0 +LINTS_PASSED=0 +LINTS_FAILED=0 + +# Run ESLint for JavaScript/TypeScript +run_eslint() { + log_info "Running ESLint..." + cd "$PROJECT_ROOT" + + local eslint_cmd="" + + if [[ -f "node_modules/.bin/eslint" ]]; then + eslint_cmd="npx eslint" + elif command -v eslint &>/dev/null; then + eslint_cmd="eslint" + fi + + if [[ -n "$eslint_cmd" ]]; then + ((LINTS_RUN++)) || true + + # Check for ESLint config + local has_config=false + for cfg in .eslintrc .eslintrc.js .eslintrc.json .eslintrc.yml .eslintrc.yaml eslint.config.js eslint.config.mjs; do + [[ -f "$PROJECT_ROOT/$cfg" ]] && has_config=true && break + done + + if [[ "$has_config" == "false" ]]; then + log_warn "No ESLint config found, skipping" + return 0 + fi + + local cmd="$eslint_cmd ." + [[ "$FIX" == "true" ]] && cmd="$cmd --fix" + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "ESLint passed" + else + ((LINTS_FAILED++)) || true + log_error "ESLint found issues" + return 1 + fi + fi +} + +# Run Prettier for formatting +run_prettier() { + log_info "Running Prettier..." + cd "$PROJECT_ROOT" + + local prettier_cmd="" + + if [[ -f "node_modules/.bin/prettier" ]]; then + prettier_cmd="npx prettier" + elif command -v prettier &>/dev/null; then + prettier_cmd="prettier" + fi + + if [[ -n "$prettier_cmd" ]]; then + # Check for Prettier config + local has_config=false + for cfg in .prettierrc .prettierrc.js .prettierrc.json .prettierrc.yml prettier.config.js; do + [[ -f "$PROJECT_ROOT/$cfg" ]] && has_config=true && break + done + + if [[ "$has_config" == "false" ]]; then + log_warn "No Prettier config found, skipping" + return 0 + fi + + ((LINTS_RUN++)) || true + + local cmd="$prettier_cmd --check ." + [[ "$FIX" == "true" ]] && cmd="$prettier_cmd --write ." + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "Prettier check passed" + else + ((LINTS_FAILED++)) || true + log_error "Prettier found formatting issues" + return 1 + fi + fi +} + +# Run TypeScript compiler for type checking +run_tsc() { + log_info "Running TypeScript type check..." + cd "$PROJECT_ROOT" + + if [[ -f "tsconfig.json" ]]; then + ((LINTS_RUN++)) || true + + log_info "Running: npx tsc --noEmit" + if npx tsc --noEmit; then + ((LINTS_PASSED++)) || true + log_success "TypeScript type check passed" + else + ((LINTS_FAILED++)) || true + log_error "TypeScript type check failed" + return 1 + fi + fi +} + +# Run Python linters +run_python_lint() { + log_info "Running Python linters..." + cd "$PROJECT_ROOT" + + # Activate virtual environment if exists + if [[ -d ".venv" ]]; then + source .venv/bin/activate 2>/dev/null || true + elif [[ -d "venv" ]]; then + source venv/bin/activate 2>/dev/null || true + fi + + # Ruff (fast Python linter) + if command -v ruff &>/dev/null; then + ((LINTS_RUN++)) || true + + local cmd="ruff check ." + [[ "$FIX" == "true" ]] && cmd="ruff check --fix ." + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "Ruff passed" + else + ((LINTS_FAILED++)) || true + log_error "Ruff found issues" + fi + # Flake8 + elif command -v flake8 &>/dev/null; then + ((LINTS_RUN++)) || true + + log_info "Running: flake8 ." + if flake8 .; then + ((LINTS_PASSED++)) || true + log_success "Flake8 passed" + else + ((LINTS_FAILED++)) || true + log_error "Flake8 found issues" + fi + # Pylint + elif command -v pylint &>/dev/null; then + ((LINTS_RUN++)) || true + + log_info "Running: pylint **/*.py" + if find . -name "*.py" -not -path "./venv/*" -not -path "./.venv/*" | xargs pylint --exit-zero; then + ((LINTS_PASSED++)) || true + log_success "Pylint passed" + else + ((LINTS_FAILED++)) || true + log_error "Pylint found issues" + fi + fi + + # Black (formatter) + if command -v black &>/dev/null; then + ((LINTS_RUN++)) || true + + local cmd="black --check ." + [[ "$FIX" == "true" ]] && cmd="black ." + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "Black check passed" + else + ((LINTS_FAILED++)) || true + log_error "Black found formatting issues" + fi + fi + + # mypy (type checker) + if command -v mypy &>/dev/null && [[ -f "mypy.ini" || -f "pyproject.toml" ]]; then + ((LINTS_RUN++)) || true + + log_info "Running: mypy ." + if mypy .; then + ((LINTS_PASSED++)) || true + log_success "mypy type check passed" + else + ((LINTS_FAILED++)) || true + log_error "mypy found type issues" + fi + fi +} + +# Run Ruby linters +run_ruby_lint() { + log_info "Running Ruby linters..." + cd "$PROJECT_ROOT" + + # RuboCop + if [[ -f "Gemfile" ]] && grep -q "rubocop" Gemfile 2>/dev/null; then + ((LINTS_RUN++)) || true + + local cmd="bundle exec rubocop" + [[ "$FIX" == "true" ]] && cmd="$cmd -a" + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "RuboCop passed" + else + ((LINTS_FAILED++)) || true + log_error "RuboCop found issues" + return 1 + fi + fi +} + +# Run Go linters +run_go_lint() { + log_info "Running Go linters..." + cd "$PROJECT_ROOT" + + # golangci-lint + if command -v golangci-lint &>/dev/null; then + ((LINTS_RUN++)) || true + + local cmd="golangci-lint run" + [[ "$FIX" == "true" ]] && cmd="$cmd --fix" + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "golangci-lint passed" + else + ((LINTS_FAILED++)) || true + log_error "golangci-lint found issues" + return 1 + fi + # go vet (built-in) + elif command -v go &>/dev/null; then + ((LINTS_RUN++)) || true + + log_info "Running: go vet ./..." + if go vet ./...; then + ((LINTS_PASSED++)) || true + log_success "go vet passed" + else + ((LINTS_FAILED++)) || true + log_error "go vet found issues" + return 1 + fi + fi + + # gofmt check + if command -v gofmt &>/dev/null; then + ((LINTS_RUN++)) || true + + log_info "Checking gofmt..." + local unformatted + unformatted=$(gofmt -l . 2>/dev/null | grep -v vendor || true) + + if [[ -z "$unformatted" ]]; then + ((LINTS_PASSED++)) || true + log_success "gofmt check passed" + else + if [[ "$FIX" == "true" ]]; then + gofmt -w . + ((LINTS_PASSED++)) || true + log_success "gofmt applied fixes" + else + ((LINTS_FAILED++)) || true + log_error "gofmt found unformatted files:" + echo "$unformatted" + return 1 + fi + fi + fi +} + +# Run Rust linters +run_rust_lint() { + log_info "Running Rust linters..." + cd "$PROJECT_ROOT" + + if [[ -f "Cargo.toml" ]]; then + # cargo fmt check + ((LINTS_RUN++)) || true + + local cmd="cargo fmt --check" + [[ "$FIX" == "true" ]] && cmd="cargo fmt" + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "cargo fmt check passed" + else + ((LINTS_FAILED++)) || true + log_error "cargo fmt found formatting issues" + fi + + # cargo clippy + ((LINTS_RUN++)) || true + + log_info "Running: cargo clippy" + if cargo clippy -- -D warnings; then + ((LINTS_PASSED++)) || true + log_success "cargo clippy passed" + else + ((LINTS_FAILED++)) || true + log_error "cargo clippy found issues" + return 1 + fi + fi +} + +# Run shell script linters +run_shell_lint() { + log_info "Running shell script linters..." + cd "$PROJECT_ROOT" + + # shellcheck + if command -v shellcheck &>/dev/null; then + local shell_files + shell_files=$(find . -name "*.sh" -o -name "*.bash" 2>/dev/null | grep -v node_modules | grep -v vendor || true) + + if [[ -n "$shell_files" ]]; then + ((LINTS_RUN++)) || true + + log_info "Running: shellcheck on shell scripts" + if echo "$shell_files" | xargs shellcheck; then + ((LINTS_PASSED++)) || true + log_success "shellcheck passed" + else + ((LINTS_FAILED++)) || true + log_error "shellcheck found issues" + return 1 + fi + fi + fi +} + +# Run PHP linters +run_php_lint() { + log_info "Running PHP linters..." + cd "$PROJECT_ROOT" + + # PHP-CS-Fixer + if [[ -f "vendor/bin/php-cs-fixer" ]]; then + ((LINTS_RUN++)) || true + + local cmd="./vendor/bin/php-cs-fixer fix --dry-run --diff" + [[ "$FIX" == "true" ]] && cmd="./vendor/bin/php-cs-fixer fix" + + log_info "Running: $cmd" + if eval "$cmd"; then + ((LINTS_PASSED++)) || true + log_success "PHP-CS-Fixer passed" + else + ((LINTS_FAILED++)) || true + log_error "PHP-CS-Fixer found issues" + return 1 + fi + # PHPStan + elif [[ -f "vendor/bin/phpstan" ]]; then + ((LINTS_RUN++)) || true + + log_info "Running: ./vendor/bin/phpstan analyse" + if ./vendor/bin/phpstan analyse; then + ((LINTS_PASSED++)) || true + log_success "PHPStan passed" + else + ((LINTS_FAILED++)) || true + log_error "PHPStan found issues" + return 1 + fi + fi +} + +# Print lint summary +print_summary() { + echo "" + echo "========================================" + echo " LINT SUMMARY" + echo "========================================" + echo "Linters run: $LINTS_RUN" + echo "Linters passed: $LINTS_PASSED" + echo "Linters failed: $LINTS_FAILED" + echo "========================================" + + if [[ $LINTS_FAILED -gt 0 ]]; then + log_error "Some linters found issues!" + return 1 + elif [[ $LINTS_RUN -eq 0 ]]; then + log_warn "No linters were run" + return 0 + else + log_success "All linters passed!" + return 0 + fi +} + +# Main function +main() { + log_info "Starting linter in: $PROJECT_ROOT" + echo "" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --fix) FIX="true"; shift ;; + --verbose|-v) VERBOSE="true"; shift ;; + *) shift ;; + esac + done + + # Run detection if not already done + if [[ -z "${DETECTED_LANGUAGES:-}" ]]; then + source "$SCRIPT_DIR/detect.sh" + detect_languages + detect_package_managers + fi + + local exit_code=0 + + # Run linters based on detected languages + IFS=',' read -ra languages <<< "${DETECTED_LANGUAGES:-}" + + for lang in "${languages[@]}"; do + case "$lang" in + javascript|typescript) + run_eslint || exit_code=1 + run_prettier || exit_code=1 + [[ "$lang" == "typescript" ]] && (run_tsc || exit_code=1) + ;; + python) + run_python_lint || exit_code=1 + ;; + ruby) + run_ruby_lint || exit_code=1 + ;; + go) + run_go_lint || exit_code=1 + ;; + rust) + run_rust_lint || exit_code=1 + ;; + php) + run_php_lint || exit_code=1 + ;; + shell) + run_shell_lint || exit_code=1 + ;; + esac + done + + # Always run shell linter on ci-scripts + run_shell_lint || exit_code=1 + + print_summary || exit_code=$? + + return $exit_code +} + +# Run if executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/ci-scripts/setup.sh b/ci-scripts/setup.sh new file mode 100755 index 0000000..0df55a2 --- /dev/null +++ b/ci-scripts/setup.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# Universal Setup Script +# Installs dependencies based on detected project type +# Exit codes: 0 = success, 1 = error + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" + +# Source detection script +source "$SCRIPT_DIR/detect.sh" 2>/dev/null || true + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Setup Node.js dependencies +setup_nodejs() { + log_info "Setting up Node.js dependencies..." + + cd "$PROJECT_ROOT" + + if [[ -f "pnpm-lock.yaml" ]]; then + log_info "Using pnpm..." + pnpm install --frozen-lockfile 2>/dev/null || pnpm install + elif [[ -f "yarn.lock" ]]; then + log_info "Using yarn..." + yarn install --frozen-lockfile 2>/dev/null || yarn install + elif [[ -f "bun.lockb" ]]; then + log_info "Using bun..." + bun install --frozen-lockfile 2>/dev/null || bun install + elif [[ -f "package-lock.json" ]]; then + log_info "Using npm ci..." + npm ci 2>/dev/null || npm install + elif [[ -f "package.json" ]]; then + log_info "Using npm install..." + npm install + fi + + log_success "Node.js dependencies installed" +} + +# Setup Python dependencies +setup_python() { + log_info "Setting up Python dependencies..." + + cd "$PROJECT_ROOT" + + # Create virtual environment if not exists + if [[ ! -d ".venv" && ! -d "venv" ]]; then + log_info "Creating virtual environment..." + python3 -m venv .venv || python -m venv .venv + fi + + # Activate virtual environment + if [[ -d ".venv" ]]; then + source .venv/bin/activate 2>/dev/null || true + elif [[ -d "venv" ]]; then + source venv/bin/activate 2>/dev/null || true + fi + + # Install dependencies + if [[ -f "poetry.lock" ]]; then + log_info "Using poetry..." + poetry install --no-interaction + elif [[ -f "Pipfile" ]]; then + log_info "Using pipenv..." + pipenv install --dev + elif [[ -f "pyproject.toml" ]]; then + log_info "Installing from pyproject.toml..." + pip install -e ".[dev]" 2>/dev/null || pip install -e . + elif [[ -f "requirements.txt" ]]; then + log_info "Using pip..." + pip install -r requirements.txt + [[ -f "requirements-dev.txt" ]] && pip install -r requirements-dev.txt + fi + + log_success "Python dependencies installed" +} + +# Setup Ruby dependencies +setup_ruby() { + log_info "Setting up Ruby dependencies..." + + cd "$PROJECT_ROOT" + + if [[ -f "Gemfile" ]]; then + log_info "Using bundler..." + bundle install + fi + + log_success "Ruby dependencies installed" +} + +# Setup Go dependencies +setup_go() { + log_info "Setting up Go dependencies..." + + cd "$PROJECT_ROOT" + + if [[ -f "go.mod" ]]; then + log_info "Downloading Go modules..." + go mod download + go mod verify + fi + + log_success "Go dependencies installed" +} + +# Setup Rust dependencies +setup_rust() { + log_info "Setting up Rust dependencies..." + + cd "$PROJECT_ROOT" + + if [[ -f "Cargo.toml" ]]; then + log_info "Fetching Rust crates..." + cargo fetch + fi + + log_success "Rust dependencies installed" +} + +# Setup Java/Maven dependencies +setup_java_maven() { + log_info "Setting up Maven dependencies..." + + cd "$PROJECT_ROOT" + + if [[ -f "pom.xml" ]]; then + mvn dependency:resolve -q + fi + + log_success "Maven dependencies installed" +} + +# Setup Java/Gradle dependencies +setup_java_gradle() { + log_info "Setting up Gradle dependencies..." + + cd "$PROJECT_ROOT" + + if [[ -f "build.gradle" || -f "build.gradle.kts" ]]; then + if [[ -f "gradlew" ]]; then + ./gradlew dependencies --quiet + else + gradle dependencies --quiet + fi + fi + + log_success "Gradle dependencies installed" +} + +# Setup PHP dependencies +setup_php() { + log_info "Setting up PHP dependencies..." + + cd "$PROJECT_ROOT" + + if [[ -f "composer.json" ]]; then + composer install --no-interaction + fi + + log_success "PHP dependencies installed" +} + +# Setup .NET dependencies +setup_dotnet() { + log_info "Setting up .NET dependencies..." + + cd "$PROJECT_ROOT" + + dotnet restore + + log_success ".NET dependencies installed" +} + +# Main function +main() { + log_info "Starting dependency setup in: $PROJECT_ROOT" + + # Run detection if not already done + if [[ -z "${DETECTED_PACKAGE_MANAGERS:-}" ]]; then + source "$SCRIPT_DIR/detect.sh" + detect_languages + detect_package_managers + fi + + local setup_count=0 + + # Setup based on detected package managers + IFS=',' read -ra managers <<< "$DETECTED_PACKAGE_MANAGERS" + + for manager in "${managers[@]}"; do + case "$manager" in + npm|yarn|pnpm|bun) + setup_nodejs + ((setup_count++)) || true + break # Only need to run once for Node.js + ;; + pip|pipenv|poetry) + setup_python + ((setup_count++)) || true + break # Only need to run once for Python + ;; + bundler) + setup_ruby + ((setup_count++)) || true + ;; + go-modules) + setup_go + ((setup_count++)) || true + ;; + cargo) + setup_rust + ((setup_count++)) || true + ;; + maven) + setup_java_maven + ((setup_count++)) || true + ;; + gradle) + setup_java_gradle + ((setup_count++)) || true + ;; + composer) + setup_php + ((setup_count++)) || true + ;; + dotnet) + setup_dotnet + ((setup_count++)) || true + ;; + esac + done + + if [[ $setup_count -eq 0 ]]; then + log_warn "No recognized package managers found. Skipping dependency installation." + fi + + echo "" + log_success "Setup complete! ($setup_count package manager(s) configured)" +} + +# Run if executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/ci-scripts/sync-mirror.sh b/ci-scripts/sync-mirror.sh new file mode 100755 index 0000000..f2250a2 --- /dev/null +++ b/ci-scripts/sync-mirror.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +# Mirror Sync Script +# Syncs branches and tags between GitHub (origin) and GitLab (mirror) +# Designed to be triggered by GitHub Actions on push events (event-driven, not polling) +# Exit codes: 0 = success, 1 = error + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" + +# Configuration +SOURCE_REMOTE="${SOURCE_REMOTE:-origin}" +MIRROR_REMOTE="${MIRROR_REMOTE:-gitlab}" +MIRROR_URL="${MIRROR_URL:-}" +DRY_RUN="${DRY_RUN:-false}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Setup mirror remote +setup_mirror_remote() { + log_info "Setting up mirror remote..." + + if [[ -z "$MIRROR_URL" ]]; then + log_error "MIRROR_URL environment variable is required" + return 1 + fi + + # Add or update mirror remote + if git remote get-url "$MIRROR_REMOTE" &>/dev/null; then + git remote set-url "$MIRROR_REMOTE" "$MIRROR_URL" + log_info "Updated $MIRROR_REMOTE URL to $MIRROR_URL" + else + git remote add "$MIRROR_REMOTE" "$MIRROR_URL" + log_info "Added $MIRROR_REMOTE remote: $MIRROR_URL" + fi +} + +# Sync specific branch +sync_branch() { + local branch="$1" + + log_info "Syncing branch: $branch" + + if [[ "$DRY_RUN" == "true" ]]; then + log_info "[DRY RUN] Would push $branch to $MIRROR_REMOTE" + return 0 + fi + + if git push "$MIRROR_REMOTE" "$SOURCE_REMOTE/$branch:refs/heads/$branch" --force; then + log_success "Synced branch: $branch" + return 0 + else + log_error "Failed to sync branch: $branch" + return 1 + fi +} + +# Sync all branches +sync_all_branches() { + log_info "Syncing all branches..." + + local branches + branches=$(git branch -r | grep "^ $SOURCE_REMOTE/" | sed "s| $SOURCE_REMOTE/||" | grep -v HEAD || true) + + local success=0 + local failed=0 + + while IFS= read -r branch; do + [[ -z "$branch" ]] && continue + + if sync_branch "$branch"; then + ((success++)) || true + else + ((failed++)) || true + fi + done <<< "$branches" + + echo "" + log_info "Branch sync complete: $success succeeded, $failed failed" + + [[ $failed -gt 0 ]] && return 1 + return 0 +} + +# Sync all tags +sync_tags() { + log_info "Syncing tags..." + + if [[ "$DRY_RUN" == "true" ]]; then + log_info "[DRY RUN] Would push all tags to $MIRROR_REMOTE" + return 0 + fi + + if git push "$MIRROR_REMOTE" --tags --force; then + log_success "Tags synced successfully" + return 0 + else + log_error "Failed to sync tags" + return 1 + fi +} + +# Sync triggered by push event (event-driven) +sync_on_push() { + local ref="${GITHUB_REF:-}" + local sha="${GITHUB_SHA:-}" + + if [[ -z "$ref" ]]; then + log_error "GITHUB_REF not set - are you running in GitHub Actions?" + return 1 + fi + + log_info "Event-driven sync triggered" + log_info "Ref: $ref" + log_info "SHA: ${sha:0:12}" + + # Extract branch or tag name + local ref_type ref_name + if [[ "$ref" == refs/heads/* ]]; then + ref_type="branch" + ref_name="${ref#refs/heads/}" + elif [[ "$ref" == refs/tags/* ]]; then + ref_type="tag" + ref_name="${ref#refs/tags/}" + else + log_warn "Unknown ref type: $ref" + return 0 + fi + + log_info "Syncing $ref_type: $ref_name" + + if [[ "$DRY_RUN" == "true" ]]; then + log_info "[DRY RUN] Would push $ref to $MIRROR_REMOTE" + return 0 + fi + + if git push "$MIRROR_REMOTE" "$ref:$ref" --force; then + log_success "Synced $ref_type: $ref_name" + return 0 + else + log_error "Failed to sync $ref_type: $ref_name" + return 1 + fi +} + +# Full mirror sync +full_sync() { + log_info "Starting full mirror sync..." + + # Fetch latest from source + log_info "Fetching from $SOURCE_REMOTE..." + git fetch "$SOURCE_REMOTE" --prune --tags + + local exit_code=0 + + sync_all_branches || exit_code=1 + sync_tags || exit_code=1 + + return $exit_code +} + +# Main function +main() { + cd "$PROJECT_ROOT" + + log_info "Mirror sync utility" + echo "" + + local mode="push" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --mirror-url) MIRROR_URL="$2"; shift 2 ;; + --source) SOURCE_REMOTE="$2"; shift 2 ;; + --mirror) MIRROR_REMOTE="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --full) mode="full"; shift ;; + --push) mode="push"; shift ;; + *) shift ;; + esac + done + + # Setup remote if URL provided + if [[ -n "$MIRROR_URL" ]]; then + setup_mirror_remote || return 1 + fi + + # Verify mirror remote exists + if ! git remote get-url "$MIRROR_REMOTE" &>/dev/null; then + log_error "Mirror remote '$MIRROR_REMOTE' not configured. Set MIRROR_URL or add remote manually." + return 1 + fi + + local exit_code=0 + + case "$mode" in + push) + sync_on_push || exit_code=1 + ;; + full) + full_sync || exit_code=1 + ;; + esac + + echo "" + if [[ $exit_code -eq 0 ]]; then + log_success "Mirror sync completed successfully!" + else + log_error "Mirror sync completed with errors" + fi + + return $exit_code +} + +# Run if executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/ci-scripts/test.sh b/ci-scripts/test.sh new file mode 100755 index 0000000..2bbfe50 --- /dev/null +++ b/ci-scripts/test.sh @@ -0,0 +1,435 @@ +#!/usr/bin/env bash +# Universal Test Runner +# Runs tests based on detected frameworks and configuration +# Exit codes: 0 = all tests pass, 1 = test failures, 2 = no tests found + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" + +# Configuration +COVERAGE="${COVERAGE:-false}" +VERBOSE="${VERBOSE:-false}" +CI="${CI:-false}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Source detection script +source "$SCRIPT_DIR/detect.sh" 2>/dev/null || true + +# Track test results +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Run Node.js tests +run_nodejs_tests() { + log_info "Running Node.js tests..." + cd "$PROJECT_ROOT" + + local test_cmd="" + local npm_cmd="npm" + + # Detect package manager + [[ -f "pnpm-lock.yaml" ]] && npm_cmd="pnpm" + [[ -f "yarn.lock" ]] && npm_cmd="yarn" + [[ -f "bun.lockb" ]] && npm_cmd="bun" + + # Check package.json for test script + if [[ -f "package.json" ]]; then + if grep -q '"test"' package.json; then + test_cmd="$npm_cmd test" + fi + fi + + # Try specific frameworks + if [[ -z "$test_cmd" ]]; then + if [[ -f "jest.config.js" || -f "jest.config.ts" || -f "jest.config.json" ]]; then + test_cmd="npx jest" + elif [[ -f "vitest.config.js" || -f "vitest.config.ts" ]]; then + test_cmd="npx vitest run" + elif [[ -d "node_modules/.bin" ]]; then + [[ -f "node_modules/.bin/jest" ]] && test_cmd="npx jest" + [[ -f "node_modules/.bin/mocha" ]] && test_cmd="npx mocha" + [[ -f "node_modules/.bin/vitest" ]] && test_cmd="npx vitest run" + [[ -f "node_modules/.bin/ava" ]] && test_cmd="npx ava" + fi + fi + + if [[ -n "$test_cmd" ]]; then + ((TESTS_RUN++)) || true + + # Add coverage flag if requested + if [[ "$COVERAGE" == "true" ]]; then + [[ "$test_cmd" == *"jest"* ]] && test_cmd="$test_cmd --coverage" + [[ "$test_cmd" == *"vitest"* ]] && test_cmd="$test_cmd --coverage" + fi + + # Add CI flag if in CI environment + if [[ "$CI" == "true" ]]; then + [[ "$test_cmd" == *"jest"* ]] && test_cmd="$test_cmd --ci" + fi + + log_info "Running: $test_cmd" + if eval "$test_cmd"; then + ((TESTS_PASSED++)) || true + log_success "Node.js tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Node.js tests failed" + return 1 + fi + else + log_warn "No Node.js test configuration found" + fi +} + +# Run Python tests +run_python_tests() { + log_info "Running Python tests..." + cd "$PROJECT_ROOT" + + # Activate virtual environment if exists + if [[ -d ".venv" ]]; then + source .venv/bin/activate 2>/dev/null || true + elif [[ -d "venv" ]]; then + source venv/bin/activate 2>/dev/null || true + fi + + local test_cmd="" + + # Check for pytest + if command -v pytest &>/dev/null || [[ -f ".venv/bin/pytest" ]]; then + test_cmd="pytest" + [[ "$VERBOSE" == "true" ]] && test_cmd="$test_cmd -v" + [[ "$COVERAGE" == "true" ]] && test_cmd="$test_cmd --cov=. --cov-report=xml" + # Check for tox + elif [[ -f "tox.ini" ]] && command -v tox &>/dev/null; then + test_cmd="tox" + # Fallback to unittest + elif find . -name "test_*.py" -o -name "*_test.py" 2>/dev/null | grep -q .; then + test_cmd="python -m unittest discover" + fi + + if [[ -n "$test_cmd" ]]; then + ((TESTS_RUN++)) || true + + log_info "Running: $test_cmd" + if eval "$test_cmd"; then + ((TESTS_PASSED++)) || true + log_success "Python tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Python tests failed" + return 1 + fi + else + log_warn "No Python test configuration found" + fi +} + +# Run Ruby tests +run_ruby_tests() { + log_info "Running Ruby tests..." + cd "$PROJECT_ROOT" + + local test_cmd="" + + # Check for RSpec + if [[ -d "spec" ]]; then + test_cmd="bundle exec rspec" + # Check for Minitest + elif [[ -d "test" ]]; then + test_cmd="bundle exec rake test" + fi + + if [[ -n "$test_cmd" ]]; then + ((TESTS_RUN++)) || true + + log_info "Running: $test_cmd" + if eval "$test_cmd"; then + ((TESTS_PASSED++)) || true + log_success "Ruby tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Ruby tests failed" + return 1 + fi + else + log_warn "No Ruby test configuration found" + fi +} + +# Run Go tests +run_go_tests() { + log_info "Running Go tests..." + cd "$PROJECT_ROOT" + + local test_cmd="go test ./..." + [[ "$VERBOSE" == "true" ]] && test_cmd="$test_cmd -v" + [[ "$COVERAGE" == "true" ]] && test_cmd="$test_cmd -coverprofile=coverage.out" + + if find . -name "*_test.go" 2>/dev/null | grep -q .; then + ((TESTS_RUN++)) || true + + log_info "Running: $test_cmd" + if eval "$test_cmd"; then + ((TESTS_PASSED++)) || true + log_success "Go tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Go tests failed" + return 1 + fi + else + log_warn "No Go test files found" + fi +} + +# Run Rust tests +run_rust_tests() { + log_info "Running Rust tests..." + cd "$PROJECT_ROOT" + + if [[ -f "Cargo.toml" ]]; then + ((TESTS_RUN++)) || true + + local test_cmd="cargo test" + [[ "$VERBOSE" == "true" ]] && test_cmd="$test_cmd -- --nocapture" + + log_info "Running: $test_cmd" + if eval "$test_cmd"; then + ((TESTS_PASSED++)) || true + log_success "Rust tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Rust tests failed" + return 1 + fi + else + log_warn "No Cargo.toml found" + fi +} + +# Run Java/Maven tests +run_java_maven_tests() { + log_info "Running Maven tests..." + cd "$PROJECT_ROOT" + + if [[ -f "pom.xml" ]]; then + ((TESTS_RUN++)) || true + + log_info "Running: mvn test" + if mvn test -B; then + ((TESTS_PASSED++)) || true + log_success "Maven tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Maven tests failed" + return 1 + fi + fi +} + +# Run Java/Gradle tests +run_java_gradle_tests() { + log_info "Running Gradle tests..." + cd "$PROJECT_ROOT" + + if [[ -f "build.gradle" || -f "build.gradle.kts" ]]; then + ((TESTS_RUN++)) || true + + local gradle_cmd="gradle" + [[ -f "gradlew" ]] && gradle_cmd="./gradlew" + + log_info "Running: $gradle_cmd test" + if $gradle_cmd test; then + ((TESTS_PASSED++)) || true + log_success "Gradle tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Gradle tests failed" + return 1 + fi + fi +} + +# Run PHP tests +run_php_tests() { + log_info "Running PHP tests..." + cd "$PROJECT_ROOT" + + if [[ -f "phpunit.xml" || -f "phpunit.xml.dist" ]]; then + ((TESTS_RUN++)) || true + + local test_cmd="./vendor/bin/phpunit" + [[ "$COVERAGE" == "true" ]] && test_cmd="$test_cmd --coverage-text" + + log_info "Running: $test_cmd" + if eval "$test_cmd"; then + ((TESTS_PASSED++)) || true + log_success "PHP tests passed" + else + ((TESTS_FAILED++)) || true + log_error "PHP tests failed" + return 1 + fi + else + log_warn "No PHPUnit configuration found" + fi +} + +# Run .NET tests +run_dotnet_tests() { + log_info "Running .NET tests..." + cd "$PROJECT_ROOT" + + if find . -name "*.csproj" 2>/dev/null | grep -q .; then + ((TESTS_RUN++)) || true + + local test_cmd="dotnet test" + [[ "$COVERAGE" == "true" ]] && test_cmd="$test_cmd --collect:'XPlat Code Coverage'" + + log_info "Running: $test_cmd" + if eval "$test_cmd"; then + ((TESTS_PASSED++)) || true + log_success ".NET tests passed" + else + ((TESTS_FAILED++)) || true + log_error ".NET tests failed" + return 1 + fi + fi +} + +# Run shell script tests (BATS) +run_shell_tests() { + log_info "Running shell tests..." + cd "$PROJECT_ROOT" + + if [[ -d "test" ]] && find test -name "*.bats" 2>/dev/null | grep -q .; then + if command -v bats &>/dev/null; then + ((TESTS_RUN++)) || true + + log_info "Running: bats test/" + if bats test/*.bats; then + ((TESTS_PASSED++)) || true + log_success "Shell tests passed" + else + ((TESTS_FAILED++)) || true + log_error "Shell tests failed" + return 1 + fi + else + log_warn "BATS not installed, skipping shell tests" + fi + fi +} + +# Print test summary +print_summary() { + echo "" + echo "========================================" + echo " TEST SUMMARY" + echo "========================================" + echo "Test suites run: $TESTS_RUN" + echo "Test suites passed: $TESTS_PASSED" + echo "Test suites failed: $TESTS_FAILED" + echo "========================================" + + if [[ $TESTS_FAILED -gt 0 ]]; then + log_error "Some tests failed!" + return 1 + elif [[ $TESTS_RUN -eq 0 ]]; then + log_warn "No tests were run" + return 2 + else + log_success "All tests passed!" + return 0 + fi +} + +# Main function +main() { + log_info "Starting test runner in: $PROJECT_ROOT" + echo "" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --coverage) COVERAGE="true"; shift ;; + --verbose|-v) VERBOSE="true"; shift ;; + --ci) CI="true"; shift ;; + *) shift ;; + esac + done + + # Run detection if not already done + if [[ -z "${DETECTED_LANGUAGES:-}" ]]; then + source "$SCRIPT_DIR/detect.sh" + detect_languages + detect_package_managers + detect_test_frameworks + fi + + local exit_code=0 + + # Run tests based on detected languages/frameworks + IFS=',' read -ra languages <<< "${DETECTED_LANGUAGES:-}" + + for lang in "${languages[@]}"; do + case "$lang" in + javascript|typescript) + run_nodejs_tests || exit_code=1 + ;; + python) + run_python_tests || exit_code=1 + ;; + ruby) + run_ruby_tests || exit_code=1 + ;; + go) + run_go_tests || exit_code=1 + ;; + rust) + run_rust_tests || exit_code=1 + ;; + java|kotlin) + if [[ -f "$PROJECT_ROOT/pom.xml" ]]; then + run_java_maven_tests || exit_code=1 + elif [[ -f "$PROJECT_ROOT/build.gradle" || -f "$PROJECT_ROOT/build.gradle.kts" ]]; then + run_java_gradle_tests || exit_code=1 + fi + ;; + php) + run_php_tests || exit_code=1 + ;; + csharp) + run_dotnet_tests || exit_code=1 + ;; + shell) + run_shell_tests || exit_code=1 + ;; + esac + done + + print_summary || exit_code=$? + + return $exit_code +} + +# Run if executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/ci-scripts/verify-mirror.sh b/ci-scripts/verify-mirror.sh new file mode 100755 index 0000000..4597970 --- /dev/null +++ b/ci-scripts/verify-mirror.sh @@ -0,0 +1,311 @@ +#!/usr/bin/env bash +# Mirror Verification Script +# Verifies that two git remotes are in sync +# Exit codes: 0 = in sync, 1 = out of sync, 2 = error + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" + +# Configuration +SOURCE_REMOTE="${SOURCE_REMOTE:-origin}" +MIRROR_REMOTE="${MIRROR_REMOTE:-gitlab}" +VERBOSE="${VERBOSE:-false}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Fetch latest from remotes +fetch_remotes() { + log_info "Fetching from remotes..." + + git fetch "$SOURCE_REMOTE" --prune --tags 2>/dev/null || { + log_error "Failed to fetch from $SOURCE_REMOTE" + return 1 + } + + git fetch "$MIRROR_REMOTE" --prune --tags 2>/dev/null || { + log_warn "Failed to fetch from $MIRROR_REMOTE (may not be configured)" + return 1 + } + + log_success "Fetched from both remotes" +} + +# Compare branches between remotes +compare_branches() { + log_info "Comparing branches..." + + local source_branches mirror_branches + local missing_in_mirror=() + local missing_in_source=() + local out_of_sync=() + + # Get branch lists + source_branches=$(git branch -r | grep "^ $SOURCE_REMOTE/" | sed "s| $SOURCE_REMOTE/||" | grep -v HEAD || true) + mirror_branches=$(git branch -r | grep "^ $MIRROR_REMOTE/" | sed "s| $MIRROR_REMOTE/||" | grep -v HEAD || true) + + # Check for branches missing in mirror + while IFS= read -r branch; do + [[ -z "$branch" ]] && continue + if ! echo "$mirror_branches" | grep -q "^$branch$"; then + missing_in_mirror+=("$branch") + fi + done <<< "$source_branches" + + # Check for branches missing in source + while IFS= read -r branch; do + [[ -z "$branch" ]] && continue + if ! echo "$source_branches" | grep -q "^$branch$"; then + missing_in_source+=("$branch") + fi + done <<< "$mirror_branches" + + # Check for branches that exist in both but are out of sync + while IFS= read -r branch; do + [[ -z "$branch" ]] && continue + if echo "$mirror_branches" | grep -q "^$branch$"; then + local source_sha mirror_sha + source_sha=$(git rev-parse "$SOURCE_REMOTE/$branch" 2>/dev/null || echo "") + mirror_sha=$(git rev-parse "$MIRROR_REMOTE/$branch" 2>/dev/null || echo "") + + if [[ -n "$source_sha" && -n "$mirror_sha" && "$source_sha" != "$mirror_sha" ]]; then + out_of_sync+=("$branch") + fi + fi + done <<< "$source_branches" + + # Report results + echo "" + echo "========================================" + echo " BRANCH COMPARISON" + echo "========================================" + echo "" + + if [[ ${#missing_in_mirror[@]} -gt 0 ]]; then + log_warn "Branches missing in $MIRROR_REMOTE:" + printf ' - %s\n' "${missing_in_mirror[@]}" + echo "" + fi + + if [[ ${#missing_in_source[@]} -gt 0 ]]; then + log_warn "Branches missing in $SOURCE_REMOTE:" + printf ' - %s\n' "${missing_in_source[@]}" + echo "" + fi + + if [[ ${#out_of_sync[@]} -gt 0 ]]; then + log_error "Branches out of sync:" + printf ' - %s\n' "${out_of_sync[@]}" + echo "" + fi + + local total_issues=$((${#missing_in_mirror[@]} + ${#missing_in_source[@]} + ${#out_of_sync[@]})) + + if [[ $total_issues -eq 0 ]]; then + log_success "All branches are in sync!" + return 0 + else + return 1 + fi +} + +# Compare tags between remotes +compare_tags() { + log_info "Comparing tags..." + + local source_tags mirror_tags + local missing_in_mirror=() + local missing_in_source=() + local tag_mismatch=() + + # Get tag lists + source_tags=$(git tag -l --sort=-creatordate 2>/dev/null || true) + + # Get tags from mirror remote + mirror_tags=$(git ls-remote --tags "$MIRROR_REMOTE" 2>/dev/null | awk '{print $2}' | sed 's|refs/tags/||' | grep -v '\^{}' || true) + + # For this check, we'll compare local tags (which should be from source) + # with what the mirror reports + + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + if ! echo "$mirror_tags" | grep -q "^$tag$"; then + missing_in_mirror+=("$tag") + fi + done <<< "$source_tags" + + # Report results + echo "" + echo "========================================" + echo " TAG COMPARISON" + echo "========================================" + echo "" + + local source_count mirror_count + source_count=$(echo "$source_tags" | grep -c . || echo 0) + mirror_count=$(echo "$mirror_tags" | grep -c . || echo 0) + + echo "Tags in $SOURCE_REMOTE: $source_count" + echo "Tags in $MIRROR_REMOTE: $mirror_count" + echo "" + + if [[ ${#missing_in_mirror[@]} -gt 0 ]]; then + log_warn "Tags missing in $MIRROR_REMOTE (first 10):" + printf ' - %s\n' "${missing_in_mirror[@]:0:10}" + [[ ${#missing_in_mirror[@]} -gt 10 ]] && echo " ... and $((${#missing_in_mirror[@]} - 10)) more" + echo "" + return 1 + else + log_success "All tags are in sync!" + return 0 + fi +} + +# Verify commit history integrity +verify_history() { + log_info "Verifying commit history integrity..." + + local default_branch + default_branch=$(git symbolic-ref refs/remotes/"$SOURCE_REMOTE"/HEAD 2>/dev/null | sed "s|refs/remotes/$SOURCE_REMOTE/||" || echo "main") + + if ! git rev-parse "$SOURCE_REMOTE/$default_branch" &>/dev/null; then + default_branch="master" + fi + + echo "" + echo "========================================" + echo " HISTORY VERIFICATION" + echo "========================================" + echo "" + echo "Default branch: $default_branch" + + # Get commit counts + local source_commits + source_commits=$(git rev-list --count "$SOURCE_REMOTE/$default_branch" 2>/dev/null || echo 0) + echo "Commits in $SOURCE_REMOTE/$default_branch: $source_commits" + + if git rev-parse "$MIRROR_REMOTE/$default_branch" &>/dev/null; then + local mirror_commits + mirror_commits=$(git rev-list --count "$MIRROR_REMOTE/$default_branch" 2>/dev/null || echo 0) + echo "Commits in $MIRROR_REMOTE/$default_branch: $mirror_commits" + + if [[ "$source_commits" -eq "$mirror_commits" ]]; then + log_success "Commit counts match!" + else + log_warn "Commit counts differ (source: $source_commits, mirror: $mirror_commits)" + fi + else + log_warn "Cannot find $MIRROR_REMOTE/$default_branch" + fi + + # Verify latest commit SHA matches + local source_head mirror_head + source_head=$(git rev-parse "$SOURCE_REMOTE/$default_branch" 2>/dev/null || echo "unknown") + mirror_head=$(git rev-parse "$MIRROR_REMOTE/$default_branch" 2>/dev/null || echo "unknown") + + echo "" + echo "Latest commit on $SOURCE_REMOTE/$default_branch: ${source_head:0:12}" + echo "Latest commit on $MIRROR_REMOTE/$default_branch: ${mirror_head:0:12}" + + if [[ "$source_head" == "$mirror_head" ]]; then + log_success "HEAD commits match!" + return 0 + else + log_error "HEAD commits differ!" + return 1 + fi +} + +# Generate verification report +generate_report() { + local report_file="$PROJECT_ROOT/mirror-verification-report.txt" + + { + echo "Mirror Verification Report" + echo "==========================" + echo "Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "" + echo "Source Remote: $SOURCE_REMOTE" + echo "Mirror Remote: $MIRROR_REMOTE" + echo "" + + echo "Remote URLs:" + git remote -v + echo "" + + echo "Branches:" + git branch -r + echo "" + + echo "Tags (last 20):" + git tag -l --sort=-creatordate | head -20 + echo "" + + echo "Recent commits (source):" + git log --oneline -10 "$SOURCE_REMOTE/$(git symbolic-ref refs/remotes/$SOURCE_REMOTE/HEAD 2>/dev/null | sed "s|refs/remotes/$SOURCE_REMOTE/||" || echo main)" 2>/dev/null || echo "N/A" + + } > "$report_file" + + log_info "Report saved to: $report_file" +} + +# Main function +main() { + cd "$PROJECT_ROOT" + + log_info "Starting mirror verification" + log_info "Source remote: $SOURCE_REMOTE" + log_info "Mirror remote: $MIRROR_REMOTE" + echo "" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --source) SOURCE_REMOTE="$2"; shift 2 ;; + --mirror) MIRROR_REMOTE="$2"; shift 2 ;; + --verbose|-v) VERBOSE="true"; shift ;; + --report) generate_report; shift ;; + *) shift ;; + esac + done + + local exit_code=0 + + # Run verification steps + fetch_remotes || exit_code=2 + + if [[ $exit_code -eq 0 ]]; then + compare_branches || exit_code=1 + compare_tags || exit_code=1 + verify_history || exit_code=1 + fi + + echo "" + echo "========================================" + echo " FINAL RESULT" + echo "========================================" + + case $exit_code in + 0) log_success "Mirror verification passed! All remotes are in sync." ;; + 1) log_warn "Mirror verification found discrepancies." ;; + 2) log_error "Mirror verification failed due to errors." ;; + esac + + return $exit_code +} + +# Run if executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..0b0ccb2 --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Test runner script for the Universal Project Manager CI scripts +# Runs BATS tests if available, otherwise skips with a warning + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Install BATS if not available +install_bats() { + log_info "Installing BATS..." + + if command -v apt-get &>/dev/null; then + sudo apt-get update && sudo apt-get install -y bats + elif command -v brew &>/dev/null; then + brew install bats-core + elif command -v npm &>/dev/null; then + npm install -g bats + else + log_error "Cannot install BATS automatically. Please install it manually." + log_info "Visit: https://github.com/bats-core/bats-core" + return 1 + fi +} + +# Run BATS tests +run_bats_tests() { + log_info "Running BATS tests..." + + local test_files + test_files=$(find "$SCRIPT_DIR" -name "*.bats" 2>/dev/null || true) + + if [[ -z "$test_files" ]]; then + log_warn "No BATS test files found" + return 0 + fi + + if command -v bats &>/dev/null; then + bats "$SCRIPT_DIR"/*.bats + else + log_warn "BATS not installed. Skipping shell tests." + log_info "Install with: npm install -g bats" + return 0 + fi +} + +# Run shellcheck on all scripts +run_shellcheck() { + log_info "Running shellcheck on CI scripts..." + + if command -v shellcheck &>/dev/null; then + local scripts + scripts=$(find "$PROJECT_ROOT/ci-scripts" -name "*.sh" 2>/dev/null || true) + + if [[ -n "$scripts" ]]; then + echo "$scripts" | xargs shellcheck + log_success "shellcheck passed" + fi + else + log_warn "shellcheck not installed. Skipping static analysis." + fi +} + +# Main function +main() { + log_info "Starting test runner..." + echo "" + + local exit_code=0 + + # Run shellcheck + run_shellcheck || exit_code=1 + + echo "" + + # Run BATS tests + run_bats_tests || exit_code=1 + + echo "" + + if [[ $exit_code -eq 0 ]]; then + log_success "All tests passed!" + else + log_error "Some tests failed" + fi + + return $exit_code +} + +# Run if executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/tests/test_detect.bats b/tests/test_detect.bats new file mode 100755 index 0000000..8c084ea --- /dev/null +++ b/tests/test_detect.bats @@ -0,0 +1,149 @@ +#!/usr/bin/env bats +# Tests for ci-scripts/detect.sh + +setup() { + # Get the directory containing the test file + TEST_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)" + PROJECT_ROOT="$(cd "$TEST_DIR/.." && pwd)" + + # Source the detect script + source "$PROJECT_ROOT/ci-scripts/detect.sh" + + # Create a temporary directory for test fixtures + TEST_TEMP="$(mktemp -d)" + export PROJECT_ROOT="$TEST_TEMP" +} + +teardown() { + # Clean up temporary directory + rm -rf "$TEST_TEMP" +} + +@test "detect_languages finds JavaScript when package.json exists" { + touch "$TEST_TEMP/package.json" + detect_languages + + [[ "$DETECTED_LANGUAGES" == *"javascript"* ]] +} + +@test "detect_languages finds Python when requirements.txt exists" { + touch "$TEST_TEMP/requirements.txt" + detect_languages + + [[ "$DETECTED_LANGUAGES" == *"python"* ]] +} + +@test "detect_languages finds Go when go.mod exists" { + touch "$TEST_TEMP/go.mod" + detect_languages + + [[ "$DETECTED_LANGUAGES" == *"go"* ]] +} + +@test "detect_languages finds Rust when Cargo.toml exists" { + touch "$TEST_TEMP/Cargo.toml" + detect_languages + + [[ "$DETECTED_LANGUAGES" == *"rust"* ]] +} + +@test "detect_languages finds Ruby when Gemfile exists" { + touch "$TEST_TEMP/Gemfile" + detect_languages + + [[ "$DETECTED_LANGUAGES" == *"ruby"* ]] +} + +@test "detect_package_managers finds npm when package-lock.json exists" { + touch "$TEST_TEMP/package-lock.json" + detect_package_managers + + [[ "$DETECTED_PACKAGE_MANAGERS" == *"npm"* ]] +} + +@test "detect_package_managers finds yarn when yarn.lock exists" { + touch "$TEST_TEMP/yarn.lock" + detect_package_managers + + [[ "$DETECTED_PACKAGE_MANAGERS" == *"yarn"* ]] +} + +@test "detect_package_managers finds pip when requirements.txt exists" { + touch "$TEST_TEMP/requirements.txt" + detect_package_managers + + [[ "$DETECTED_PACKAGE_MANAGERS" == *"pip"* ]] +} + +@test "detect_package_managers finds cargo when Cargo.toml exists" { + touch "$TEST_TEMP/Cargo.toml" + detect_package_managers + + [[ "$DETECTED_PACKAGE_MANAGERS" == *"cargo"* ]] +} + +@test "detect_test_frameworks finds pytest when pytest.ini exists" { + touch "$TEST_TEMP/pytest.ini" + detect_test_frameworks + + [[ "$DETECTED_TEST_FRAMEWORKS" == *"pytest"* ]] +} + +@test "detect_test_frameworks finds rspec when spec directory exists" { + mkdir -p "$TEST_TEMP/spec" + detect_test_frameworks + + [[ "$DETECTED_TEST_FRAMEWORKS" == *"rspec"* ]] +} + +@test "detect_test_frameworks finds jest when package.json contains jest" { + echo '{"devDependencies": {"jest": "^29.0.0"}}' > "$TEST_TEMP/package.json" + detect_test_frameworks + + [[ "$DETECTED_TEST_FRAMEWORKS" == *"jest"* ]] +} + +@test "detect_build_systems finds make when Makefile exists" { + touch "$TEST_TEMP/Makefile" + detect_build_systems + + [[ "$DETECTED_BUILD_SYSTEMS" == *"make"* ]] +} + +@test "detect_build_systems finds cmake when CMakeLists.txt exists" { + touch "$TEST_TEMP/CMakeLists.txt" + detect_build_systems + + [[ "$DETECTED_BUILD_SYSTEMS" == *"cmake"* ]] +} + +@test "detect_build_systems finds docker when Dockerfile exists" { + touch "$TEST_TEMP/Dockerfile" + detect_build_systems + + [[ "$DETECTED_BUILD_SYSTEMS" == *"docker"* ]] +} + +@test "PRIMARY_LANGUAGE is set to first detected language" { + touch "$TEST_TEMP/package.json" + touch "$TEST_TEMP/requirements.txt" + detect_languages + + # Should be the first language detected + [[ -n "$PRIMARY_LANGUAGE" ]] +} + +@test "generate_json_output produces valid JSON structure" { + touch "$TEST_TEMP/package.json" + detect_languages + detect_package_managers + detect_test_frameworks + detect_build_systems + + output=$(generate_json_output) + + # Check it contains expected keys + [[ "$output" == *'"languages"'* ]] + [[ "$output" == *'"primary_language"'* ]] + [[ "$output" == *'"package_managers"'* ]] +}