Compare commits
100 Commits
6fd124d0fa
...
feature/Pi
| Author | SHA1 | Date | |
|---|---|---|---|
| 13123ed961 | |||
| a8760ac3bc | |||
| 6efdbcb8be | |||
| df746b77aa | |||
| ba56e51d01 | |||
| d1b8aa9230 | |||
| 030e7a30d6 | |||
| 3378c8ead4 | |||
| e1015da044 | |||
| 4a67d7c3f1 | |||
| 0241ca1fde | |||
| fd309858cf | |||
| 8d2add8bfb | |||
| 2476e81bea | |||
| be66b16671 | |||
| 12f7a79daf | |||
| 99bb20b1e0 | |||
| dd44689f29 | |||
| 3797459037 | |||
| 0a5b383329 | |||
| f353f1d5a7 | |||
| fd8be556ec | |||
| a1c9006e77 | |||
| 40e6c00543 | |||
| 1904c353db | |||
| a8ddaff131 | |||
| b271b32bf3 | |||
| a553770fc3 | |||
| 87636453eb | |||
| 8eeb6389df | |||
| 895f7a9a9c | |||
| 67bd56d9ef | |||
| 165a89f475 | |||
| aecf3d33b4 | |||
| 1e95f8e1a5 | |||
| 474fc97629 | |||
| 701ace2f0f | |||
| d44c393614 | |||
| 6c275b9ca3 | |||
| d84efc9509 | |||
| 9d3ac40a2f | |||
| 8ca8665cda | |||
| a643301db0 | |||
| 542c256097 | |||
| 1307d97505 | |||
| ee61de4181 | |||
| 77abe5b850 | |||
| 0fb7adbf42 | |||
| 5003990575 | |||
| 9b75f67475 | |||
| 3601d03214 | |||
| 5ffecc4cdf | |||
| 3b6e4260f4 | |||
| 11bad428ad | |||
| 914dcf94da | |||
| 1f96df217a | |||
| ff9695cfff | |||
| 0eb6f5357b | |||
| 461b3e59fa | |||
| ccee999ea3 | |||
| 2ecf448b74 | |||
| c4c0364f0a | |||
| b981b629bc | |||
| 9f5cbd3d4e | |||
| 625cd8e7e3 | |||
| 98d9e43787 | |||
| 58277268e3 | |||
| 02664443b9 | |||
| 998b11b2de | |||
| 318fef7348 | |||
| 24cd276eb2 | |||
| cf2350d3a1 | |||
| ee8d64a2f6 | |||
| 3937da50a3 | |||
| 2e2d8f5df4 | |||
| 981562eb82 | |||
| 94f9ff0154 | |||
| 9d1805deee | |||
| 6111148009 | |||
| 0706abed62 | |||
| d7c34b9180 | |||
| 5e12091d95 | |||
| 5efc4ac9d7 | |||
| a00f44b4c7 | |||
| 3effc2de9b | |||
| 99fda0c88e | |||
| 0e066171b0 | |||
| bc062975f8 | |||
| 5fbeaac3e3 | |||
| ed0d01e924 | |||
| 6215f1acbe | |||
| 5c981f5c7d | |||
| b797d2a6d2 | |||
| ff0bb40028 | |||
| 8f0d2dd59e | |||
| c84923c01d | |||
| 1f9252d7e0 | |||
| ccb23dd7bf | |||
| 258cf53b4b | |||
| d1c08efd7f |
6
.idea/.idea.FA/.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/.idea.FA/.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.ask.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AskMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/.idea.FA/.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Ask2AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/.idea.FA/.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EditMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
<changelist name="Uncommitted_changes_before_Checkout_at_25_01_2026,_12_47_[Changes]" date="1769341635822" recycled="false" toDelete="true">
|
||||
<option name="PATH" value="$PROJECT_DIR$/.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Checkout_at_25_01_2026,_12_47_[Changes]/shelved.patch" />
|
||||
<option name="DESCRIPTION" value="Uncommitted changes before Checkout at 25/01/2026, 12:47 [Changes]" />
|
||||
</changelist>
|
||||
File diff suppressed because one or more lines are too long
4
.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Update_at_24_08_2025__08_18__Changes_.xml
generated
Normal file
4
.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Update_at_24_08_2025__08_18__Changes_.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<changelist name="Uncommitted_changes_before_Update_at_24_08_2025,_08_18_[Changes]" date="1756016300553" recycled="false" toDelete="true">
|
||||
<option name="PATH" value="$PROJECT_DIR$/.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Update_at_24_08_2025,_08_18_[Changes]/shelved.patch" />
|
||||
<option name="DESCRIPTION" value="Uncommitted changes before Update at 24/08/2025, 08:18 [Changes]" />
|
||||
</changelist>
|
||||
@@ -124,11 +124,15 @@ steps:
|
||||
dotnet test --configuration Release --logger "trx;LogFileName=tests.trx" --results-directory TestResults
|
||||
done < changed-projects.txt
|
||||
depends_on: [restore]
|
||||
ai-agent-gate:
|
||||
pipeline-agent:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
environment:
|
||||
OPENAI_API_KEY:
|
||||
from_secret: openai_api_key
|
||||
VAULT_URL:
|
||||
from_secret: vault_url
|
||||
VAULT_TOKEN:
|
||||
from_secret: vault_token
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
@@ -139,14 +143,8 @@ steps:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== AI-Gate: analiza wyników testów: ==="
|
||||
if [ ! -f "TestResults/tests.trx" ]; then
|
||||
echo "Brak pliku TestResults/tests.trx – blokuję pipeline."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dotnet run --project PipelineAgent/PipelineAgent.csproj "test-gate" "TestResults/tests.trx"
|
||||
depends_on: [test]
|
||||
dotnet run --project PipelineAgent/PipelineAgent.csproj "check-changes" "$(pwd)"
|
||||
depends_on: [detect-changed-projects]
|
||||
pack-datamodels:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
@@ -195,7 +193,8 @@ steps:
|
||||
done < changed-projects.txt
|
||||
echo "=== Spakowane pakiety ==="
|
||||
ls -la nupkg/ || echo "Brak spakowanych pakietów!"
|
||||
depends_on: [ai-agent-gate]
|
||||
#depends_on: [ai-agent-gate]
|
||||
depends_on: [test]
|
||||
|
||||
publish-linux-local:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
@@ -248,7 +247,8 @@ steps:
|
||||
else
|
||||
echo "Brak aplikacji LinuxLocal do publikacji"
|
||||
fi
|
||||
depends_on: [ai-agent-gate]
|
||||
#depends_on: [ai-agent-gate]
|
||||
depends_on: [test]
|
||||
|
||||
publish-datamodels-to-baget:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
|
||||
491
.woodpecker/merge_to_master.yml
Normal file
491
.woodpecker/merge_to_master.yml
Normal file
@@ -0,0 +1,491 @@
|
||||
skip_clone: true
|
||||
when:
|
||||
event: pull_request
|
||||
action:
|
||||
- merge
|
||||
branch: master
|
||||
steps:
|
||||
clone-manual:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
remote: http://gitea:3000/${CI_REPO}.git
|
||||
branch: ${CI_COMMIT_BRANCH}
|
||||
depth: 1
|
||||
detect-changed-projects:
|
||||
image: alpine
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
echo "CI_PIPELINE_FILES=${CI_PIPELINE_FILES:-<empty>}"
|
||||
|
||||
apk add --no-cache findutils
|
||||
|
||||
> changed-projects.txt
|
||||
|
||||
echo "${CI_PIPELINE_FILES:-}" \
|
||||
| sed 's/^\[//; s/\]$//' \
|
||||
| tr ',' '\n' \
|
||||
| sed 's/^ *//; s/ *$//' \
|
||||
| sed '/^$/d' \
|
||||
| while read path; do
|
||||
echo "→ changed file: $path"
|
||||
|
||||
dir=$(dirname "$path")
|
||||
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
|
||||
csproj=$(find "$dir" -maxdepth 1 -name "*.csproj" -type f | head -n 1 || true)
|
||||
if [ -n "$csproj" ]; then
|
||||
echo "$csproj" >> changed-projects.txt
|
||||
break
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
done
|
||||
done
|
||||
|
||||
# unikalne projekty
|
||||
sort -u -o changed-projects.txt changed-projects.txt
|
||||
|
||||
echo "Zmienione projekty:"
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pipeline zakonczony."
|
||||
# exit 0 = sukces, wiec kolejne kroki, ktore od niego zaleza, nie poleca
|
||||
exit 0
|
||||
fi
|
||||
cat changed-projects.txt
|
||||
depends_on: [clone-manual]
|
||||
debug-location:
|
||||
image: alpine
|
||||
commands:
|
||||
- pwd
|
||||
- ls -la
|
||||
- find /woodpecker -maxdepth 4 -type d 2>/dev/null
|
||||
depends_on: [detect-changed-projects]
|
||||
restore:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
echo "Aktualna ścieżka do pliku: $(pwd)"
|
||||
echo "=== Restore wszystkich projektów ==="
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektów – nic do restore."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat <<EOF > NuGet.Config
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
|
||||
echo "=== Restore zmienionych projektów ==="
|
||||
while read csproj; do
|
||||
echo "→ dotnet restore $csproj"
|
||||
dotnet restore "$csproj"
|
||||
done < changed-projects.txt
|
||||
|
||||
# find . -name "*.csproj" -type f -exec dotnet restore "{}" \;
|
||||
depends_on: [detect-changed-projects]
|
||||
test:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam test."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== Konfiguracja NuGet sources ==="
|
||||
cat <<EOF > NuGet.Config
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
|
||||
echo "=== Uruchamianie testów dla zmienionych projektow ==="
|
||||
while read csproj; do
|
||||
echo "→ dotnet test $csproj"
|
||||
dotnet test --configuration Release --logger "trx;LogFileName=tests.trx" --results-directory TestResults
|
||||
done < changed-projects.txt
|
||||
depends_on: [restore]
|
||||
pack-datamodels:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam pack-datamodels."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== Instalacja MinVer CLI ==="
|
||||
dotnet tool install --tool-path /tmp/minver minver-cli
|
||||
echo "=== Obliczona wersja przez MinVer ==="
|
||||
MINVER_VERSION=$(/tmp/minver/minver \
|
||||
--auto-increment patch \
|
||||
--minimum-major-minor 2.0 \
|
||||
--default-pre-release-identifiers alpha.0 \
|
||||
--verbosity info)
|
||||
echo "Wersja: $MINVER_VERSION"
|
||||
mkdir -p nupkg
|
||||
echo "=== Pełniejsze fetch git (unshallow + tags) ==="
|
||||
git fetch --prune --unshallow || echo "Już full clone – OK"
|
||||
git fetch --tags
|
||||
echo "=== Dostępne tagi ==="
|
||||
git tag -l
|
||||
echo "=== Aktualny commit i opis ==="
|
||||
git describe --tags --always --dirty
|
||||
echo "=== Diagnostyka projektów ==="
|
||||
find . -name "*.csproj" -type f | sort
|
||||
mkdir -p nupkg
|
||||
echo "=== Pakowanie projektów DataModel (wykrywane po obecności <PackageId>) ==="
|
||||
while read csproj; do
|
||||
if grep -q '<PackageId>' "$csproj"; then
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
echo "→ Pakuję $PROJECT_NAME ($csproj)"
|
||||
dotnet pack "$csproj" \
|
||||
--configuration Release \
|
||||
-o "./nupkg" \
|
||||
/p:PackageVersion=$MINVER_VERSION
|
||||
else
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
echo "→ Pomijam $PROJECT_NAME – brak <PackageId>"
|
||||
fi
|
||||
done < changed-projects.txt
|
||||
echo "=== Spakowane pakiety ==="
|
||||
ls -la nupkg/ || echo "Brak spakowanych pakietów!"
|
||||
depends_on: [test]
|
||||
|
||||
publish-linux-local:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam AI-gate."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p linux-apps
|
||||
|
||||
echo "=== Konfiguracja NuGet sources ==="
|
||||
cat <<EOF > NuGet.Config
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
|
||||
echo "=== Publikacja aplikacji LinuxLocal ==="
|
||||
#find . -name "*.csproj" -type f | while read csproj; do
|
||||
while read csproj; do
|
||||
PROJECT_TYPE=$(dotnet msbuild "$csproj" -getProperty:ProjectType -noLogo 2>/dev/null || echo "Unknown")
|
||||
|
||||
if [ "$PROJECT_TYPE" = "LinuxLocal" ]; then
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
echo "→ Publishing $PROJECT_NAME"
|
||||
|
||||
dotnet publish "$csproj" \
|
||||
--configuration Release \
|
||||
--output "./linux-apps/$PROJECT_NAME" \
|
||||
--runtime linux-x64 \
|
||||
--self-contained false
|
||||
fi
|
||||
#done
|
||||
done < changed-projects.txt
|
||||
|
||||
echo ""
|
||||
if [ -d "./linux-apps" ] && [ "$(ls -A ./linux-apps)" ]; then
|
||||
echo "Opublikowane aplikacje:"
|
||||
ls -la linux-apps/
|
||||
else
|
||||
echo "Brak aplikacji LinuxLocal do publikacji"
|
||||
fi
|
||||
depends_on: [test]
|
||||
|
||||
publish-datamodels-to-baget:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
environment:
|
||||
BAGETTER_API_KEY:
|
||||
from_secret: baget_api_key
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam publish-datamodels-to-baget."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== Test połączenia z BaGetter ==="
|
||||
curl -f http://baget:80/v3/index.json || echo "Nie można połączyć się z BaGetter!"
|
||||
cat <<EOF > NuGet.Config
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
echo "=== Publikacja pakietów do BaGetter ==="
|
||||
find ./nupkg -name "*.nupkg" -type f | while read pkg; do
|
||||
echo "→ Push $(basename "$pkg")"
|
||||
dotnet nuget push "$pkg" \
|
||||
--source "BaGet" \
|
||||
--api-key "$BAGETTER_API_KEY" \
|
||||
--skip-duplicate
|
||||
done
|
||||
depends_on: [pack-datamodels]
|
||||
|
||||
select-projects-for-container:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
> projects-to-build.txt
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam select-projects-for-container."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== Selekcja projektów do konteneryzacji ==="
|
||||
#find . -name "*.csproj" -type f | while read csproj; do
|
||||
while read csproj; do
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
PROJECT_TYPE=$(dotnet msbuild "$csproj" -getProperty:ProjectType -noLogo 2>/dev/null || echo "Unknown")
|
||||
|
||||
if [ "$PROJECT_TYPE" = "Linux" ] || [ "$PROJECT_TYPE" = "Api" ] || [ "$PROJECT_TYPE" = "ServerProcess" ] || [ "$PROJECT_TYPE" = "Blazor" ] || [ "$PROJECT_TYPE" = "LinuxLocal" ]; then
|
||||
PROJECT_DIR=$(dirname "$csproj")
|
||||
echo "→ Dodaję $PROJECT_NAME (typ: $PROJECT_TYPE)"
|
||||
echo "$PROJECT_DIR|$PROJECT_NAME" >> projects-to-build.txt
|
||||
else
|
||||
echo "→ Pomijam $PROJECT_NAME (typ: $PROJECT_TYPE)"
|
||||
fi
|
||||
#done
|
||||
done < changed-projects.txt
|
||||
|
||||
echo ""
|
||||
if [ -s projects-to-build.txt ]; then
|
||||
echo "Znalezione projekty:"
|
||||
cat projects-to-build.txt
|
||||
echo "Łącznie: $(wc -l < projects-to-build.txt)"
|
||||
else
|
||||
echo "BRAK PROJEKTÓW DO BUDOWY!"
|
||||
fi
|
||||
depends_on: [publish-datamodels-to-baget]
|
||||
|
||||
containerize-apps:
|
||||
image: docker:27-cli
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_registry_user
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_registry_token
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam contenerize-apps."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REGISTRY_ORG="fa"
|
||||
|
||||
echo "=== DEBUG: Zawartość projects-to-build.txt ==="
|
||||
cat projects-to-build.txt || echo "Plik nie istnieje!"
|
||||
echo "=== DEBUG: REGISTRY_ORG = '$REGISTRY_ORG' ==="
|
||||
|
||||
echo "$GITEA_TOKEN" | docker login git.modwad.pl -u "$GITEA_USER" --password-stdin
|
||||
|
||||
if [ ! -s projects-to-build.txt ]; then
|
||||
echo "Brak projektow"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rm -f NuGet.Config
|
||||
|
||||
cat > NuGet.Config <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://127.0.0.1:8080/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
|
||||
cat projects-to-build.txt | while IFS='|' read -r PDIR PNAME; do
|
||||
PDIR=$(echo "$PDIR" | xargs)
|
||||
PNAME=$(echo "$PNAME" | xargs)
|
||||
|
||||
echo "=== DEBUG: PDIR='$PDIR' PNAME='$PNAME' ==="
|
||||
|
||||
test -z "$PNAME" && continue
|
||||
test ! -d "$PDIR" && continue
|
||||
|
||||
echo -n "git.modwad.pl/fa/" > /tmp/tag.txt
|
||||
echo "$PNAME" | tr 'A-Z' 'a-z' | tr -d '\n' >> /tmp/tag.txt
|
||||
|
||||
echo "=== DEBUG: Generated tag: $(cat /tmp/tag.txt) ==="
|
||||
|
||||
echo "$PNAME" > /tmp/pname.txt
|
||||
echo "$PDIR" | sed 's|^\./||' > /tmp/pdir.txt
|
||||
|
||||
cat > "Dockerfile.temp" <<'ENDOFDOCKERFILE'
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore "%%PROJECTDIR%%/%%PROJECTNAME%%.csproj"
|
||||
RUN dotnet publish "%%PROJECTDIR%%/%%PROJECTNAME%%.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
ENTRYPOINT ["dotnet", "%%PROJECTNAME%%.dll"]
|
||||
ENDOFDOCKERFILE
|
||||
|
||||
REAL_PNAME=$(cat /tmp/pname.txt)
|
||||
REAL_PDIR=$(cat /tmp/pdir.txt)
|
||||
sed "s|%%PROJECTNAME%%|$REAL_PNAME|g; s|%%PROJECTDIR%%|$REAL_PDIR|g" "Dockerfile.temp" > "Dockerfile.final"
|
||||
|
||||
echo "=== Budowanie obrazu ==="
|
||||
DOCKER_BUILDKIT=0 docker build \
|
||||
--network=host \
|
||||
-t "$(cat /tmp/tag.txt):${CI_COMMIT_SHA}" \
|
||||
-t "$(cat /tmp/tag.txt):latest" \
|
||||
-f "Dockerfile.final" \
|
||||
.
|
||||
|
||||
echo "=== Pushing SHA tag ==="
|
||||
docker push "$(cat /tmp/tag.txt):${CI_COMMIT_SHA}"
|
||||
|
||||
echo "=== Pushing latest tag ==="
|
||||
docker push "$(cat /tmp/tag.txt):latest"
|
||||
|
||||
echo "=== SUCCESS: $(cat /tmp/tag.txt) ==="
|
||||
|
||||
rm -f "Dockerfile.temp" "Dockerfile.final"
|
||||
done
|
||||
|
||||
echo "=== WSZYSTKIE PROJEKTY ZAKONCZONE ==="
|
||||
depends_on: [select-projects-for-container]
|
||||
|
||||
deploy-linux-local:
|
||||
image: alpine:latest
|
||||
commands:
|
||||
- |
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam deploy-linux-local."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== Sprawdzanie aplikacji LinuxLocal ==="
|
||||
if [ ! -d "./linux-apps" ]; then
|
||||
echo "Folder ./linux-apps nie istnieje"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== DEBUG: Zawartość ./linux-apps/ ==="
|
||||
ls -la ./linux-apps/
|
||||
|
||||
echo "=== Wdrażanie aplikacji LinuxLocal do /opt/fa-apps/ ==="
|
||||
mkdir -p /opt/fa-apps
|
||||
|
||||
cd ./linux-apps
|
||||
|
||||
for app in *; do
|
||||
echo "→ DEBUG: Sprawdzam $app"
|
||||
|
||||
if [ ! -d "$app" ]; then
|
||||
echo " → $app nie jest folderem, pomijam"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " → Kopiuję $app"
|
||||
echo " → Zawartość:"
|
||||
ls -la "$app/"
|
||||
|
||||
rm -rf "/opt/fa-apps/$app"
|
||||
cp -r "$app" "/opt/fa-apps/$app"
|
||||
|
||||
find "/opt/fa-apps/$app" -type f -executable -exec chmod +x {} \;
|
||||
|
||||
echo " → Skopiowano!"
|
||||
done
|
||||
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
echo ""
|
||||
echo "=== Wdrożone aplikacje ==="
|
||||
ls -laR /opt/fa-apps/
|
||||
depends_on: [publish-linux-local, containerize-apps]
|
||||
|
||||
cleanup-docker:
|
||||
image: docker:27-cli
|
||||
commands:
|
||||
- |
|
||||
echo "=== Status przed czyszczeniem ==="
|
||||
docker system df
|
||||
|
||||
echo ""
|
||||
echo "=== Usuwanie dangling images ==="
|
||||
docker image prune -f
|
||||
|
||||
echo ""
|
||||
echo "=== Usuwanie starych wersji aplikacji ==="
|
||||
docker images --format "{{.Repository}}:{{.Tag}}" | grep "git.modwad.pl/" | grep -v ":latest" | grep -v ":${CI_COMMIT_SHA}" | while read img; do
|
||||
echo "Usuwam: $img"
|
||||
docker rmi "$img" || true
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Usuwanie kontenerów ==="
|
||||
docker container prune -f
|
||||
|
||||
echo ""
|
||||
echo "=== Usuwanie build cache ==="
|
||||
docker builder prune -af
|
||||
|
||||
echo ""
|
||||
echo "=== Status po czyszczeniu ==="
|
||||
docker system df
|
||||
depends_on: [deploy-linux-local]
|
||||
when:
|
||||
status: [success, failure]
|
||||
89
.woodpecker/push_to_repo.yml
Normal file
89
.woodpecker/push_to_repo.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
skip_clone: true
|
||||
when:
|
||||
event: push
|
||||
branch:
|
||||
- feature/*
|
||||
|
||||
steps:
|
||||
clone-manual:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
remote: http://gitea:3000/${CI_REPO}.git
|
||||
branch: ${CI_COMMIT_BRANCH}
|
||||
depth: 1
|
||||
detect-changed-projects:
|
||||
image: alpine
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
echo "CI_PIPELINE_FILES=${CI_PIPELINE_FILES:-<empty>}"
|
||||
|
||||
apk add --no-cache findutils
|
||||
|
||||
> changed-projects.txt
|
||||
|
||||
echo "${CI_PIPELINE_FILES:-}" \
|
||||
| sed 's/^\[//; s/\]$//' \
|
||||
| tr ',' '\n' \
|
||||
| sed 's/^ *//; s/ *$//' \
|
||||
| sed '/^$/d' \
|
||||
| while read path; do
|
||||
echo "→ changed file: $path"
|
||||
|
||||
dir=$(dirname "$path")
|
||||
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
|
||||
csproj=$(find "$dir" -maxdepth 1 -name "*.csproj" -type f | head -n 1 || true)
|
||||
if [ -n "$csproj" ]; then
|
||||
echo "$csproj" >> changed-projects.txt
|
||||
break
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
done
|
||||
done
|
||||
|
||||
# unikalne projekty
|
||||
sort -u -o changed-projects.txt changed-projects.txt
|
||||
|
||||
echo "Zmienione projekty:"
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pipeline zakonczony."
|
||||
# exit 0 = sukces, wiec kolejne kroki, ktore od niego zaleza, nie poleca
|
||||
exit 0
|
||||
fi
|
||||
cat changed-projects.txt
|
||||
depends_on: [clone-manual]
|
||||
pipeline-agent:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
environment:
|
||||
OPENAI_API_KEY:
|
||||
from_secret: openai_api_key
|
||||
VAULT_URL:
|
||||
from_secret: vault_url
|
||||
VAULT_TOKEN:
|
||||
from_secret: vault_token
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
echo "=== Konfiguracja NuGet sources ==="
|
||||
cat <<EOF > NuGet.Config
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
|
||||
if [ ! -s changed-projects.txt ]; then
|
||||
echo "Brak zmienionych projektow – pomijam AI-gate."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dotnet run --project PipelineAgent/PipelineAgent.csproj "check-changes" "$(pwd)"
|
||||
depends_on: [detect-changed-projects]
|
||||
47
BroseCumulativeReport.Tests/AppTests.cs
Normal file
47
BroseCumulativeReport.Tests/AppTests.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using FaKrosnoEfDataModel.Services;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace BroseCumulativeReport.Tests
|
||||
{
|
||||
public class AppTests
|
||||
{
|
||||
private readonly Mock<IScheduleOrderService> _mockScheduleOrderService = new();
|
||||
private readonly Mock<IScheduleOrderDetailsService> _mockScheduleOrderDetailsService = new();
|
||||
private readonly Mock<ICustomerOrderService> _mockCustomerOrderService = new();
|
||||
private readonly Mock<IMaterialTransactionService> _mockMaterialTransactionService = new();
|
||||
private readonly Mock<IConfiguration> _mockConfiguration = new();
|
||||
|
||||
private App CreateApp()
|
||||
{
|
||||
return new App(
|
||||
_mockScheduleOrderService.Object,
|
||||
_mockScheduleOrderDetailsService.Object,
|
||||
_mockCustomerOrderService.Object,
|
||||
_mockMaterialTransactionService.Object,
|
||||
_mockConfiguration.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClosestSendingDate_ShouldReturnNextThursday_WhenCalledWithVariousDates()
|
||||
{
|
||||
// Arrange
|
||||
DateTime friday = new DateTime(2023, 1, 6); // Friday
|
||||
DateTime thursday = new DateTime(2023, 1, 5); // Thursday
|
||||
DateTime wednesday = new DateTime(2023,1,4); // Wednesday
|
||||
|
||||
// Act
|
||||
DateTime resultFriday = App.GetClosestSendingDate(friday);
|
||||
DateTime resultThursday = App.GetClosestSendingDate(thursday);
|
||||
DateTime resultWednesday = App.GetClosestSendingDate(wednesday);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new DateTime(2023, 1, 12), resultFriday); // Next Thursday
|
||||
Assert.Equal(new DateTime(2023, 1, 12), resultThursday); // Next Thursday
|
||||
Assert.Equal(new DateTime(2023, 1, 5), resultWednesday); // This Thursday
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
68
BroseCumulativeReport.Tests/ExcelGeneratorTests.cs
Normal file
68
BroseCumulativeReport.Tests/ExcelGeneratorTests.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using BroseCumulativeReport;
|
||||
using BroseCumulativeReport.Model;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace BroseCumulativeReport.Tests
|
||||
{
|
||||
public class ExcelGeneratorTests
|
||||
{
|
||||
private readonly Mock<IConfiguration> _mockConfiguration = new();
|
||||
|
||||
public ExcelGeneratorTests()
|
||||
{
|
||||
// Setup mock IConfiguration behavior if needed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateExcel_ShouldCreateFile_WhenProvidedValidInput()
|
||||
{
|
||||
// Arrange
|
||||
var excelGenerator = new ExcelGenerator(_mockConfiguration.Object);
|
||||
string tempFilePath = Path.GetTempFileName();
|
||||
List<MainRow> mainRows = new List<MainRow>();
|
||||
|
||||
// Minimal MainRow data to avoid empty file scenario
|
||||
mainRows.Add(new MainRow
|
||||
{
|
||||
CustomerName = "Customer",
|
||||
CustomerOrderNumber = "Order1",
|
||||
OrderNumber = "OrderNum",
|
||||
OrderDate = DateTime.Today,
|
||||
SalesChannelProductCode = "SCP",
|
||||
ShipperProductCode = "SPC",
|
||||
LastWzNumber = "WZ123",
|
||||
WzQuantity = 1,
|
||||
SendDatesWithQuantity = new List<(DateTime, int)> { (DateTime.Today, 1) },
|
||||
ChildRows = new List<ChildRow>()
|
||||
});
|
||||
|
||||
// Act
|
||||
excelGenerator.GenerateExcel(tempFilePath, mainRows);
|
||||
|
||||
// Assert
|
||||
Assert.True(File.Exists(tempFilePath));
|
||||
|
||||
// Cleanup
|
||||
File.Delete(tempFilePath);
|
||||
}
|
||||
|
||||
//[Fact]
|
||||
// This test requires refactoring ExcelGenerator to allow injection of SMTP client
|
||||
// to mock and verify sending logic without side effects.
|
||||
//public void SendEmail_ShouldSendEmailWithoutExceptions_WhenCalled()
|
||||
//{
|
||||
// // Arrange
|
||||
// var excelGenerator = new ExcelGenerator(_mockConfiguration.Object);
|
||||
// string dummyFilePath = "dummyfile.xlsx";
|
||||
|
||||
// // Act & Assert
|
||||
// var exception = Record.Exception(() => excelGenerator.SendEmail(dummyFilePath));
|
||||
// Assert.Null(exception);
|
||||
//}
|
||||
}
|
||||
}
|
||||
24
BroseCumulativeReport.Tests/IAppTests.cs
Normal file
24
BroseCumulativeReport.Tests/IAppTests.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Threading.Tasks;
|
||||
using BroseCumulativeReport;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace BroseCumulativeReport.Tests
|
||||
{
|
||||
public class IAppTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunAsync_ShouldBeAwaitable()
|
||||
{
|
||||
// Arrange
|
||||
var mockApp = new Mock<IApp>();
|
||||
mockApp.Setup(app => app.RunAsync()).Returns(Task.CompletedTask).Verifiable();
|
||||
|
||||
// Act
|
||||
await mockApp.Object.RunAsync();
|
||||
|
||||
// Assert
|
||||
mockApp.Verify(v => v.RunAsync(), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
BroseCumulativeReport.Tests/Model/ChildRowTests.cs
Normal file
26
BroseCumulativeReport.Tests/Model/ChildRowTests.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using BroseCumulativeReport.Model;
|
||||
using Xunit;
|
||||
|
||||
namespace BroseCumulativeReport.Tests.Model
|
||||
{
|
||||
public class ChildRowTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ShouldInitializeProperties()
|
||||
{
|
||||
// Arrange
|
||||
string wzNumber = "WZ1";
|
||||
decimal quantity = 5.5m;
|
||||
DateTime date = DateTime.Today;
|
||||
|
||||
// Act
|
||||
var childRow = new ChildRow(wzNumber, quantity, date);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(wzNumber, childRow.WzNumber);
|
||||
Assert.Equal(quantity, childRow.Quantity);
|
||||
Assert.Equal(date, childRow.Date);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
BroseCumulativeReport.Tests/Model/ItemTests.cs
Normal file
30
BroseCumulativeReport.Tests/Model/ItemTests.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using BroseCumulativeReport.Model;
|
||||
using Xunit;
|
||||
|
||||
namespace BroseCumulativeReport.Tests.Model
|
||||
{
|
||||
public class ItemTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ShouldInitializeProperties()
|
||||
{
|
||||
// Arrange
|
||||
string customerName = "Customer";
|
||||
string salesCode = "SCP";
|
||||
string shipperCode = "SPC";
|
||||
int quantity = 10;
|
||||
string qtyType = "Type";
|
||||
|
||||
// Act
|
||||
var item = new Item(customerName, salesCode, shipperCode, quantity, qtyType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(customerName, item.CustomerName);
|
||||
Assert.Equal(salesCode, item.SalesChannelProductCode);
|
||||
Assert.Equal(shipperCode, item.ShipperProductCode);
|
||||
Assert.Equal(quantity, item.Quantity);
|
||||
Assert.Equal(qtyType, item.QuantityType);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
BroseCumulativeReport.Tests/Model/MainRowTests.cs
Normal file
38
BroseCumulativeReport.Tests/Model/MainRowTests.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BroseCumulativeReport.Model;
|
||||
using Xunit;
|
||||
|
||||
namespace BroseCumulativeReport.Tests.Model
|
||||
{
|
||||
public class MainRowTests
|
||||
{
|
||||
[Fact]
|
||||
public void Properties_ShouldSetAndGetCorrectly()
|
||||
{
|
||||
var mainRow = new MainRow
|
||||
{
|
||||
CustomerName = "Customer",
|
||||
CustomerOrderNumber = "OrderNumber",
|
||||
OrderNumber = "Order123",
|
||||
OrderDate = DateTime.Now,
|
||||
SalesChannelProductCode = "SCP",
|
||||
ShipperProductCode = "SPC",
|
||||
LastWzNumber = "WZ123",
|
||||
WzQuantity = 10,
|
||||
SendDatesWithQuantity = new List<(DateTime, int)> { (DateTime.Today, 1) },
|
||||
ChildRows = new List<ChildRow>()
|
||||
};
|
||||
|
||||
Assert.Equal("Customer", mainRow.CustomerName);
|
||||
Assert.Equal("OrderNumber", mainRow.CustomerOrderNumber);
|
||||
Assert.Equal("Order123", mainRow.OrderNumber);
|
||||
Assert.Equal("SCP", mainRow.SalesChannelProductCode);
|
||||
Assert.Equal("SPC", mainRow.ShipperProductCode);
|
||||
Assert.Equal("WZ123", mainRow.LastWzNumber);
|
||||
Assert.Equal(10, mainRow.WzQuantity);
|
||||
Assert.Single(mainRow.SendDatesWithQuantity);
|
||||
Assert.Empty(mainRow.ChildRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
BroseCumulativeReport.Tests/Model/WzTests.cs
Normal file
33
BroseCumulativeReport.Tests/Model/WzTests.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using BroseCumulativeReport.Model;
|
||||
using FaKrosnoEfDataModel.Dtos;
|
||||
using Xunit;
|
||||
|
||||
namespace BroseCumulativeReport.Tests.Model
|
||||
{
|
||||
public class WzTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ShouldSetPropertiesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
string poNumber = "PO123";
|
||||
string wzNumberInput = "WZ_456";
|
||||
string expectedWzNumber = "WZ456";
|
||||
string custOrderNumber = "CO789";
|
||||
var scheduleOrder = new ScheduleOrderDto(); // Assuming default constructor available
|
||||
int quantity = 42;
|
||||
|
||||
// Act
|
||||
var wz = new Wz(poNumber, wzNumberInput, custOrderNumber, scheduleOrder, quantity);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(poNumber, wz.PoNumber);
|
||||
// Verify that underscores are removed as expected
|
||||
Assert.Equal(expectedWzNumber, wz.WzNumber);
|
||||
Assert.Equal(custOrderNumber, wz.CustomerOrderNumber);
|
||||
Assert.Equal(scheduleOrder, wz.ScheduleOrder);
|
||||
Assert.Equal(quantity, wz.Quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
BroseCumulativeReport.Tests/ServiceTests.cs
Normal file
25
BroseCumulativeReport.Tests/ServiceTests.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using BroseCumulativeReport;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace BroseCumulativeReport.Tests
|
||||
{
|
||||
public class ServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateHostBuilder_ShouldReturnHostBuilder_WithExpectedConfiguration()
|
||||
{
|
||||
// Arrange
|
||||
var service = new Service();
|
||||
string[] args = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
IHostBuilder hostBuilder = service.CreateHostBuilder(args);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(hostBuilder);
|
||||
// TODO: Additional asserts to verify configuration and service setup if possible
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,11 @@ public class ValidatorService(IRestClient restClient) : IValidatorService
|
||||
request.AddQueryParameter("customerNumber", customer.CustNum);
|
||||
request.AddQueryParameter("customerSequence", customer.CustSeq.ToString());
|
||||
request.AddQueryParameter("poNumber", scheduleOrder.PONum);
|
||||
|
||||
|
||||
var response = await restClient.ExecuteAsync<CustomerOrderDto>(request);
|
||||
if (response is { IsSuccessful: true, Data: not null }) return true;
|
||||
|
||||
request = new RestRequest("api/customerOrders/list-by-customer");
|
||||
request.AddQueryParameter("customerNumber", customer.CustNum);
|
||||
request.AddQueryParameter("customerSequence", customer.CustSeq.ToString());
|
||||
|
||||
var listResponse = await restClient.ExecuteAsync<IList<CustomerOrderDto>>(request);
|
||||
return listResponse is { IsSuccessful: true, Data.Count: > 0 };
|
||||
return response is { IsSuccessful: true, Data: not null };
|
||||
}
|
||||
|
||||
|
||||
public async Task<ItemCustDto?> GetItemCust(string itemNumber, string customerNumber)
|
||||
{
|
||||
var request = new RestRequest("api/ItemCust");
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
"OrderNumber": "FP1015",
|
||||
"Recipient": 16,
|
||||
"NewOrderNumber": "940088"
|
||||
},
|
||||
{
|
||||
"OrderNumber": "0092190784",
|
||||
"Recipient": 27,
|
||||
"NewOrderNumber": "92190784"
|
||||
}
|
||||
],
|
||||
"CustomerMappers": [
|
||||
|
||||
22
DevelopmentAgent/DevelopmentAgent.csproj
Normal file
22
DevelopmentAgent/DevelopmentAgent.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>DeploymentAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
|
||||
<PackageReference Include="Services" Version="2.0.0-alpha.0.149" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Prompts\TestProjectGenerator.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
36
DevelopmentAgent/Program.cs
Normal file
36
DevelopmentAgent/Program.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using DeploymentAgent.Shell;
|
||||
using DeploymentAgent.TestsGenerator;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Services.Gitea;
|
||||
using Services.OpenAI;
|
||||
using Services.Vault;
|
||||
|
||||
IHost builder = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||
|
||||
services.AddSingleton<IVaultService>(vaultService);
|
||||
|
||||
services.AddScoped<IGiteaService, GiteaService>();
|
||||
services.AddScoped<IOpenAiService, OpenAiService>();
|
||||
services.AddScoped<IShellRunner, ShellRunner>();
|
||||
services.AddScoped<ITestsGeneratorAgent, TestsGeneratorAgent>();
|
||||
}).Build();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("Usage: DeploymentAgent <command> [options]");
|
||||
Console.WriteLine("Commands: tests-generator");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return args[0] switch
|
||||
{
|
||||
"tests-generator" => await builder.Services.GetRequiredService<ITestsGeneratorAgent>()
|
||||
.GenerateTestsAsync(args.ElementAtOrDefault(1) ?? string.Empty),
|
||||
_ => 1
|
||||
};
|
||||
147
DevelopmentAgent/Prompts/TestProjectGenerator.txt
Normal file
147
DevelopmentAgent/Prompts/TestProjectGenerator.txt
Normal file
@@ -0,0 +1,147 @@
|
||||
Jesteś C# Developerem specjalizującym się w tworzeniu projektów testowych dla aplikacji .NET.
|
||||
|
||||
Wejście:
|
||||
Dostajesz pojedynczy obiekt JSON w formacie:
|
||||
{
|
||||
"projectName": string, // ścieżka katalogu projektu, dla którego mają zostać wygenerowane testy (np. "BroseCumulativeReport" lub "src/BroseCumulativeReport")
|
||||
"solutionPath": string, // pełna ścieżka do pliku .sln, do którego ma zostać dodany projekt testowy (np. "/Users/user/RiderProjects/FA/FA.sln")
|
||||
"files": [
|
||||
{
|
||||
"path": string, // ścieżka pliku C# względem katalogu solution (np. "BroseCumulativeReport/Services/FooService.cs")
|
||||
"content": string // pełna zawartość pliku C#
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Twoje zadanie:
|
||||
|
||||
1. Przeanalizuj wszystkie pliki z pola "files":
|
||||
- wykryj publiczne klasy, metody i właściwości,
|
||||
- wygeneruj testy jednostkowe pokrywające każdą publiczną metodę i właściwość,
|
||||
- uwzględnij różne scenariusze, w tym przypadki brzegowe i obsługę wyjątków,
|
||||
- użyj xUnit jako frameworka testowego,
|
||||
- tam, gdzie klasy mają zależności (np. przez konstruktor lub wstrzykiwanie interfejsów), sugeruj ich mockowanie za pomocą Moq (np. new Mock<IMyService>()).
|
||||
|
||||
2. Na podstawie wartości "projectName":
|
||||
- przyjmij, że "projectName" wskazuje katalog projektu głównego względem katalogu solution (np. "BroseCumulativeReport" albo "src/BroseCumulativeReport"),
|
||||
- pobierz ostatni segment tej ścieżki jako nazwę projektu głównego, np.:
|
||||
- projectName: "BroseCumulativeReport" → nazwa projektu głównego: "BroseCumulativeReport",
|
||||
- projectName: "src/BroseCumulativeReport" → nazwa projektu głównego: "BroseCumulativeReport",
|
||||
- nazwij projekt testowy "{NazwaProjektuGłównego}.Tests", np. "BroseCumulativeReport.Tests".
|
||||
|
||||
3. Przygotuj plik projektu testowego "{TestProjectName}.csproj" o zawartości:
|
||||
|
||||
- <Project Sdk="Microsoft.NET.Sdk">
|
||||
- <PropertyGroup> z:
|
||||
- <TargetFramework> dopasowany do projektu głównego; jeśli nie znasz dokładnej wartości, użyj "net8.0",
|
||||
- <IsPackable>false</IsPackable>.
|
||||
- <ItemGroup> z referencjami do paczek:
|
||||
- Microsoft.NET.Test.Sdk
|
||||
- xunit
|
||||
- xunit.runner.visualstudio
|
||||
- coverlet.collector
|
||||
- Moq
|
||||
- <ItemGroup> z:
|
||||
- <ProjectReference Include="ŚCIEŻKA_DO_PROJEKTU_GŁÓWNEGO.csproj" />
|
||||
|
||||
WAŻNE (ProjectReference i ścieżki):
|
||||
|
||||
- Przyjmij standardowy układ katalogów:
|
||||
- katalog z solutionPath (ROOT), np. "/Users/user/RiderProjects/FA",
|
||||
- w nim katalog projektu głównego, np. "BroseCumulativeReport" (ostatni segment projectName),
|
||||
- w nim plik .csproj projektu głównego: "{NazwaProjektuGłównego}.csproj" (np. "BroseCumulativeReport/BroseCumulativeReport.csproj"),
|
||||
- projekt testowy w katalogu "{TestProjectName}" obok projektu głównego: "{TestProjectName}" pod ROOT (np. "BroseCumulativeReport.Tests").
|
||||
- Katalog projektu testowego będzie więc:
|
||||
- ROOT/{TestProjectName}, np. "/Users/user/RiderProjects/FA/BroseCumulativeReport.Tests".
|
||||
- Ścieżka w `<ProjectReference Include="...">` musi być RELATYWNA względem katalogu projektu testowego (katalogu, w którym leży plik .csproj testów), czyli:
|
||||
- z "BroseCumulativeReport.Tests" przechodzisz poziom wyżej do ROOT, a następnie do katalogu projektu głównego.
|
||||
- poprawny wzór:
|
||||
"../{NazwaProjektuGłównego}/{NazwaProjektuGłównego}.csproj"
|
||||
- PRZYKŁAD:
|
||||
- solutionPath: "/Users/piotrkus/RiderProjects/FA/FA.sln"
|
||||
- projectName: "BroseCumulativeReport"
|
||||
- nazwa projektu głównego: "BroseCumulativeReport"
|
||||
- katalog projektu testowego: "/Users/piotrkus/RiderProjects/FA/BroseCumulativeReport.Tests"
|
||||
- katalog projektu głównego: "/Users/piotrkus/RiderProjects/FA/BroseCumulativeReport"
|
||||
- wtedy w pliku BroseCumulativeReport.Tests.csproj wstaw:
|
||||
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||
- Wygeneruj `<ProjectReference Include="...">` jako LITERALNY tekst w XML, bez używania zmiennych powłoki bash (nie używaj $MAIN_PROJECT_REL_PATH). Wstaw pełną ścieżkę względną względem katalogu projektu testowego.
|
||||
|
||||
4. Zaprojektuj strukturę katalogów testowych:
|
||||
|
||||
- katalog główny projektu testowego: "{TestProjectName}" (np. "BroseCumulativeReport.Tests"),
|
||||
- wewnątrz niego ewentualne podkatalogi odzwierciedlające strukturę namespace/folderów projektu głównego (np. "Model", "Configuration"),
|
||||
- dla każdej publicznej klasy z projektu głównego wygeneruj odpowiadający plik testowy:
|
||||
- np. BroseCumulativeReport.App → AppTests.cs,
|
||||
- BroseCumulativeReport.ExcelGenerator → ExcelGeneratorTests.cs,
|
||||
- BroseCumulativeReport.Model.MainRow → Model/MainRowTests.cs, itd.
|
||||
- w każdej klasie testowej:
|
||||
- wygeneruj metody testowe dla wszystkich publicznych metod i właściwości,
|
||||
- używaj nazewnictwa w stylu: MethodName_ShouldDoSomething_WhenCondition,
|
||||
- korzystaj z [Fact] dla prostych przypadków oraz [Theory] z [InlineData(...)] tam, gdzie można sensownie zaproponować różne dane wejściowe,
|
||||
- stosuj strukturę Arrange / Act / Assert z TODO-komentarzami tam, gdzie potrzebne są decyzje domenowe,
|
||||
- dla zależności korzystaj z Moq (np. var dependency = new Mock<IMyService>(); i dependency.Object wstrzykiwane do konstruktora).
|
||||
|
||||
5. Na podstawie powyższych informacji wygeneruj skrypt powłoki /bin/sh, który:
|
||||
|
||||
- jest samowystarczalny: NIE czyta JSON-a ani żadnych danych wejściowych,
|
||||
- nie przyjmuje żadnych argumentów,
|
||||
- zakłada, że:
|
||||
- plik solution znajduje się dokładnie pod ścieżką podaną w "solutionPath" w wejściowym JSON-ie,
|
||||
- projekt testowy ma być utworzony w tym samym katalogu co plik solution (czyli obok pliku .sln, nie w katalogu projektu głównego).
|
||||
|
||||
Skrypt powinien wykonywać następujące kroki:
|
||||
|
||||
1) Ustawić zmienną SOLUTION_PATH na wartość z pola "solutionPath" (osadzoną na stałe w skrypcie, np. SOLUTION_PATH="/Users/piotrkus/RiderProjects/FA/FA.sln").
|
||||
2) Wyznaczyć SOLUTION_DIR jako katalog nadrzędny pliku solution:
|
||||
SOLUTION_DIR=$(dirname "$SOLUTION_PATH")
|
||||
3) Utworzyć katalog projektu testowego:
|
||||
TEST_PROJECT_NAME="{TestProjectName}" (np. "BroseCumulativeReport.Tests")
|
||||
TEST_PROJECT_DIR="$SOLUTION_DIR/$TEST_PROJECT_NAME"
|
||||
oraz podkatalogi, które zaprojektowałeś (np. "$TEST_PROJECT_DIR/Model", "$TEST_PROJECT_DIR/Configuration").
|
||||
4) W katalogu TEST_PROJECT_DIR zapisać plik "{TestProjectName}.csproj" z wygenerowaną zawartością, używając heredoca.
|
||||
- W pliku .csproj wstaw literalną ścieżkę ProjectReference zgodnie z regułą:
|
||||
"../{NazwaProjektuGłównego}/{NazwaProjektuGłównego}.csproj"
|
||||
- Możesz użyć heredoca z 'EOF', ponieważ nie potrzebujesz interpolacji zmiennych bash wewnątrz XML.
|
||||
5) W katalogu TEST_PROJECT_DIR utworzyć strukturę podkatalogów testowych i zapisać wszystkie wygenerowane pliki testowe (po jednym heredocu na plik).
|
||||
6) Wykonać polecenie:
|
||||
dotnet sln "$SOLUTION_PATH" add "$TEST_PROJECT_DIR/{TestProjectName}.csproj"
|
||||
aby dodać projekt testowy do rozwiązania.
|
||||
7) Wypisać na stderr lub stdout krótki komunikat o sukcesie (np. "Test project {TestProjectName} created and added to solution").
|
||||
|
||||
Skrypt może zawierać podstawowe sprawdzenia:
|
||||
- czy polecenie "dotnet" jest dostępne,
|
||||
- czy plik solution istnieje pod ścieżką SOLUTION_PATH,
|
||||
- w razie krytycznych błędów powinien zakończyć się niezerowym kodem wyjścia.
|
||||
|
||||
WAŻNE:
|
||||
|
||||
- Cała logika analizy kodu C# i generowania treści plików testowych musi być wykonana przez Ciebie PRZED wygenerowaniem skryptu (na podstawie "files").
|
||||
- Skrypt ma tylko:
|
||||
- utworzyć katalog projektu testowego,
|
||||
- zapisać gotowy plik .csproj i pliki testowe (dokładnie takie, jakie wygenerujesz),
|
||||
- dodać projekt do solution.
|
||||
- Skrypt NIE może parsować JSON-a, NIE może czytać plików źródłowych projektu głównego ani próbować ich analizować – wszystko, czego potrzebuje, musi być już wpisane w jego treść.
|
||||
- W szczególności w pliku .csproj:
|
||||
- NIE używaj zmiennych powłoki takich jak $MAIN_PROJECT_REL_PATH wewnątrz `<ProjectReference Include="...">`.
|
||||
- Wstaw pełną ścieżkę względną względem katalogu projektu testowego, np.:
|
||||
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||
|
||||
FORMAT ODPOWIEDZI:
|
||||
|
||||
Zwróć dokładnie jeden obiekt JSON o strukturze:
|
||||
|
||||
{
|
||||
"script": "KOMPLETNY SKRYPT /bin/sh",
|
||||
"comments": "Dodatkowe uwagi lub instrukcje dotyczące użycia skryptu"
|
||||
}
|
||||
|
||||
Zasady formatowania:
|
||||
- W polu "script" umieść kompletny skrypt /bin/sh gotowy do zapisania do pliku i uruchomienia bez modyfikacji.
|
||||
- Skrypt w "script" nie może zawierać żadnych znaczników Markdown ani bloków ``` – ma to być czysty tekst skryptu.
|
||||
- W polu "comments" podaj zwięzłe informacje:
|
||||
- jak uruchomić skrypt,
|
||||
- jakie są wymagania (np. zainstalowany .NET SDK),
|
||||
- ewentualne założenia dotyczące ścieżek.
|
||||
- Nie dodawaj żadnego tekstu poza tym jednym obiektem JSON.
|
||||
- Upewnij się, że wygenerowany JSON jest poprawny składniowo (prawidłowe cudzysłowy, przecinki, brak zbędnych znaków).
|
||||
6
DevelopmentAgent/Shell/IShellRunner.cs
Normal file
6
DevelopmentAgent/Shell/IShellRunner.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DeploymentAgent.Shell;
|
||||
|
||||
public interface IShellRunner
|
||||
{
|
||||
(int ExitCode, string StdOut, string StdErr) RunScript(string script);
|
||||
}
|
||||
35
DevelopmentAgent/Shell/ShellRunner.cs
Normal file
35
DevelopmentAgent/Shell/ShellRunner.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DeploymentAgent.Shell;
|
||||
|
||||
public class ShellRunner : IShellRunner
|
||||
{
|
||||
public (int ExitCode, string StdOut, string StdErr) RunScript(string script)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/sh",
|
||||
Arguments = "-s",
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = new Process();
|
||||
process.StartInfo = psi;
|
||||
|
||||
process.Start();
|
||||
|
||||
process.StandardInput.WriteLine(script);
|
||||
process.StandardInput.Close();
|
||||
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
return (process.ExitCode, output, error);
|
||||
}
|
||||
}
|
||||
6
DevelopmentAgent/TestsGenerator/ITestsGeneratorAgent.cs
Normal file
6
DevelopmentAgent/TestsGenerator/ITestsGeneratorAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DeploymentAgent.TestsGenerator;
|
||||
|
||||
public interface ITestsGeneratorAgent
|
||||
{
|
||||
Task<int> GenerateTestsAsync(string codePath);
|
||||
}
|
||||
7
DevelopmentAgent/TestsGenerator/Models/FileModel.cs
Normal file
7
DevelopmentAgent/TestsGenerator/Models/FileModel.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DeploymentAgent.TestsGenerator.Models;
|
||||
|
||||
public class FileModel
|
||||
{
|
||||
public string Path { get; set; } = null!;
|
||||
public string Content { get; set; } = null!;
|
||||
}
|
||||
8
DevelopmentAgent/TestsGenerator/Models/RequestModel.cs
Normal file
8
DevelopmentAgent/TestsGenerator/Models/RequestModel.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace DeploymentAgent.TestsGenerator.Models;
|
||||
|
||||
public class RequestModel
|
||||
{
|
||||
public string ProjectName { get; set; } = null!;
|
||||
public string SolutionPath { get; set; } = null!;
|
||||
public List<FileModel> Files { get; set; } = new();
|
||||
}
|
||||
80
DevelopmentAgent/TestsGenerator/TestsGeneratorAgent.cs
Normal file
80
DevelopmentAgent/TestsGenerator/TestsGeneratorAgent.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using DeploymentAgent.Shell;
|
||||
using DeploymentAgent.TestsGenerator.Models;
|
||||
using Services.OpenAI;
|
||||
|
||||
namespace DeploymentAgent.TestsGenerator;
|
||||
|
||||
public class TestsGeneratorAgent(IOpenAiService openAiService, IShellRunner shellRunner) : ITestsGeneratorAgent
|
||||
{
|
||||
public async Task<int> GenerateTestsAsync(string codePath)
|
||||
{
|
||||
var prompt = await File.ReadAllTextAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts",
|
||||
"TestProjectGenerator.txt"));
|
||||
|
||||
var requestModel = GetFormattedJsonRequest(codePath);
|
||||
var decisionDoc = await openAiService.GetResponseFromChat(requestModel, prompt);
|
||||
|
||||
if (decisionDoc == null)
|
||||
{
|
||||
Console.WriteLine("AI‑Gate: no response from LLM");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var generatedTests = decisionDoc.RootElement.GetProperty("script").GetString();
|
||||
var comments = decisionDoc.RootElement.GetProperty("comments").GetString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(generatedTests)) return 1;
|
||||
|
||||
var (exitCode, stdout, stderr) = shellRunner.RunScript(generatedTests);
|
||||
Console.WriteLine("AI‑Gate: PowerShell script executed.");
|
||||
Console.WriteLine("Comments from LLM:");
|
||||
Console.WriteLine(comments);
|
||||
Console.WriteLine("Shell output:");
|
||||
Console.WriteLine(exitCode);
|
||||
Console.WriteLine(stdout);
|
||||
Console.WriteLine(stderr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private RequestModel GetFormattedJsonRequest(string rootPath)
|
||||
{
|
||||
var files = new List<FileModel>();
|
||||
|
||||
foreach (var filePath in Directory.EnumerateFiles(rootPath, "*.cs", SearchOption.AllDirectories))
|
||||
{
|
||||
var content = File.ReadAllText(filePath);
|
||||
files.Add(new FileModel
|
||||
{
|
||||
Path = filePath,
|
||||
Content = content
|
||||
});
|
||||
}
|
||||
|
||||
return new RequestModel
|
||||
{
|
||||
ProjectName = rootPath,
|
||||
SolutionPath = GetSolutionName(rootPath) ?? string.Empty,
|
||||
Files = files
|
||||
};
|
||||
}
|
||||
|
||||
private string? GetSolutionName(string csprojPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(csprojPath))
|
||||
throw new ArgumentException("projectDirectory is null or empty", nameof(csprojPath));
|
||||
|
||||
var dir = new DirectoryInfo(csprojPath);
|
||||
|
||||
while (dir != null)
|
||||
{
|
||||
var sln = dir.GetFiles("*.sln", SearchOption.TopDirectoryOnly)
|
||||
.FirstOrDefault();
|
||||
if (sln != null)
|
||||
return sln.FullName;
|
||||
|
||||
dir = dir.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
103
FA.sln
103
FA.sln
@@ -19,40 +19,142 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeartBeatClient.Tests", "He
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineAgent", "PipelineAgent\PipelineAgent.csproj", "{348BC96F-2001-4251-BC41-FAFC44B4559F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevelopmentAgent", "DevelopmentAgent\DevelopmentAgent.csproj", "{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BroseCumulativeReport.Tests", "BroseCumulativeReport.Tests\BroseCumulativeReport.Tests.csproj", "{6D4722BD-C891-4C87-9A32-289A2DB92F44}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeartBeatServer.Tests", "HeartBeatServer.Tests\HeartBeatServer.Tests.csproj", "{45544483-3E12-4885-A36A-38455818E296}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x86.Build.0 = Release|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x64.Build.0 = Release|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x86.Build.0 = Release|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x64.Build.0 = Release|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x86.Build.0 = Release|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x86.Build.0 = Release|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Release|x64.Build.0 = Release|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{45544483-3E12-4885-A36A-38455818E296}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -61,6 +163,7 @@ Global
|
||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||
{45544483-3E12-4885-A36A-38455818E296} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {5510D1A6-B22F-4C3D-9EA8-66F1676CF7D5}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
using HeartBeatClients.Core;
|
||||
using FaKrosnoEfDataModel;
|
||||
using HeartBeatClients.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Services.Vault;
|
||||
using SytelineSaAppEfDataModel;
|
||||
using FaKrosnoMappingProfile = FaKrosnoEfDataModel.MappingProfile;
|
||||
using SytelineSaAppMappingProfile = SytelineSaAppEfDataModel.MappingProfile;
|
||||
|
||||
namespace HeartBeatClients;
|
||||
|
||||
public class Builder
|
||||
{
|
||||
public IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
})
|
||||
.ConfigureServices((hostContext, services) =>
|
||||
{
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||
|
||||
var sytelineConnection = vaultService.GetSecretAsync("database/syteline_prod", "connection_string").GetAwaiter().GetResult();
|
||||
var faKrosnoConnection = vaultService.GetSecretAsync("database/fakrosno", "connection_string").GetAwaiter().GetResult();
|
||||
|
||||
services.AddDbContext<SytelineSaAppDbContext>(options => options.UseSqlServer(sytelineConnection));
|
||||
services.AddDbContext<FaKrosnoDbContext>(options => options.UseSqlServer(faKrosnoConnection));
|
||||
|
||||
services.AddAutoMapper(typeof(FaKrosnoMappingProfile), typeof(SytelineSaAppMappingProfile));
|
||||
services.AddSingleton<IVaultService>(vaultService);
|
||||
services.AddScoped<IEmailGeneratorService, EmailGeneratorService>();
|
||||
services.AddScoped<IHeartBeatDecisionService, HeartBeatDecisionService>();
|
||||
services.AddScoped<IApp, App>();
|
||||
});
|
||||
}
|
||||
@@ -6,44 +6,58 @@ using Services.Vault;
|
||||
|
||||
namespace HeartBeatClients.Core;
|
||||
|
||||
public class App(IConfiguration configuration, IEmailGeneratorService emailGeneratorService, IVaultService vaultService) : IApp
|
||||
public class App : IApp
|
||||
{
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly IEmailGeneratorService emailGeneratorService;
|
||||
private readonly IVaultService vaultService;
|
||||
|
||||
public App(IConfiguration configuration, IEmailGeneratorService emailGeneratorService, IVaultService vaultService)
|
||||
{
|
||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
this.emailGeneratorService = emailGeneratorService ?? throw new ArgumentNullException(nameof(emailGeneratorService));
|
||||
this.vaultService = vaultService ?? throw new ArgumentNullException(nameof(vaultService));
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
var contaboConnection = await vaultService.GetSecretAsync("database/contabo", "connection_string", "secret");
|
||||
if (string.IsNullOrWhiteSpace(contaboConnection))
|
||||
{
|
||||
// Consider logging error or throwing as connection string must be valid
|
||||
throw new InvalidOperationException("Database connection string cannot be null or empty.");
|
||||
}
|
||||
|
||||
var heartbeatSection = configuration.GetSection("HeartBeatClient");
|
||||
if (!int.TryParse(heartbeatSection["HeartBeatInterval"], out int heartBeatInterval))
|
||||
{
|
||||
// You might want to provide a default or throw
|
||||
heartBeatInterval = 0;
|
||||
heartBeatInterval = 5; // Default value
|
||||
}
|
||||
|
||||
if (heartBeatInterval <= 0)
|
||||
{
|
||||
// Consider logging a warning that heartbeat interval is invalid
|
||||
return;
|
||||
}
|
||||
|
||||
ResultModel? result = await GetLastUpdateDate(contaboConnection);
|
||||
|
||||
DateTime now = DateTime.Now;
|
||||
DateTime lastUpdateDate = result?.LastUpdateDate.ToUniversalTime() ?? DateTime.MinValue;
|
||||
|
||||
double minutesDifference = (now - lastUpdateDate).TotalMinutes;
|
||||
double minutesDifference = (DateTime.UtcNow - lastUpdateDate).TotalMinutes;
|
||||
|
||||
if (minutesDifference > heartBeatInterval && result?.EmailSent == false)
|
||||
if (result != null && minutesDifference > heartBeatInterval && result.EmailSent == false)
|
||||
{
|
||||
await GenerateAlert(lastUpdateDate);
|
||||
await UpdateHeartBeat(contaboConnection, result!.Id);
|
||||
await UpdateHeartBeat(contaboConnection, result.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GenerateAlert(DateTime lastUpdateDate)
|
||||
{
|
||||
EmailModel emailModel = new EmailModel
|
||||
var emailModel = new EmailModel
|
||||
{
|
||||
Subject = $"HeartBeat: Serwer nie odpowiada",
|
||||
Body = $"Serwer nie odpowiada od {lastUpdateDate}. Proszę o sprawdzenie stanu serwera."
|
||||
Body = $"Serwer nie odpowiada od {lastUpdateDate:u}. Proszę o sprawdzenie stanu serwera."
|
||||
};
|
||||
|
||||
emailModel.To.Add("piotr.kus@fakrosno.pl");
|
||||
@@ -51,30 +65,32 @@ public class App(IConfiguration configuration, IEmailGeneratorService emailGener
|
||||
await emailGeneratorService.SendEmailAsync(emailModel);
|
||||
}
|
||||
|
||||
private static readonly string QueryGetLastUpdate = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\"." +
|
||||
"\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
||||
|
||||
private async Task<ResultModel?> GetLastUpdateDate(string connectionString)
|
||||
{
|
||||
const string query = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\"." +
|
||||
"\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
||||
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var command = new NpgsqlCommand(query, connection);
|
||||
await using var command = new NpgsqlCommand(QueryGetLastUpdate, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
if (await reader.ReadAsync())
|
||||
{
|
||||
return new ResultModel(reader.GetInt32(0), reader.GetDateTime(1), reader.GetBoolean(2));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly string QueryUpdateHeartBeat = "UPDATE \"OrdersManagement\"." +
|
||||
"\"HeartBeat\" SET \"EmailSent\" = true WHERE \"ID\" = @Id;";
|
||||
|
||||
private async Task UpdateHeartBeat(string connectionString, int id)
|
||||
{
|
||||
const string query = "UPDATE \"OrdersManagement\".\"HeartBeat\" SET \"EmailSent\" = true WHERE \"ID\" = @Id;";
|
||||
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var command = new NpgsqlCommand(query, connection);
|
||||
await using var command = new NpgsqlCommand(QueryUpdateHeartBeat, connection);
|
||||
command.Parameters.AddWithValue("@Id", id);
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
@@ -1,23 +1,49 @@
|
||||
using System.Reflection;
|
||||
using HeartBeatClients;
|
||||
using FaKrosnoEfDataModel;
|
||||
using HeartBeatClients.Core;
|
||||
using HeartBeatClients.Models;
|
||||
using HeartBeatClients.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Services.Vault;
|
||||
using SytelineSaAppEfDataModel;
|
||||
using MappingProfile = FaKrosnoEfDataModel.MappingProfile;
|
||||
|
||||
IHost? host = null;
|
||||
|
||||
try
|
||||
{
|
||||
Builder builder = new Builder();
|
||||
host = builder.CreateHostBuilder(args).Build();
|
||||
var builder = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
})
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||
|
||||
var sytelineConnection = vaultService.GetSecretAsync("database/syteline_prod", "connection_string").GetAwaiter().GetResult();
|
||||
var faKrosnoConnection = vaultService.GetSecretAsync("database/fakrosno", "connection_string").GetAwaiter().GetResult();
|
||||
|
||||
services.AddDbContext<SytelineSaAppDbContext>(options => options.UseSqlServer(sytelineConnection));
|
||||
services.AddDbContext<FaKrosnoDbContext>(options => options.UseSqlServer(faKrosnoConnection));
|
||||
|
||||
services.AddAutoMapper(typeof(MappingProfile), typeof(SytelineSaAppEfDataModel.MappingProfile));
|
||||
services.AddSingleton<IVaultService>(vaultService);
|
||||
services.AddScoped<IEmailGeneratorService, EmailGeneratorService>();
|
||||
services.AddScoped<IHeartBeatDecisionService, HeartBeatDecisionService>();
|
||||
services.AddScoped<IApp, App>();
|
||||
});
|
||||
|
||||
host = builder.Build();
|
||||
|
||||
var vaultService = host.Services.GetRequiredService<IVaultService>();
|
||||
var syncfusionLicense = vaultService.GetSecretAsync("licenses/syncfusion", "license_key", "secret")
|
||||
.GetAwaiter().GetResult();
|
||||
var syncfusionLicense = await vaultService.GetSecretAsync("licenses/syncfusion", "license_key", "secret");
|
||||
|
||||
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(syncfusionLicense ??
|
||||
Environment.GetEnvironmentVariable(
|
||||
|
||||
16
HeartBeatServer.Tests/HeartBeatServer.Tests.csproj
Normal file
16
HeartBeatServer.Tests/HeartBeatServer.Tests.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
||||
<PackageReference Include="xunit" Version="2.5.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../HeartBeatServer/HeartBeatServer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
43
HeartBeatServer.Tests/Models/EmailModelTests.cs
Normal file
43
HeartBeatServer.Tests/Models/EmailModelTests.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using HeartBeatServer.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace HeartBeatServer.Tests.Models
|
||||
{
|
||||
public class EmailModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Properties_ShouldInitializeEmptyCollections_AndSetValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
var emailModel = new EmailModel();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(emailModel.To);
|
||||
Assert.NotNull(emailModel.Cc);
|
||||
Assert.NotNull(emailModel.Bcc);
|
||||
Assert.NotNull(emailModel.Attachments);
|
||||
Assert.Empty(emailModel.To);
|
||||
Assert.Empty(emailModel.Cc);
|
||||
Assert.Empty(emailModel.Bcc);
|
||||
Assert.Empty(emailModel.Attachments);
|
||||
Assert.Equal(string.Empty, emailModel.Subject);
|
||||
Assert.Equal(string.Empty, emailModel.Body);
|
||||
|
||||
// Act - set properties
|
||||
emailModel.Subject = "subject";
|
||||
emailModel.Body = "body";
|
||||
emailModel.To.Add("to@example.com");
|
||||
emailModel.Cc.Add("cc@example.com");
|
||||
emailModel.Bcc.Add("bcc@example.com");
|
||||
emailModel.Attachments.Add("file.txt");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("subject", emailModel.Subject);
|
||||
Assert.Equal("body", emailModel.Body);
|
||||
Assert.Contains("to@example.com", emailModel.To);
|
||||
Assert.Contains("cc@example.com", emailModel.Cc);
|
||||
Assert.Contains("bcc@example.com", emailModel.Bcc);
|
||||
Assert.Contains("file.txt", emailModel.Attachments);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
HeartBeatServer.Tests/Models/ResultModelTests.cs
Normal file
39
HeartBeatServer.Tests/Models/ResultModelTests.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using HeartBeatServer.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace HeartBeatServer.Tests.Models
|
||||
{
|
||||
public class ResultModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Properties_ShouldGetSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var id = 1;
|
||||
var lastUpdateDate = DateTime.Now;
|
||||
var emailSent = true;
|
||||
|
||||
// Act
|
||||
var result = new ResultModel(id, lastUpdateDate, emailSent);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(id, result.Id);
|
||||
Assert.Equal(lastUpdateDate, result.LastUpdateDate);
|
||||
Assert.Equal(emailSent, result.EmailSent);
|
||||
|
||||
// Act - test set accessors
|
||||
var newId = 2;
|
||||
var newDate = DateTime.Now.AddDays(1);
|
||||
var newEmailSent = false;
|
||||
result.Id = newId;
|
||||
result.LastUpdateDate = newDate;
|
||||
result.EmailSent = newEmailSent;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(newId, result.Id);
|
||||
Assert.Equal(newDate, result.LastUpdateDate);
|
||||
Assert.Equal(newEmailSent, result.EmailSent);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
HeartBeatServer.Tests/Services/EmailGeneratorServiceTests.cs
Normal file
25
HeartBeatServer.Tests/Services/EmailGeneratorServiceTests.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using HeartBeatServer.Models;
|
||||
using HeartBeatServer.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace HeartBeatServer.Tests.Services
|
||||
{
|
||||
public class EmailGeneratorServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void SendEmail_ShouldThrow_WhenMissingSmtpSettings()
|
||||
{
|
||||
// Arrange
|
||||
var configuration = new ConfigurationBuilder().Build();
|
||||
var emailService = new EmailGeneratorService(configuration);
|
||||
|
||||
var email = new EmailModel();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() => emailService.SendEmailAsync(email).ConfigureAwait(false).GetAwaiter().GetResult());
|
||||
// Expected to fail int.Parse on Port or missing required data
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using HeartBeatServer.Models;
|
||||
using HeartBeatServer.Services;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace HeartBeatServer.Tests.Services
|
||||
{
|
||||
public class IEmailGeneratorServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void SendEmail_InterfaceMethod_CallMock()
|
||||
{
|
||||
// Arrange
|
||||
var mockService = new Mock<IEmailGeneratorService>();
|
||||
var email = new EmailModel();
|
||||
|
||||
// Act
|
||||
mockService.Object.SendEmailAsync(email).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
mockService.Verify(s => s.SendEmailAsync(email), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using HeartBeatServers.Core;
|
||||
using FaKrosnoEfDataModel;
|
||||
using HeartBeatServers.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Services.Vault;
|
||||
using SytelineSaAppEfDataModel;
|
||||
using FaKrosnoMappingProfile = FaKrosnoEfDataModel.MappingProfile;
|
||||
using SytelineSaAppMappingProfile = SytelineSaAppEfDataModel.MappingProfile;
|
||||
|
||||
namespace HeartBeatServers;
|
||||
|
||||
public class Builder
|
||||
{
|
||||
public IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.AddJsonFile("appSettings.json", optional: false, reloadOnChange: true);
|
||||
})
|
||||
.ConfigureServices((hostContext, services) =>
|
||||
{
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||
|
||||
var sytelineConnection = vaultService.GetSecretAsync("database/syteline_prod", "connection_string").GetAwaiter().GetResult();
|
||||
var faKrosnoConnection = vaultService.GetSecretAsync("database/fakrosno", "connection_string").GetAwaiter().GetResult();
|
||||
|
||||
services.AddDbContext<SytelineSaAppDbContext>(options => options.UseSqlServer(sytelineConnection));
|
||||
services.AddDbContext<FaKrosnoDbContext>(options => options.UseSqlServer(faKrosnoConnection));
|
||||
|
||||
services.AddAutoMapper(typeof(FaKrosnoMappingProfile), typeof(SytelineSaAppMappingProfile));
|
||||
services.AddSingleton<IVaultService>(vaultService);
|
||||
services.AddScoped<IEmailGeneratorService, EmailGeneratorService>();
|
||||
services.AddScoped<IApp, App>();
|
||||
});
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
using HeartBeatServers.Models;
|
||||
using HeartBeatServer.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
using Services.Vault;
|
||||
|
||||
namespace HeartBeatServers.Core;
|
||||
namespace HeartBeatServer.Core;
|
||||
|
||||
public class App(IConfiguration configuration, IVaultService vaultService) : IApp
|
||||
{
|
||||
private const string QueryGetLastUpdate = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\".\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
||||
private const string QueryInsertHeartBeat = "INSERT INTO \"OrdersManagement\".\"HeartBeat\" (\"LastUpdateDate\", \"EmailSent\") VALUES (@LastUpdateDate, @EmailSent);";
|
||||
private const string QueryUpdateHeartBeat = "UPDATE \"OrdersManagement\".\"HeartBeat\" SET \"LastUpdateDate\" = @LastUpdateDate WHERE \"ID\" = @Id;";
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
var contaboConnection = await vaultService.GetSecretAsync("database/contabo", "connection_string", "secret") ?? string.Empty;
|
||||
|
||||
ResultModel? result = await GetLastUpdateDate(contaboConnection);
|
||||
if (string.IsNullOrWhiteSpace(contaboConnection))
|
||||
throw new InvalidOperationException("Database connection string cannot be null or empty.");
|
||||
|
||||
var result = await GetLastUpdateDate(contaboConnection);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
@@ -25,29 +32,26 @@ public class App(IConfiguration configuration, IVaultService vaultService) : IAp
|
||||
|
||||
private async Task<ResultModel?> GetLastUpdateDate(string connectionString)
|
||||
{
|
||||
var results = new List<ResultModel?>();
|
||||
string query = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\".\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
||||
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var command = new NpgsqlCommand(query, connection);
|
||||
await using var command = new NpgsqlCommand(QueryGetLastUpdate, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
while (reader.Read())
|
||||
|
||||
if (await reader.ReadAsync())
|
||||
{
|
||||
results.Add(new ResultModel(reader.GetInt32(0), reader.GetDateTime(1), reader.GetBoolean(2)));
|
||||
return new ResultModel(reader.GetInt32(0), reader.GetDateTime(1), reader.GetBoolean(2));
|
||||
}
|
||||
|
||||
return results.FirstOrDefault();
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task InsertHeartBeat(string connectionString)
|
||||
{
|
||||
string query = "INSERT INTO \"OrdersManagement\".\"HeartBeat\" (\"LastUpdateDate\", \"EmailSent\") VALUES (@LastUpdateDate, @EmailSent)";
|
||||
var lastUpdateDate = DateTime.Now;
|
||||
DateTime lastUpdateDate = DateTime.UtcNow;
|
||||
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var command = new NpgsqlCommand(query, connection);
|
||||
await using var command = new NpgsqlCommand(QueryInsertHeartBeat, connection);
|
||||
command.Parameters.AddWithValue("@LastUpdateDate", lastUpdateDate);
|
||||
command.Parameters.AddWithValue("@EmailSent", false);
|
||||
await command.ExecuteNonQueryAsync();
|
||||
@@ -55,12 +59,11 @@ public class App(IConfiguration configuration, IVaultService vaultService) : IAp
|
||||
|
||||
private async Task UpdateHeartBeat(string connectionString, int id)
|
||||
{
|
||||
string query = "UPDATE \"OrdersManagement\".\"HeartBeat\" SET \"LastUpdateDate\" = @LastUpdateDate, \"EmailSent\" = false WHERE \"ID\" = @Id";
|
||||
var lastUpdateDate = DateTime.Now;
|
||||
DateTime lastUpdateDate = DateTime.UtcNow;
|
||||
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var command = new NpgsqlCommand(query, connection);
|
||||
await using var command = new NpgsqlCommand(QueryUpdateHeartBeat, connection);
|
||||
command.Parameters.AddWithValue("@Id", id);
|
||||
command.Parameters.AddWithValue("@LastUpdateDate", lastUpdateDate);
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace HeartBeatServers.Core;
|
||||
namespace HeartBeatServer.Core;
|
||||
|
||||
public interface IApp
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<ProjectType>ServerProcess</ProjectType>
|
||||
<RootNamespace>HeartBeatServers</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace HeartBeatServers.Models;
|
||||
namespace HeartBeatServer.Models;
|
||||
|
||||
public class EmailModel
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace HeartBeatServers.Models;
|
||||
namespace HeartBeatServer.Models;
|
||||
|
||||
public class ResultModel(int id, DateTime lastUpdateDate, bool emailSent)
|
||||
{
|
||||
public int Id { get; set; } = id;
|
||||
public DateTime LastUpdateDate { get; set; } = lastUpdateDate;
|
||||
public bool EmailSent { get; set; } = false;
|
||||
public bool EmailSent { get; set; } = emailSent;
|
||||
}
|
||||
@@ -1,43 +1,92 @@
|
||||
using System.Reflection;
|
||||
using HeartBeatServers;
|
||||
using HeartBeatServers.Core;
|
||||
using HeartBeatServers.Models;
|
||||
using HeartBeatServers.Services;
|
||||
using System.Reflection;
|
||||
using FaKrosnoEfDataModel;
|
||||
using HeartBeatServer.Core;
|
||||
using HeartBeatServer.Models;
|
||||
using HeartBeatServer.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(
|
||||
"NRAiBiAaIQQuGjN/V09+XU9HdVRDX3xKf0x/TGpQb19xflBPallYVBYiSV9jS3tTckVgWHldc3ZUR2lfVE90Vg==");
|
||||
using Services.Vault;
|
||||
using SytelineSaAppEfDataModel;
|
||||
|
||||
IHost? host = null;
|
||||
|
||||
try
|
||||
// Changed Main method to async Task Main to support async operations correctly
|
||||
async Task MainAsync(string[] args)
|
||||
{
|
||||
Builder builder = new Builder();
|
||||
host = builder.CreateHostBuilder(args).Build();
|
||||
|
||||
var app = host.Services.GetRequiredService<IApp>();
|
||||
await app.RunAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
|
||||
if (host == null)
|
||||
try
|
||||
{
|
||||
var builder = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
});
|
||||
|
||||
// --- NOWE: async część przed ConfigureServices ---
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||
|
||||
var sytelineConnection = await vaultService
|
||||
.GetSecretAsync("database/syteline_prod", "connection_string");
|
||||
var faKrosnoConnection = await vaultService
|
||||
.GetSecretAsync("database/fakrosno", "connection_string");
|
||||
// -------------------------------------------------
|
||||
|
||||
builder.ConfigureServices((_, services) =>
|
||||
{
|
||||
// używamy już gotowego vaultService i connection stringów
|
||||
services.AddSingleton<IVaultService>(vaultService);
|
||||
|
||||
services.AddDbContext<SytelineSaAppDbContext>(
|
||||
options => options.UseSqlServer(sytelineConnection));
|
||||
services.AddDbContext<FaKrosnoDbContext>(
|
||||
options => options.UseSqlServer(faKrosnoConnection));
|
||||
|
||||
services.AddSingleton<IEmailGeneratorService, EmailGeneratorService>();
|
||||
services.AddSingleton<IApp, App>();
|
||||
});
|
||||
|
||||
var builderResult = builder.Build();
|
||||
host = builderResult;
|
||||
|
||||
var vault = host.Services.GetRequiredService<IVaultService>();
|
||||
var syncfusionLicense = await vault.GetSecretAsync("licenses/syncfusion", "license_key", "secret");
|
||||
|
||||
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(syncfusionLicense);
|
||||
|
||||
var app = host.Services.GetRequiredService<IApp>();
|
||||
await app.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
|
||||
if (host == null)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var emailGeneratorService = host.Services.GetRequiredService<IEmailGeneratorService>();
|
||||
var emailModel = new EmailModel
|
||||
{
|
||||
Subject = $"{Assembly.GetEntryAssembly()!.GetName().Name}: Errors encountered during program execution",
|
||||
Body = $"Errors: {e}"
|
||||
};
|
||||
emailModel.To.Add("piotr.kus@fakrosno.pl");
|
||||
await emailGeneratorService.SendEmailAsync(emailModel);
|
||||
}
|
||||
catch (Exception emailException)
|
||||
{
|
||||
Console.WriteLine($"Error sending error notification email: {emailException}");
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
IEmailGeneratorService emailGeneratorService = host.Services.GetRequiredService<IEmailGeneratorService>();
|
||||
|
||||
EmailModel emailModel = new EmailModel
|
||||
{
|
||||
Subject = $"{Assembly.GetEntryAssembly()!.GetName().Name}: Znaleziono błędy w trakcie wykonywania programu",
|
||||
Body = $"Błędy: {e}"
|
||||
};
|
||||
emailModel.To.Add("piotr.kus@fakrosno.pl");
|
||||
|
||||
emailGeneratorService.SendEmail(emailModel);
|
||||
|
||||
throw;
|
||||
}
|
||||
// Entry point changed to async Main
|
||||
await MainAsync(args);
|
||||
|
||||
11
HeartBeatServer/Properties/launchSettings.json
Normal file
11
HeartBeatServer/Properties/launchSettings.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"HeartBeatServer": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"VAULT_URL": "https://vault.modwad.pl",
|
||||
"VAULT_TOKEN": "hvs.CAESID7fTT61Cs0C37k4pEr98_GXonsWfIgavM0a0nEAb9-qGh4KHGh2cy5DUnNnNjB5Yklla3c3cVNrYmdab01GSzM"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,97 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
using HeartBeatServers.Models;
|
||||
using System.Threading.Tasks;
|
||||
using HeartBeatServer.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace HeartBeatServers.Services;
|
||||
namespace HeartBeatServer.Services;
|
||||
|
||||
public class EmailGeneratorService(IConfiguration configuration) : IEmailGeneratorService
|
||||
public class EmailGeneratorService : IEmailGeneratorService, IDisposable
|
||||
{
|
||||
public void SendEmail(EmailModel email)
|
||||
private readonly IConfiguration configuration;
|
||||
private bool disposedValue;
|
||||
|
||||
public EmailGeneratorService(IConfiguration configuration)
|
||||
{
|
||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(EmailModel email)
|
||||
{
|
||||
if (email == null) throw new ArgumentNullException(nameof(email));
|
||||
|
||||
IConfigurationSection smtpSettings = configuration.GetSection("SmtpSettings");
|
||||
string smtpHost = smtpSettings["Host"] ?? string.Empty;
|
||||
int smtpPort = int.Parse(smtpSettings["Port"] ?? "0");
|
||||
|
||||
if (smtpSettings == null)
|
||||
{
|
||||
throw new ArgumentException("SMTP settings are not configured.");
|
||||
}
|
||||
|
||||
string smtpHost = smtpSettings["Host"] ?? throw new ArgumentException("SMTP host is not configured.");
|
||||
|
||||
if (!int.TryParse(smtpSettings["Port"], out int smtpPort) || smtpPort <= 0)
|
||||
{
|
||||
throw new ArgumentException("SMTP port is invalid or not configured.");
|
||||
}
|
||||
|
||||
string smtpUsername = smtpSettings["Username"] ?? string.Empty;
|
||||
string smtpPassword = smtpSettings["Password"] ?? string.Empty;
|
||||
string fromEmail = smtpSettings["FromEmail"] ?? string.Empty;
|
||||
string fromEmail = smtpSettings["FromEmail"] ?? throw new ArgumentException("FromEmail is not configured.");
|
||||
|
||||
using var mailMessage = new MailMessage();
|
||||
mailMessage.From = new MailAddress(fromEmail);
|
||||
mailMessage.Subject = email.Subject;
|
||||
mailMessage.Body = email.Body;
|
||||
mailMessage.IsBodyHtml = false;
|
||||
using var mailMessage = new MailMessage()
|
||||
{
|
||||
From = new MailAddress(fromEmail),
|
||||
Subject = email.Subject ?? string.Empty,
|
||||
Body = email.Body ?? string.Empty,
|
||||
IsBodyHtml = false
|
||||
};
|
||||
|
||||
foreach (string attachment in email.Attachments)
|
||||
{
|
||||
mailMessage.Attachments.Add(new Attachment(attachment));
|
||||
if (!string.IsNullOrWhiteSpace(attachment))
|
||||
mailMessage.Attachments.Add(new Attachment(attachment));
|
||||
}
|
||||
|
||||
foreach (string toAddress in email.To)
|
||||
{
|
||||
mailMessage.To.Add(toAddress);
|
||||
if (!string.IsNullOrWhiteSpace(toAddress))
|
||||
mailMessage.To.Add(toAddress);
|
||||
}
|
||||
|
||||
foreach (string ccAddress in email.Cc)
|
||||
{
|
||||
mailMessage.CC.Add(ccAddress);
|
||||
if (!string.IsNullOrWhiteSpace(ccAddress))
|
||||
mailMessage.CC.Add(ccAddress);
|
||||
}
|
||||
|
||||
foreach (string bccAddress in email.Bcc)
|
||||
{
|
||||
mailMessage.Bcc.Add(bccAddress);
|
||||
if (!string.IsNullOrWhiteSpace(bccAddress))
|
||||
mailMessage.Bcc.Add(bccAddress);
|
||||
}
|
||||
|
||||
using var smtpClient = new SmtpClient(smtpHost, smtpPort);
|
||||
smtpClient.EnableSsl = true;
|
||||
smtpClient.Credentials = new NetworkCredential(smtpUsername, smtpPassword);
|
||||
smtpClient.Send(mailMessage);
|
||||
using var smtpClient = new SmtpClient(smtpHost, smtpPort)
|
||||
{
|
||||
EnableSsl = true,
|
||||
Credentials = new NetworkCredential(smtpUsername, smtpPassword)
|
||||
};
|
||||
|
||||
await smtpClient.SendMailAsync(mailMessage);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
// No unmanaged resources besides smtpClient which is scoped locally now
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using HeartBeatServers.Models;
|
||||
using HeartBeatServer.Models;
|
||||
|
||||
namespace HeartBeatServers.Services;
|
||||
namespace HeartBeatServer.Services;
|
||||
|
||||
public interface IEmailGeneratorService
|
||||
{
|
||||
void SendEmail(EmailModel email);
|
||||
Task SendEmailAsync(EmailModel email);
|
||||
}
|
||||
6
HeartBeatServer/Services/ISmtpClientWrapperService.cs
Normal file
6
HeartBeatServer/Services/ISmtpClientWrapperService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace HeartBeatServer.Services;
|
||||
|
||||
public interface ISmtpClientWrapperService
|
||||
{
|
||||
|
||||
}
|
||||
6
HeartBeatServer/Services/SmtpClientWrapperService.cs
Normal file
6
HeartBeatServer/Services/SmtpClientWrapperService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace HeartBeatServer.Services;
|
||||
|
||||
public class SmtpClientWrapperService : ISmtpClientWrapperService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,136 +1,189 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Gitea.Net.Api;
|
||||
using Gitea.Net.Client;
|
||||
using Gitea.Net.Model;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using PipelineAgent.ChangesChecker.Models;
|
||||
using Services.Gitea;
|
||||
using Services.OpenAI;
|
||||
using Services.Vault;
|
||||
|
||||
namespace PipelineAgent.ChangesChecker;
|
||||
|
||||
public class ChangesCheckerAgent
|
||||
public class ChangesCheckerAgent(IOpenAiService openAiService, IVaultService vaultService, IGiteaService giteaService)
|
||||
: IChangesCheckerAgent
|
||||
{
|
||||
public async Task<int> CheckChangesAsync()
|
||||
public async Task<int> CheckChangesAsync(string repositoryPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string> lastChangesFromGitea = await GetLastChangesFromGitea();
|
||||
|
||||
var userContent = JsonSerializer.Serialize(lastChangesFromGitea);
|
||||
var giteaApiToken = await vaultService.GetSecretAsync("api_keys/gitea", "gitea_api_token_write", "secret")
|
||||
.ConfigureAwait(false);
|
||||
var giteaConfiguration = GetGiteaConfiguration(repositoryPath, giteaApiToken);
|
||||
var giteaClient = giteaService.CreateGiteaClient(giteaConfiguration);
|
||||
var lastChangesFromGitea =
|
||||
await GetLastChangesFromGiteaAsync(giteaClient, giteaConfiguration).ConfigureAwait(false);
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
if (lastChangesFromGitea.Count == 0)
|
||||
{
|
||||
Console.WriteLine("AI‑Gate: OPENAI_API_KEY not set, blocking by default.");
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
var client = new OpenAIClient(apiKey);
|
||||
var chatRequest = lastChangesFromGitea.Select(x => new ChatRequest(x.Value.Item1, x.Key, x.Value.Item2));
|
||||
var promptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts", "ChangesChecker.txt");
|
||||
var prompt = await File.ReadAllTextAsync(promptPath).ConfigureAwait(false);
|
||||
|
||||
var systemPrompt = """
|
||||
Jesteś Senior .NET Developerem z kilkunastoletnim stażem.
|
||||
Na podstawie listy zawartości plików zmienionych w ostatnich commitach:
|
||||
var decisionDoc = await openAiService.GetResponseFromChat(chatRequest, prompt).ConfigureAwait(false);
|
||||
|
||||
- zdekoduj zawartość pliku zapisaną w base64
|
||||
- sprwadź kod pod kątem poprawności i potencjalnych błędów
|
||||
- zasugeruj poprawki lub ulepszenia, jeśli to konieczne, tak, żeby kod był jak najbardziej optymalny i profesjonalny
|
||||
- zwróć JEDEN obiekt JSON w formacie:
|
||||
{
|
||||
"file-content": "zdekodowana zawartość pliku",
|
||||
"improvements": "wprowadzone poprawki w kodzie lub 'brak' w jezyku polskim",
|
||||
"changed-file" "nowy plik z wprowadzonymi poprawkami lub 'brak'",
|
||||
"decision": "approve" | "changes-requested"
|
||||
}
|
||||
if (decisionDoc == null)
|
||||
{
|
||||
Console.WriteLine("AI–Gate: no response from LLM, continuing by default.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Decyzja:
|
||||
- "approve" gdy kod jest w porzadku i nie potrzebuje zadnych zmian.
|
||||
- "changes-requested" gdy kod wymaga poprawek lub ulepszeń.
|
||||
Nie dodawaj żadnego tekstu poza JSON.
|
||||
""";
|
||||
if (!decisionDoc.RootElement.TryGetProperty("decision", out var decisionElement) ||
|
||||
decisionElement.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
Console.WriteLine("AI–Gate: decision property missing or null in LLM response, continuing by default.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// var userContent = JsonSerializer.Serialize(failuresForModel);
|
||||
//
|
||||
var chat = client.GetChatClient("gpt-4.1-mini");
|
||||
|
||||
var response = await chat.CompleteChatAsync(new SystemChatMessage(systemPrompt),
|
||||
new UserChatMessage($"Pliki ostatnio zmienione:\n{userContent}"));
|
||||
|
||||
var json = response.Value.Content[0].Text;
|
||||
Console.WriteLine("AI‑Gate LLM raw response:");
|
||||
Console.WriteLine(json);
|
||||
|
||||
var decisionDoc = JsonDocument.Parse(json);
|
||||
var decision = decisionDoc.RootElement.GetProperty("decision").GetString();
|
||||
var improvements = decisionDoc.RootElement.GetProperty("improvements").GetString();
|
||||
var fileContent = decisionDoc.RootElement.GetProperty("file-content").GetString();
|
||||
var changedFile = decisionDoc.RootElement.GetProperty("changed-file").GetString();
|
||||
|
||||
Console.WriteLine($"AI‑Gate decision: {decision}");
|
||||
Console.WriteLine($"Improvements: {improvements}");
|
||||
Console.WriteLine($"FileContent: {fileContent}");
|
||||
Console.WriteLine($"ChangedFile: {changedFile}");
|
||||
//
|
||||
// return decision switch
|
||||
// {
|
||||
// "allow" => 0,
|
||||
// "allow-with-warning" => 0,
|
||||
// _ => 1
|
||||
// };
|
||||
if (!decisionDoc.RootElement.TryGetProperty("changes", out var changesElement) ||
|
||||
changesElement.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var content = GetFormattedJsonRequest(changesElement);
|
||||
var (status, response) = await giteaService.SendRequestAsync(giteaConfiguration, "contents", content)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (status)
|
||||
{
|
||||
Console.WriteLine("AI–Gate: created PR with suggested changes.");
|
||||
Console.WriteLine(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("AI–Gate: failed to create PR with suggested changes.");
|
||||
Console.WriteLine(response);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("AI–Gate: operation was canceled.");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"AI‑Gate: error while reading TRX: {ex.Message}");
|
||||
return 1;
|
||||
Console.WriteLine($"AI–Gate: while processing: {ex.Message}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetLastChangesFromGitea()
|
||||
private async Task<Dictionary<string, (string, string)>> GetLastChangesFromGiteaAsync(RepositoryApi giteaClient,
|
||||
GiteaConfiguration giteaConfiguration)
|
||||
{
|
||||
var lastChanges = new List<string>();
|
||||
var lastChanges = new Dictionary<string, (string, string)>(StringComparer.Ordinal);
|
||||
var lastUpdatedBranch =
|
||||
(await giteaService.GetAllBranchesAsync(giteaClient, giteaConfiguration).ConfigureAwait(false))
|
||||
.Where(x => x.Name != "master" && x.Name.StartsWith("feature/")).OrderByDescending(x => x.Commit.Timestamp)
|
||||
.FirstOrDefault();
|
||||
|
||||
RepositoryApi repositoryApi = await CreateGiteaClient();
|
||||
|
||||
var lastCommits = await repositoryApi.RepoGetAllCommitsAsync("FA", "FA", "master", limit: 2);
|
||||
var commitAffectedFilesList = lastCommits.SelectMany(x => x.Files).ToList();
|
||||
|
||||
foreach (CommitAffectedFiles commitAffectedFile in commitAffectedFilesList)
|
||||
if (lastUpdatedBranch == null ||
|
||||
lastUpdatedBranch.Commit.Message.Contains("LLM: Code review suggestions", StringComparison.Ordinal))
|
||||
{
|
||||
var repoGetContentsAsync =
|
||||
await repositoryApi.RepoGetContentsAsync("FA", "FA", commitAffectedFile.Filename);
|
||||
return lastChanges;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(repoGetContentsAsync.Content))
|
||||
var lastCommit = await giteaService
|
||||
.GetCommitByIdAsync(giteaClient, giteaConfiguration, lastUpdatedBranch.Commit.Id).ConfigureAwait(false);
|
||||
|
||||
if (lastCommit == null)
|
||||
{
|
||||
return lastChanges;
|
||||
}
|
||||
|
||||
foreach (CommitAffectedFiles commitAffectedFile in lastCommit.Files)
|
||||
{
|
||||
var fileContent = await giteaService
|
||||
.GetFileContentAsync(giteaClient, giteaConfiguration, commitAffectedFile.Filename,
|
||||
lastUpdatedBranch.Name).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(fileContent.Content) && !lastChanges.ContainsKey(commitAffectedFile.Filename))
|
||||
{
|
||||
lastChanges.Add(repoGetContentsAsync.Content);
|
||||
lastChanges.Add(commitAffectedFile.Filename, (lastUpdatedBranch.Name, Base64Decode(fileContent.Content)));
|
||||
}
|
||||
}
|
||||
|
||||
return lastChanges;
|
||||
}
|
||||
|
||||
private VaultService CreateVaultService()
|
||||
private GiteaConfiguration GetGiteaConfiguration(string repositoryPath, string giteaApiToken)
|
||||
{
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
return new VaultService(vaultUrl, vaultToken);
|
||||
ReadOnlySpan<char> repoSpan = repositoryPath.AsSpan();
|
||||
int lastSlash = repoSpan.LastIndexOf('/');
|
||||
int secondLastSlash = lastSlash > 0 ? repoSpan.Slice(0, lastSlash).LastIndexOf('/') : -1;
|
||||
|
||||
string owner = secondLastSlash >= 0
|
||||
? repoSpan.Slice(secondLastSlash + 1, lastSlash - secondLastSlash - 1).ToString()
|
||||
: string.Empty;
|
||||
string repository = lastSlash >= 0 ? repoSpan.Slice(lastSlash + 1).ToString() : repositoryPath;
|
||||
const string branch = "master";
|
||||
const string host = "https://git.modwad.pl";
|
||||
|
||||
return new GiteaConfiguration
|
||||
{
|
||||
Owner = owner,
|
||||
Repository = repository,
|
||||
Branch = branch,
|
||||
ApiToken = giteaApiToken,
|
||||
Host = host
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<RepositoryApi> CreateGiteaClient()
|
||||
private string Base64Decode(string base64EncodedData)
|
||||
{
|
||||
VaultService vaultService = CreateVaultService();
|
||||
string giteaApiToken = await vaultService.GetSecretAsync("api_keys/gitea", "gitea_api_token", "secret") ??
|
||||
string.Empty;
|
||||
byte[] bytes = Convert.FromBase64String(base64EncodedData);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
var config = new Configuration
|
||||
private StringContent GetFormattedJsonRequest(JsonElement json)
|
||||
{
|
||||
var branch = json.GetProperty("branch").GetString();
|
||||
var newBranch = json.GetProperty("new_branch").GetString();
|
||||
var message = json.GetProperty("message").GetString();
|
||||
|
||||
var filesJson = json.GetProperty("files");
|
||||
var files = new List<object>(filesJson.GetArrayLength());
|
||||
|
||||
foreach (var fileEl in filesJson.EnumerateArray())
|
||||
{
|
||||
BasePath = "https://git.modwad.pl/api/v1",
|
||||
ApiKey =
|
||||
var operation = fileEl.GetProperty("operation").GetString();
|
||||
var path = fileEl.GetProperty("path").GetString();
|
||||
var contentPlain = fileEl.GetProperty("content").GetString() ?? string.Empty;
|
||||
|
||||
var contentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(contentPlain));
|
||||
|
||||
files.Add(new
|
||||
{
|
||||
["token"] = giteaApiToken
|
||||
}
|
||||
operation,
|
||||
path,
|
||||
content = contentBase64
|
||||
});
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
branch,
|
||||
new_branch = newBranch,
|
||||
message,
|
||||
files
|
||||
};
|
||||
|
||||
return new RepositoryApi(config);
|
||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||
|
||||
return new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
}
|
||||
}
|
||||
6
PipelineAgent/ChangesChecker/IChangesCheckerAgent.cs
Normal file
6
PipelineAgent/ChangesChecker/IChangesCheckerAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace PipelineAgent.ChangesChecker;
|
||||
|
||||
public interface IChangesCheckerAgent
|
||||
{
|
||||
Task<int> CheckChangesAsync(string repositoryPath);
|
||||
}
|
||||
3
PipelineAgent/ChangesChecker/Models/ChatRequest.cs
Normal file
3
PipelineAgent/ChangesChecker/Models/ChatRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace PipelineAgent.ChangesChecker.Models;
|
||||
|
||||
public record ChatRequest(string branchName, string FileName, string FileContent);
|
||||
@@ -9,8 +9,18 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Gitea.Net.API" Version="25.8.18" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
|
||||
<PackageReference Include="OpenAI" Version="2.8.0" />
|
||||
<PackageReference Include="Services" Version="2.0.0-alpha.0" />
|
||||
<PackageReference Include="Services" Version="2.0.0-alpha.0.154" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Prompts\TestsChecker.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Prompts\ChangesChecker.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using System.Xml.Linq;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using PipelineAgent.ChangesChecker;
|
||||
using PipelineAgent.TestsChecker;
|
||||
using Services.Gitea;
|
||||
using Services.OpenAI;
|
||||
using Services.Vault;
|
||||
|
||||
IHost builder = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||
|
||||
services.AddSingleton<IVaultService>(vaultService);
|
||||
|
||||
services.AddScoped<IGiteaService, GiteaService>();
|
||||
services.AddScoped<IOpenAiService, OpenAiService>();
|
||||
services.AddScoped<ITestsCheckerAgent, TestsCheckerAgent>();
|
||||
services.AddScoped<IChangesCheckerAgent, ChangesCheckerAgent>();
|
||||
}).Build();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("Usage: CiAgent <command> [options]");
|
||||
Console.WriteLine("Commands: test-gate, container-gate, deploy-gate");
|
||||
Console.WriteLine("Commands: test-gate, check-changes, deploy-gate");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var command = args[0];
|
||||
var commandArgs = args.Skip(1).ToArray();
|
||||
|
||||
return command switch
|
||||
return args[0] switch
|
||||
{
|
||||
"test-gate" => await RunTestGateAsync(commandArgs[0]),
|
||||
"check-changes" => await RunChangesGateAsync(),
|
||||
"test-gate" => await builder.Services.GetRequiredService<ITestsCheckerAgent>()
|
||||
.CheckTestsAsync(args.ElementAtOrDefault(1) ?? string.Empty),
|
||||
"check-changes" => await builder.Services.GetRequiredService<IChangesCheckerAgent>()
|
||||
.CheckChangesAsync(args.ElementAtOrDefault(1) ?? string.Empty),
|
||||
_ => 1
|
||||
};
|
||||
|
||||
static async Task<int> RunTestGateAsync(string arg)
|
||||
{
|
||||
var testsCheckerAgent = new TestsCheckerAgent();
|
||||
return await testsCheckerAgent.CheckTestsAsync(arg);
|
||||
}
|
||||
|
||||
static async Task<int> RunChangesGateAsync()
|
||||
{
|
||||
var changesCheckerAgent = new ChangesCheckerAgent();
|
||||
return await changesCheckerAgent.CheckChangesAsync();
|
||||
}
|
||||
};
|
||||
38
PipelineAgent/Prompts/ChangesChecker.txt
Normal file
38
PipelineAgent/Prompts/ChangesChecker.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
Jesteś Senior .NET Developerem z kilkunastoletnim stażem.
|
||||
Programujesz w .NET 8 i C# 12, znasz najlepsze praktyki programistyczne oraz najnowoczesniejsza konwencje.
|
||||
Na podstawie listy zawartości plików zmienionych w ostatnich commitach:
|
||||
|
||||
- sprwadź kod pod kątem poprawności i potencjalnych błędów
|
||||
- zasugeruj poprawki lub ulepszenia, jeśli to konieczne, tak, żeby kod był jak najbardziej optymalny i profesjonalny
|
||||
- zwróć JEDEN obiekt JSON w formacie:
|
||||
{
|
||||
decision: "approve" | "changes-requested",
|
||||
changes:
|
||||
{
|
||||
branch = "branchName podany w danych wejsciowych branch lub main/master",
|
||||
new_branch = "code-review_<losowy_ciag_znakow>",
|
||||
message = "LLM: Code review suggestions plus to co zmieniono wypisane w liscie (*) po angielsku",
|
||||
files = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
operation = "update",
|
||||
path = nazwa pliku z requesta,
|
||||
content = poprawiony plik
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
- tworz changes tylko wtedy jesli sugerujesz jakies zmiany w kodzie
|
||||
- sprawdzaj, czy sugerowane zmiany nie wprowadzają nowych błędów lub problemów
|
||||
- upewnij się, że sugerowane zmiany są zgodne z najlepszymi praktykami programistycznymi
|
||||
- jesli klasa dziedziczy po interfejsie lub klasie bazowej, nie zmieniaj nazw metod lub ich sygnatur
|
||||
- dostarczaj jasne i zwięzłe uzasadnienie dla każdej sugerowanej zmiany
|
||||
- unikaj sugerowania zmian, które są zbędne lub nieistotne
|
||||
- pamiętaj, że Twoim celem jest poprawa jakości kodu
|
||||
- sprawdzaj wygenerowany json pod kątem poprawności składniowej i kompletności przed zwróceniem go użytkownikowi
|
||||
- jesli nie sugerujesz żadnych zmian w kodzie, pole "changes" powinno być puste (null)
|
||||
Decyzja:
|
||||
- "approve" gdy kod jest w porzadku i nie potrzebuje zadnych zmian (pole changes jest null).
|
||||
- "changes-requested" gdy kod wymaga poprawek lub ulepszeń.
|
||||
Nie dodawaj żadnego tekstu poza JSON.
|
||||
13
PipelineAgent/Prompts/TestsChecker.txt
Normal file
13
PipelineAgent/Prompts/TestsChecker.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Jesteś asystentem DevOps oceniającym wyniki testów .NET.
|
||||
Na podstawie listy błędów:
|
||||
- sklasyfikuj każdy błąd jako: CriticalBug, InfraOrConfig, FlakyTest
|
||||
- zwróć JEDEN obiekt JSON w formacie:
|
||||
{
|
||||
"decision": "block" | "allow" | "allow-with-warning",
|
||||
"reason": "krótkie wyjaśnienie po polsku"
|
||||
}
|
||||
Decyzja:
|
||||
- "block" gdy jest choć jeden CriticalBug.
|
||||
- "allow-with-warning" gdy są tylko InfraOrConfig lub FlakyTest, ale wygląda to na coś, co trzeba sprawdzić.
|
||||
- "allow" gdy wszystko wskazuje na drobne lub znane flaky testy.
|
||||
Nie dodawaj żadnego tekstu poza JSON.
|
||||
6
PipelineAgent/TestsChecker/ITestsCheckerAgent.cs
Normal file
6
PipelineAgent/TestsChecker/ITestsCheckerAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace PipelineAgent.TestsChecker;
|
||||
|
||||
public interface ITestsCheckerAgent
|
||||
{
|
||||
Task<int> CheckTestsAsync(string trxPath);
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using System.Xml.Linq;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using Services.OpenAI;
|
||||
|
||||
namespace PipelineAgent.TestsChecker;
|
||||
|
||||
public class TestsCheckerAgent
|
||||
public class TestsCheckerAgent(IOpenAiService openAiService) : ITestsCheckerAgent
|
||||
{
|
||||
public async Task<int> CheckTestsAsync(string trxPath)
|
||||
{
|
||||
@@ -18,19 +16,11 @@ public class TestsCheckerAgent
|
||||
try
|
||||
{
|
||||
XDocument doc = XDocument.Load(trxPath);
|
||||
|
||||
// Standardowy namespace MSTest/TRX
|
||||
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
|
||||
|
||||
var failed = doc
|
||||
.Descendants(ns + "UnitTestResult")
|
||||
.Where(x => (string?)x.Attribute("outcome") == "Failed")
|
||||
var failed = doc.Descendants(ns + "UnitTestResult").Where(x => (string?)x.Attribute("outcome") == "Failed")
|
||||
.ToList();
|
||||
|
||||
var total = doc
|
||||
.Descendants(ns + "UnitTestResult")
|
||||
.Count();
|
||||
|
||||
var total = doc.Descendants(ns + "UnitTestResult").Count();
|
||||
var failuresForModel = failed.Select(f => new
|
||||
{
|
||||
TestName = (string?)f.Attribute("testName") ?? "<no name>",
|
||||
@@ -41,48 +31,21 @@ public class TestsCheckerAgent
|
||||
.Value
|
||||
}).ToList();
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
var prompt = await File.ReadAllTextAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts",
|
||||
"TestsChecker.txt"));
|
||||
|
||||
var decisionDoc = await openAiService.GetResponseFromChat(failuresForModel, prompt);
|
||||
|
||||
if (decisionDoc == null)
|
||||
{
|
||||
Console.WriteLine("AI‑Gate: OPENAI_API_KEY not set, blocking by default.");
|
||||
Console.WriteLine("AI‑Gate: no response from LLM, blocking by default.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var client = new OpenAIClient(apiKey);
|
||||
|
||||
var systemPrompt = """
|
||||
Jesteś asystentem DevOps oceniającym wyniki testów .NET.
|
||||
Na podstawie listy błędów:
|
||||
|
||||
- sklasyfikuj każdy błąd jako: CriticalBug, InfraOrConfig, FlakyTest
|
||||
- zwróć JEDEN obiekt JSON w formacie:
|
||||
{
|
||||
"decision": "block" | "allow" | "allow-with-warning",
|
||||
"reason": "krótkie wyjaśnienie po polsku"
|
||||
}
|
||||
|
||||
Decyzja:
|
||||
- "block" gdy jest choć jeden CriticalBug.
|
||||
- "allow-with-warning" gdy są tylko InfraOrConfig lub FlakyTest, ale wygląda to na coś, co trzeba sprawdzić.
|
||||
- "allow" gdy wszystko wskazuje na drobne lub znane flaky testy.
|
||||
Nie dodawaj żadnego tekstu poza JSON.
|
||||
""";
|
||||
|
||||
var userContent = JsonSerializer.Serialize(failuresForModel);
|
||||
|
||||
var chat = client.GetChatClient("gpt-4.1-mini");
|
||||
|
||||
var response = await chat.CompleteChatAsync(new SystemChatMessage(systemPrompt),
|
||||
new UserChatMessage($"Błędy testów:\n{userContent}"));
|
||||
|
||||
var json = response.Value.Content[0].Text;
|
||||
Console.WriteLine("AI‑Gate LLM raw response:");
|
||||
Console.WriteLine(json);
|
||||
|
||||
var decisionDoc = JsonDocument.Parse(json);
|
||||
var decision = decisionDoc.RootElement.GetProperty("decision").GetString();
|
||||
var reason = decisionDoc.RootElement.GetProperty("reason").GetString();
|
||||
|
||||
Console.WriteLine($"Total: {total}, Failed: {failed.Count}");
|
||||
Console.WriteLine($"AI‑Gate decision: {decision}");
|
||||
Console.WriteLine($"Reason: {reason}");
|
||||
|
||||
|
||||
13
Services/Gitea/GiteaConfiguration.cs
Normal file
13
Services/Gitea/GiteaConfiguration.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Services.Gitea;
|
||||
|
||||
public class GiteaConfiguration
|
||||
{
|
||||
public string Host { get; set; } = string.Empty;
|
||||
public string Owner { get; set; } = string.Empty;
|
||||
public string Repository { get; set; } = string.Empty;
|
||||
public string Branch { get; set; } = string.Empty;
|
||||
public string ApiToken { get; set; } = string.Empty;
|
||||
|
||||
public string Url => $"{Host}/api/v1/repos/{Owner}/{Repository}";
|
||||
public string BaseUrl => $"{Host}/api/v1";
|
||||
}
|
||||
114
Services/Gitea/GiteaService.cs
Normal file
114
Services/Gitea/GiteaService.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Gitea.Net.Api;
|
||||
using Gitea.Net.Client;
|
||||
using Gitea.Net.Model;
|
||||
using HttpMethod = System.Net.Http.HttpMethod;
|
||||
|
||||
namespace Services.Gitea;
|
||||
|
||||
public class GiteaService : IGiteaService
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
|
||||
public RepositoryApi CreateGiteaClient(GiteaConfiguration configuration)
|
||||
{
|
||||
if (configuration == null)
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
|
||||
var config = new Configuration
|
||||
{
|
||||
BasePath = configuration.BaseUrl
|
||||
};
|
||||
|
||||
config.ApiKey = new Dictionary<string, string>
|
||||
{
|
||||
["token"] = configuration.ApiToken
|
||||
};
|
||||
|
||||
return new RepositoryApi(config);
|
||||
}
|
||||
|
||||
public async Task<List<Commit>> GetLastCommitsAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration,
|
||||
int limit)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (limit <= 0) throw new ArgumentOutOfRangeException(nameof(limit), "Limit must be greater than zero.");
|
||||
|
||||
var lastCommits = await repositoryApi.RepoGetAllCommitsAsync(configuration.Owner, configuration.Repository,
|
||||
configuration.Branch, limit: limit);
|
||||
return lastCommits;
|
||||
}
|
||||
|
||||
public async Task<Commit?> GetLastCommitAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
var lastCommit = await GetLastCommitsAsync(repositoryApi, configuration, 1);
|
||||
return lastCommit.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<Commit?> GetCommitByIdAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration,
|
||||
string commitId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (string.IsNullOrEmpty(commitId))
|
||||
throw new ArgumentException("CommitId cannot be null or empty.", nameof(commitId));
|
||||
|
||||
var lastCommit =
|
||||
await repositoryApi.RepoGetSingleCommitAsync(configuration.Owner, configuration.Repository, commitId);
|
||||
return lastCommit;
|
||||
}
|
||||
|
||||
public async Task<List<Branch>> GetAllBranchesAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
var branches = await repositoryApi.RepoListBranchesAsync(configuration.Owner, configuration.Repository);
|
||||
return branches;
|
||||
}
|
||||
|
||||
public async Task<ContentsResponse> GetFileContentAsync(RepositoryApi repositoryApi,
|
||||
GiteaConfiguration configuration, string filename, string branch = "master")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
throw new ArgumentException("Filename cannot be null or empty.", nameof(filename));
|
||||
|
||||
var repoGetContentsAsync = await repositoryApi.RepoGetContentsAsync(configuration.Owner,
|
||||
configuration.Repository, filename, varRef: branch);
|
||||
return repoGetContentsAsync;
|
||||
}
|
||||
|
||||
public async Task<(bool Status, string Response)> SendRequestAsync(GiteaConfiguration configuration,
|
||||
string endpoint, HttpContent content)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
if (string.IsNullOrEmpty(endpoint))
|
||||
throw new ArgumentException("Endpoint cannot be null or empty.", nameof(endpoint));
|
||||
|
||||
var requestUrl = $"{configuration.Url}/{endpoint}";
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
|
||||
request.Content = content;
|
||||
|
||||
request.Headers.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("token", configuration.ApiToken);
|
||||
|
||||
try
|
||||
{
|
||||
var giteaResponse = await HttpClient.SendAsync(request);
|
||||
var response = await giteaResponse.Content.ReadAsStringAsync();
|
||||
|
||||
return (giteaResponse.IsSuccessStatusCode, response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error while sending request to Gitea: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Services/Gitea/IGiteaService.cs
Normal file
22
Services/Gitea/IGiteaService.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Gitea.Net.Api;
|
||||
using Gitea.Net.Model;
|
||||
|
||||
namespace Services.Gitea;
|
||||
|
||||
public interface IGiteaService
|
||||
{
|
||||
RepositoryApi CreateGiteaClient(GiteaConfiguration configuration);
|
||||
Task<List<Commit>> GetLastCommitsAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration, int limit);
|
||||
|
||||
Task<ContentsResponse> GetFileContentAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration,
|
||||
string filename, string branch = "master");
|
||||
|
||||
Task<Commit?> GetCommitByIdAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration, string commitId);
|
||||
|
||||
Task<Commit?> GetLastCommitAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration);
|
||||
|
||||
Task<List<Branch>> GetAllBranchesAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration);
|
||||
|
||||
Task<(bool Status, string Response)> SendRequestAsync(GiteaConfiguration configuration, string endpoint,
|
||||
HttpContent content);
|
||||
}
|
||||
8
Services/OpenAI/IOpenAIService.cs
Normal file
8
Services/OpenAI/IOpenAIService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Services.OpenAI;
|
||||
|
||||
public interface IOpenAiService
|
||||
{
|
||||
Task<JsonDocument?> GetResponseFromChat(object request, string prompt, string model = "gpt-4.1-mini");
|
||||
}
|
||||
32
Services/OpenAI/OpenAiService.cs
Normal file
32
Services/OpenAI/OpenAiService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Text.Json;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using Services.Vault;
|
||||
|
||||
namespace Services.OpenAI;
|
||||
|
||||
public class OpenAiService(IVaultService vaultService) : IOpenAiService
|
||||
{
|
||||
public async Task<JsonDocument?> GetResponseFromChat(object request, string prompt, string model = "gpt-4.1-mini")
|
||||
{
|
||||
var apiKey = await vaultService.GetSecretAsync("api_keys/openai", "openai_api_key", "secret");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
Console.WriteLine("AI‑Gate: OPENAI_API_KEY not set, blocking by default.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var client = new OpenAIClient(apiKey);
|
||||
var userContent = JsonSerializer.Serialize(request);
|
||||
var chat = client.GetChatClient(model);
|
||||
var response = await chat.CompleteChatAsync(new SystemChatMessage(prompt),
|
||||
new UserChatMessage($"{userContent}"));
|
||||
var json = response.Value.Content[0].Text;
|
||||
|
||||
Console.WriteLine("AI‑Gate LLM raw response:");
|
||||
Console.WriteLine(json);
|
||||
|
||||
return JsonDocument.Parse(json);
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,18 @@
|
||||
<Authors>Piotr Kus</Authors>
|
||||
<Description>Service for communication with Vault</Description>
|
||||
<MinVerAutoIncrement>patch</MinVerAutoIncrement>
|
||||
<MinVerMinimumMajorMinor>1.0</MinVerMinimumMajorMinor>
|
||||
<MinVerMinimumMajorMinor>2.0</MinVerMinimumMajorMinor>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="VaultSharp" Version="1.17.5.1" />
|
||||
<PackageReference Include="Gitea.Net.API" Version="25.8.18" />
|
||||
<PackageReference Include="OpenAI" Version="2.8.0"/>
|
||||
<PackageReference Include="VaultSharp" Version="1.17.5.1"/>
|
||||
<PackageReference Include="MinVer" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
46
Tests/tests.trx
Normal file
46
Tests/tests.trx
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TestRun id="a459252d-fde3-4bb0-9b86-00a2c046526b" name="@Mac 2026-01-20 05:32:45" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
|
||||
<Times creation="2026-01-20T05:32:45.9495100+01:00" queuing="2026-01-20T05:32:45.9495100+01:00" start="2026-01-20T05:32:45.6832770+01:00" finish="2026-01-20T05:32:45.9519250+01:00" />
|
||||
<TestSettings name="default" id="bb142d87-2085-458c-ae53-1afd077420df">
|
||||
<Deployment runDeploymentRoot="_Mac_2026-01-20_05_32_45" />
|
||||
</TestSettings>
|
||||
<Results>
|
||||
<UnitTestResult executionId="e21b7380-9510-4f6c-b33f-b8ccae8ed977" testId="d391ec73-c151-00a2-218d-506bf980c06b" testName="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenIntervalNotExceeded_ReturnsFalse" computerName="Mac" duration="00:00:00.0017257" startTime="2026-01-20T05:32:45.9105340+01:00" endTime="2026-01-20T05:32:45.9105410+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="e21b7380-9510-4f6c-b33f-b8ccae8ed977" />
|
||||
<UnitTestResult executionId="6187b7d7-7b65-4f82-8125-bb75724b1d8b" testId="f87ac951-f9bb-5bc1-a589-4f242697f2f7" testName="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenEmailAlreadySent_ReturnsFalse" computerName="Mac" duration="00:00:00.0000283" startTime="2026-01-20T05:32:45.9116470+01:00" endTime="2026-01-20T05:32:45.9116470+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6187b7d7-7b65-4f82-8125-bb75724b1d8b" />
|
||||
<UnitTestResult executionId="993fdb22-2e6d-4e99-9daf-b604cc09eb49" testId="79b5babd-1f8c-4a15-303e-4439267af85a" testName="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldSendAlert_WhenIntervalExceededAndNotSent_ReturnsTrue" computerName="Mac" duration="00:00:00.0000677" startTime="2026-01-20T05:32:45.9115300+01:00" endTime="2026-01-20T05:32:45.9115300+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="993fdb22-2e6d-4e99-9daf-b604cc09eb49" />
|
||||
</Results>
|
||||
<TestDefinitions>
|
||||
<UnitTest name="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldSendAlert_WhenIntervalExceededAndNotSent_ReturnsTrue" storage="/users/piotrkus/riderprojects/fa/heartbeatclient.tests/bin/debug/net8.0/heartbeatclient.tests.dll" id="79b5babd-1f8c-4a15-303e-4439267af85a">
|
||||
<Execution id="993fdb22-2e6d-4e99-9daf-b604cc09eb49" />
|
||||
<TestMethod codeBase="/Users/piotrkus/RiderProjects/FA/HeartBeatClient.Tests/bin/Debug/net8.0/HeartBeatClient.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="HeartBeatClient.Tests.HeartBeatDecisionServiceTests" name="ShouldSendAlert_WhenIntervalExceededAndNotSent_ReturnsTrue" />
|
||||
</UnitTest>
|
||||
<UnitTest name="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenEmailAlreadySent_ReturnsFalse" storage="/users/piotrkus/riderprojects/fa/heartbeatclient.tests/bin/debug/net8.0/heartbeatclient.tests.dll" id="f87ac951-f9bb-5bc1-a589-4f242697f2f7">
|
||||
<Execution id="6187b7d7-7b65-4f82-8125-bb75724b1d8b" />
|
||||
<TestMethod codeBase="/Users/piotrkus/RiderProjects/FA/HeartBeatClient.Tests/bin/Debug/net8.0/HeartBeatClient.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="HeartBeatClient.Tests.HeartBeatDecisionServiceTests" name="ShouldNotSendAlert_WhenEmailAlreadySent_ReturnsFalse" />
|
||||
</UnitTest>
|
||||
<UnitTest name="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenIntervalNotExceeded_ReturnsFalse" storage="/users/piotrkus/riderprojects/fa/heartbeatclient.tests/bin/debug/net8.0/heartbeatclient.tests.dll" id="d391ec73-c151-00a2-218d-506bf980c06b">
|
||||
<Execution id="e21b7380-9510-4f6c-b33f-b8ccae8ed977" />
|
||||
<TestMethod codeBase="/Users/piotrkus/RiderProjects/FA/HeartBeatClient.Tests/bin/Debug/net8.0/HeartBeatClient.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="HeartBeatClient.Tests.HeartBeatDecisionServiceTests" name="ShouldNotSendAlert_WhenIntervalNotExceeded_ReturnsFalse" />
|
||||
</UnitTest>
|
||||
</TestDefinitions>
|
||||
<TestEntries>
|
||||
<TestEntry testId="d391ec73-c151-00a2-218d-506bf980c06b" executionId="e21b7380-9510-4f6c-b33f-b8ccae8ed977" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="f87ac951-f9bb-5bc1-a589-4f242697f2f7" executionId="6187b7d7-7b65-4f82-8125-bb75724b1d8b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="79b5babd-1f8c-4a15-303e-4439267af85a" executionId="993fdb22-2e6d-4e99-9daf-b604cc09eb49" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
</TestEntries>
|
||||
<TestLists>
|
||||
<TestList name="Wyniki, których nie ma na liście" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestList name="Wszystkie załadowane wyniki" id="19431567-8539-422a-85d7-44ee4e166bda" />
|
||||
</TestLists>
|
||||
<ResultSummary outcome="Completed">
|
||||
<Counters total="3" executed="3" passed="3" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
|
||||
<Output>
|
||||
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.1+6b60a9e56a (64-bit .NET 8.0.12)
|
||||
[xUnit.net 00:00:00.02] Discovering: HeartBeatClient.Tests
|
||||
[xUnit.net 00:00:00.03] Discovered: HeartBeatClient.Tests
|
||||
[xUnit.net 00:00:00.03] Starting: HeartBeatClient.Tests
|
||||
[xUnit.net 00:00:00.05] Finished: HeartBeatClient.Tests
|
||||
</StdOut>
|
||||
</Output>
|
||||
</ResultSummary>
|
||||
</TestRun>
|
||||
2
parameters.env
Normal file
2
parameters.env
Normal file
@@ -0,0 +1,2 @@
|
||||
VAULT_TOKEN=hvs.CAESID7fTT61Cs0C37k4pEr98_GXonsWfIgavM0a0nEAb9-qGh4KHGh2cy5DUnNnNjB5Yklla3c3cVNrYmdab01GSzM
|
||||
VAULT_URL=https://vault.modwad.pl
|
||||
Reference in New Issue
Block a user