diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0a9e773..9702924 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,157 +1,151 @@ stages: - - build - - lint - - test - - publish - - deploy + - build + - lint + - test + - publish + - deploy before_script: - - 'export DOTNET_CLI_TELEMETRY_OPTOUT=1' - - 'export PATH=$PATH:$HOME/.dotnet/tools' - - 'which jb || dotnet tool install JetBrains.ReSharper.GlobalTools --global --add-source https://nuget.aiursoft.com/v3/index.json --configfile ./nuget.config -v d' - - 'which reportgenerator || dotnet tool install dotnet-reportgenerator-globaltool --global --add-source https://nuget.aiursoft.com/v3/index.json --configfile ./nuget.config -v d' - - 'echo "Hostname: $(hostname)"' - - 'dotnet --info' + - "export DOTNET_CLI_TELEMETRY_OPTOUT=1" + - "export PATH=$PATH:$HOME/.dotnet/tools" + - "which jb || dotnet tool install JetBrains.ReSharper.GlobalTools --global --add-source https://nuget.aiursoft.com/v3/index.json --configfile ./nuget.config -v d" + - "which reportgenerator || dotnet tool install dotnet-reportgenerator-globaltool --global --add-source https://nuget.aiursoft.com/v3/index.json --configfile ./nuget.config -v d" + - 'echo "Hostname: $(hostname)"' + - "dotnet --info" variables: - GIT_CLONE_PATH: '$CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_PIPELINE_ID' + GIT_CLONE_PATH: "$CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_PIPELINE_ID" restore: - stage: build - script: - - | - dotnet restore --no-cache --configfile nuget.config || \ - (echo "Restore failed. Retrying in 10 seconds..." && sleep 10 && dotnet restore --no-cache --configfile nuget.config) || \ - (echo "Restore failed again. Retrying in 20 seconds..." && sleep 20 && dotnet restore --no-cache --configfile nuget.config) + stage: build + script: + - | + dotnet restore --no-cache --configfile nuget.config || \ + (echo "Restore failed. Retrying in 10 seconds..." && sleep 10 && dotnet restore --no-cache --configfile nuget.config) || \ + (echo "Restore failed again. Retrying in 20 seconds..." && sleep 20 && dotnet restore --no-cache --configfile nuget.config) build: - stage: build - needs: - - restore - script: - - dotnet build -maxcpucount:1 --no-self-contained + stage: build + needs: + - restore + script: + - dotnet build -maxcpucount:1 --no-self-contained lint: - stage: lint - needs: - - build - script: - # 3 times retry because sometimes the first time will fail - - jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml || jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml || jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml - # Remove the warning of UnusedAutoPropertyAccessor InconsistentNaming - - sed -i '/InconsistentNaming/d' analyze_output.xml - - sed -i '/AssignNullToNotNullAttribute/d' analyze_output.xml # This is because jetbrains is not smart enough to understand the nullability of C# 8.0 - - sed -i '/UnusedAutoPropertyAccessor/d' analyze_output.xml - - sed -i '/DuplicateResource/d' analyze_output.xml - - grep 'WARNING' analyze_output.xml && cat analyze_output.xml && exit 1 || echo "No warning found" - artifacts: - when: always - expire_in: 1 day - paths: - - ./analyze_output.xml + stage: lint + needs: + - build + script: + - chmod +x ./lint.sh + - ./lint.sh + artifacts: + when: always + expire_in: 1 day + paths: + - ./analyze_output.xml test: - stage: test - needs: - - build - coverage: '/TOTAL_COVERAGE=(\d+.\d+)/' - script: - - dotnet test *.sln --collect:"XPlat Code Coverage" --logger "junit;MethodFormat=Class;FailureBodyFormat=Verbose" - - reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"." -reporttypes:"cobertura" - - COVERAGE_VALUE=$(grep -oPm 1 'line-rate="\K([0-9.]+)' "./Cobertura.xml") - - COVERAGE_PERCENTAGE=$(echo "scale=2; $COVERAGE_VALUE * 100" | bc) - - 'echo "TOTAL_COVERAGE=$COVERAGE_PERCENTAGE%"' - artifacts: - when: always - expire_in: 1 day - paths: - - ./**/TestResults.xml - - ./Cobertura.xml - reports: - junit: - - ./**/TestResults.xml - coverage_report: - coverage_format: cobertura - path: ./Cobertura.xml + stage: test + needs: + - build + coverage: '/TOTAL_COVERAGE=(\d+.\d+)/' + script: + - dotnet test *.sln --collect:"XPlat Code Coverage" --logger "junit;MethodFormat=Class;FailureBodyFormat=Verbose" + - reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"." -reporttypes:"cobertura" + - COVERAGE_VALUE=$(grep -oPm 1 'line-rate="\K([0-9.]+)' "./Cobertura.xml") + - COVERAGE_PERCENTAGE=$(echo "scale=2; $COVERAGE_VALUE * 100" | bc) + - 'echo "TOTAL_COVERAGE=$COVERAGE_PERCENTAGE%"' + artifacts: + when: always + expire_in: 1 day + paths: + - ./**/TestResults.xml + - ./Cobertura.xml + reports: + junit: + - ./**/TestResults.xml + coverage_report: + coverage_format: cobertura + path: ./Cobertura.xml pack: - stage: publish - needs: - - lint - - test - script: - - dotnet build -maxcpucount:1 --configuration Release --no-self-contained *.sln - - dotnet pack -maxcpucount:1 --configuration Release *.sln || echo "Some packaging failed!" - artifacts: - expire_in: 1 week - paths: - - '**/*.nupkg' + stage: publish + needs: + - lint + - test + script: + - dotnet build -maxcpucount:1 --configuration Release --no-self-contained *.sln + - dotnet pack -maxcpucount:1 --configuration Release *.sln || echo "Some packaging failed!" + artifacts: + expire_in: 1 week + paths: + - "**/*.nupkg" deploy_local_nuget: - stage: deploy - environment: production - needs: - - pack - dependencies: - - pack - script: - - | - for file in $(find . -name "*.nupkg"); do - dotnet nuget push "$file" --api-key "$LOCAL_NUGET_API_KEY" --source "https://nuget.aiursoft.com/v3/index.json" --skip-duplicate || exit 1; - done - only: - - master + stage: deploy + environment: production + needs: + - pack + dependencies: + - pack + script: + - | + for file in $(find . -name "*.nupkg"); do + dotnet nuget push "$file" --api-key "$LOCAL_NUGET_API_KEY" --source "https://nuget.aiursoft.com/v3/index.json" --skip-duplicate || exit 1; + done + only: + - master deploy_public_nuget: - stage: deploy - environment: production - needs: - - pack - - deploy_local_nuget - dependencies: - - pack - script: - - | - for file in $(find . -name "*.nupkg"); do - dotnet nuget push "$file" --api-key "$NUGET_API_KEY" --source "https://api.nuget.org/v3/index.json" --skip-duplicate || exit 1; - done - only: - - master + stage: deploy + environment: production + needs: + - pack + - deploy_local_nuget + dependencies: + - pack + script: + - | + for file in $(find . -name "*.nupkg"); do + dotnet nuget push "$file" --api-key "$NUGET_API_KEY" --source "https://api.nuget.org/v3/index.json" --skip-duplicate || exit 1; + done + only: + - master deploy_docker_registry: - stage: deploy - environment: production - needs: - - lint - - test - script: - - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then TAG="latest"; else TAG="$CI_COMMIT_REF_NAME"; fi - - echo building image hub.aiursoft.com/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$TAG - - docker build . -t hub.aiursoft.com/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest - - echo "Logging in to Docker Registry hub.aiursoft.com..." - - echo "$LOCAL_DOCKER_PASSWORD" | docker login hub.aiursoft.com -u "$LOCAL_DOCKER_USERNAME" --password-stdin - - docker save hub.aiursoft.com/${CI_PROJECT_NAMESPACE}/$CI_PROJECT_NAME:$TAG -o temp.tar - - regctl image import hub.aiursoft.com/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$TAG temp.tar - - rm ./temp.tar - rules: - - if: '$CI_COMMIT_BRANCH == "master"' - exists: - - Dockerfile + stage: deploy + environment: production + needs: + - lint + - test + script: + - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then TAG="latest"; else TAG="$CI_COMMIT_REF_NAME"; fi + - echo building image hub.aiursoft.com/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$TAG + - docker build . -t hub.aiursoft.com/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest + - echo "Logging in to Docker Registry hub.aiursoft.com..." + - echo "$LOCAL_DOCKER_PASSWORD" | docker login hub.aiursoft.com -u "$LOCAL_DOCKER_USERNAME" --password-stdin + - docker save hub.aiursoft.com/${CI_PROJECT_NAMESPACE}/$CI_PROJECT_NAME:$TAG -o temp.tar + - regctl image import hub.aiursoft.com/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$TAG temp.tar + - rm ./temp.tar + rules: + - if: '$CI_COMMIT_BRANCH == "master"' + exists: + - Dockerfile deploy_docker_hub: - stage: deploy - environment: production - needs: - - deploy_docker_registry - script: - - if [ "$CI_PROJECT_NAMESPACE" = "anduin" ]; then NAMESPACE="anduin2019"; else NAMESPACE="$CI_PROJECT_NAMESPACE"; fi - - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then TAG="latest"; else TAG="$CI_COMMIT_REF_NAME"; fi - - echo building image $NAMESPACE/$CI_PROJECT_NAME:$TAG - - docker build . -t $NAMESPACE/$CI_PROJECT_NAME:$TAG - - echo "Logging in to Docker Hub..." - - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker push $NAMESPACE/$CI_PROJECT_NAME:$TAG - rules: - - if: '$CI_COMMIT_BRANCH == "master"' - exists: - - Dockerfile + stage: deploy + environment: production + needs: + - deploy_docker_registry + script: + - if [ "$CI_PROJECT_NAMESPACE" = "anduin" ]; then NAMESPACE="anduin2019"; else NAMESPACE="$CI_PROJECT_NAMESPACE"; fi + - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then TAG="latest"; else TAG="$CI_COMMIT_REF_NAME"; fi + - echo building image $NAMESPACE/$CI_PROJECT_NAME:$TAG + - docker build . -t $NAMESPACE/$CI_PROJECT_NAME:$TAG + - echo "Logging in to Docker Hub..." + - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin + - docker push $NAMESPACE/$CI_PROJECT_NAME:$TAG + rules: + - if: '$CI_COMMIT_BRANCH == "master"' + exists: + - Dockerfile diff --git a/lint.sh b/lint.sh new file mode 100644 index 0000000..9ceba4e --- /dev/null +++ b/lint.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# Configuration matches .gitlab-ci.yml +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export PATH=$PATH:$HOME/.dotnet/tools + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Cleanup only if not in CI (GitLab CI sets the CI variable) +if [ -z "$CI" ]; then + trap 'rm -f analyze_output.xml' EXIT +fi + +echo "Checking for JetBrains ReSharper Global Tools..." +if ! command -v jb &> /dev/null; then + echo "jb not found, installing..." + dotnet tool install JetBrains.ReSharper.GlobalTools --global --add-source https://nuget.aiursoft.com/v3/index.json --configfile ./nuget.config -v d +else + echo "jb is already installed." +fi + +echo "Restoring dependencies..." +dotnet restore --no-cache --configfile nuget.config || \ +(echo "Restore failed. Retrying in 10 seconds..." && sleep 10 && dotnet restore --no-cache --configfile nuget.config) + +echo "Running ReSharper Code Inspection..." +# 3 times retry because sometimes the first time will fail (copied from CI) +if ! jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml &> /dev/null; then + echo "First attempt failed, retrying..." + if ! jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml &> /dev/null; then + echo "Second attempt failed, retrying..." + jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml &> /dev/null + fi +fi + +echo "Filtering known false positives..." +# Current filters from .gitlab-ci.yml +sed -i '/InconsistentNaming/d' analyze_output.xml +sed -i '/AssignNullToNotNullAttribute/d' analyze_output.xml +sed -i '/UnusedAutoPropertyAccessor/d' analyze_output.xml +sed -i '/DuplicateResource/d' analyze_output.xml + +# Check for warnings +# Mimic .gitlab-ci.yml logic: Fail if 'WARNING' string is found. +if grep -q 'WARNING' analyze_output.xml; then + echo -e "${RED}Linting FAILED!${NC}" + echo "Issues found:" + + # Filter issues to only show those with Severity="WARNING" or "ERROR" + # Identify IssueTypes that are Warnings or Errors + # We use grep and cut to extract the IDs purely from lines with Severity="WARNING" or "ERROR" + WARNING_IDS=$(grep -E 'Severity="(WARNING|ERROR)"' analyze_output.xml | grep -o 'Id="[^"]*"' | cut -d'"' -f2) + + # Check if we found any IDs (to avoid syntax errors in loop if empty, though unlikely if grep passed) + if [ ! -z "$WARNING_IDS" ]; then + for ID in $WARNING_IDS; do + # Find issues matching this TypeId + # We assume one issue per line + grep "TypeId=\"$ID\"" analyze_output.xml | while read -r line; do + # Extract attributes using grep -o (lazy parsing) + FILE=$(echo "$line" | grep -o 'File="[^"]*"' | cut -d'"' -f2 | sed 's/\\/\//g') + LINE=$(echo "$line" | grep -o 'Line="[^"]*"' | cut -d'"' -f2) + if [ -z "$LINE" ]; then + OFFSET=$(echo "$line" | grep -o 'Offset="[^"]*"' | cut -d'"' -f2) + if [ ! -z "$OFFSET" ]; then + LINE="Offset $OFFSET" + else + LINE="Unknown" + fi + fi + MSG=$(echo "$line" | grep -o 'Message="[^"]*"' | cut -d'"' -f2) + + echo "File: $FILE | Line: $LINE | Reason: $MSG" + done + done + else + echo "Warning severity found in header, but no specific IssueType IDs extracted. Check XML format." + fi + + exit 1 +else + echo -e "${GREEN}Linting PASSED! No warnings found.${NC}" +fi diff --git a/ninja.yaml b/ninja.yaml index 93784f4..f27c891 100644 --- a/ninja.yaml +++ b/ninja.yaml @@ -6,6 +6,8 @@ files: contentUri: https://gitlab.aiursoft.com/aiursoft/tracer/-/raw/master/.gitignore - name: .gitlab-ci.yml contentUri: https://gitlab.aiursoft.com/aiursoft/tracer/-/raw/master/.gitlab-ci.yml + - name: lint.sh + contentUri: https://gitlab.aiursoft.com/aiursoft/tracer/-/raw/master/lint.sh - name: LICENSE contentUri: https://gitlab.aiursoft.com/aiursoft/tracer/-/raw/master/LICENSE - name: CODE_OF_CONDUCT.md @@ -14,4 +16,4 @@ files: contentUri: https://gitlab.aiursoft.com/aiursoft/tracer/-/raw/master/ninja.yaml - name: nuget.config contentUri: https://gitlab.aiursoft.com/aiursoft/tracer/-/raw/master/nuget.config - - name: README.md \ No newline at end of file + - name: README.md