diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8083a069 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +**/bin +**/obj +test-artifacts +**/TestResults +**/*.lic +.env +**/.env* diff --git a/.github/workflows/_bot-build.yml b/.github/workflows/_bot-build.yml index 00a3255b..48457ae7 100644 --- a/.github/workflows/_bot-build.yml +++ b/.github/workflows/_bot-build.yml @@ -26,6 +26,10 @@ on: regex narrows further to per-bot relevance. Manual triggers always run the full build regardless. + secrets: + SIXLABORS_LICENSE_KEY: + required: false + jobs: build: runs-on: ${{ inputs.runner }} @@ -93,6 +97,8 @@ jobs: - name: Tests if: steps.should-run.outputs.yes == 'true' + env: + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} run: ${{ inputs.test-command }} - name: Upload dotnet test results diff --git a/.github/workflows/_bot-deploy.yml b/.github/workflows/_bot-deploy.yml index c795b809..ac6d1dd0 100644 --- a/.github/workflows/_bot-deploy.yml +++ b/.github/workflows/_bot-deploy.yml @@ -46,6 +46,8 @@ on: default: true description: "Open a deploy-failure issue (wakes the SRE agent) when verify-deploy fails" secrets: + SIXLABORS_LICENSE_KEY: + required: false WIREGUARD_CONFIG: required: true description: >- @@ -169,6 +171,8 @@ jobs: global-json-file: global.json - name: Tests + env: + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} run: ${{ inputs.test-command }} - name: Upload dotnet test results @@ -244,10 +248,13 @@ jobs: - name: Build and push to GHCR uses: docker/build-push-action@v5 + env: + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} with: push: true tags: ${{ inputs.docker-image }}:${{ github.sha }}, ${{ inputs.docker-image }}:latest file: ./src/Dockerfile.bot + secret-envs: ${{ inputs.bot-project == 'CouponHubBot' && 'sixlabors-license=SIXLABORS_LICENSE_KEY' || '' }} build-args: BOT_PROJECT=${{ inputs.bot-project }} platforms: linux/arm64 diff --git a/.github/workflows/coupon-build.yml b/.github/workflows/coupon-build.yml index 198e1352..a789ea75 100644 --- a/.github/workflows/coupon-build.yml +++ b/.github/workflows/coupon-build.yml @@ -14,3 +14,5 @@ jobs: dotnet test tests/CouponHubBot.Tests/CouponHubBot.Tests.fsproj -c Release --logger "trx;LogFileName=test-results.trx" --logger "console;verbosity=detailed" dotnet test tests/CouponHubBot.Ocr.Tests/CouponHubBot.Ocr.Tests.fsproj -c Release --logger "trx;LogFileName=test-results.trx" --logger "console;verbosity=detailed" bot-paths-regex: '^(src/(BotInfra|CouponHubBot)/|src/Dockerfile\.bot$|tests/(CouponHubBot\.Tests|CouponHubBot\.Ocr\.Tests|BotTestInfra|FakeTgApi|FakeAzureOcrApi)/|Directory\.Build\.props$|Directory\.Packages\.props$|global\.json$)' + secrets: + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} diff --git a/.github/workflows/coupon-deploy.yml b/.github/workflows/coupon-deploy.yml index e68fca0d..bc23866b 100644 --- a/.github/workflows/coupon-deploy.yml +++ b/.github/workflows/coupon-deploy.yml @@ -51,6 +51,7 @@ jobs: argocd-app-name: coupon-bot container-name: coupon-bot secrets: + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} # Dedicated single-occupancy peer (Szer/my-infra#111) — do not point # this back at the shared WIREGUARD_CONFIG secret; concurrent bot # deploys over one peer flap the WG endpoint (issue #368). diff --git a/.github/workflows/coupon-real-test.yml b/.github/workflows/coupon-real-test.yml index 0d7f2d26..33955617 100644 --- a/.github/workflows/coupon-real-test.yml +++ b/.github/workflows/coupon-real-test.yml @@ -164,6 +164,8 @@ jobs: - name: Build and push test image uses: docker/build-push-action@v5 + env: + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} with: push: true # test- tag — never collides with prod's `:`/`:latest` tags @@ -171,6 +173,8 @@ jobs: # convention as alita-real-test.yml. tags: ${{ env.IMAGE }}:test-${{ github.sha }} file: ./src/Dockerfile.bot + secret-envs: | + sixlabors-license=SIXLABORS_LICENSE_KEY build-args: BOT_PROJECT=CouponHubBot platforms: linux/arm64 @@ -432,6 +436,7 @@ jobs: # — this workflow is the one deliberate, workflow_dispatch-only path that is # meant to set it. Never set this for any other job/workflow. COUPON_REAL_TESTS: "1" + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} COUPON_REAL_MODE: remote COUPON_TEST_BOT_USERNAME: ${{ secrets.COUPON_CI_BOT_USERNAME }} COUPON_TEST_CHAT_ID: ${{ secrets.COUPON_CI_CHAT_ID }} diff --git a/.github/workflows/multipod-build.yml b/.github/workflows/multipod-build.yml index 9ab3971b..923f6a8f 100644 --- a/.github/workflows/multipod-build.yml +++ b/.github/workflows/multipod-build.yml @@ -12,3 +12,5 @@ jobs: test-command: | dotnet test tests/MultiPodTests/MultiPodTests.fsproj -c Release --logger "trx;LogFileName=test-results.trx" --logger "console;verbosity=detailed" bot-paths-regex: '^(src/(BotInfra|VahterBanBot|CouponHubBot)/|src/Dockerfile\.bot$|tests/(MultiPodTests|BotTestInfra|FakeTgApi|FakeAzureOcrApi)/|Directory\.Build\.props$|Directory\.Packages\.props$|global\.json$)' + secrets: + SIXLABORS_LICENSE_KEY: ${{ secrets.SIXLABORS_LICENSE_KEY }} diff --git a/.github/workflows/vahter-upstream-sync.yml b/.github/workflows/vahter-upstream-sync.yml index cc1e6a02..5ce01308 100644 --- a/.github/workflows/vahter-upstream-sync.yml +++ b/.github/workflows/vahter-upstream-sync.yml @@ -37,6 +37,7 @@ on: - 'tests/Dockerfile.fake' - 'tests/Directory.Build.targets' - '.gitignore' + - '.dockerignore' - 'LICENSE' - '.github/workflows/_bot-build.yml' - '.github/upstream/**' @@ -94,6 +95,7 @@ jobs: cp ../monorepo/Directory.Packages.props . cp ../monorepo/global.json . cp ../monorepo/.gitignore . + cp ../monorepo/.dockerignore . cp ../monorepo/LICENSE . cp ../monorepo/.github/upstream/README.md README.md diff --git a/Directory.Build.props b/Directory.Build.props index 99812e68..c6cbcf8b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,4 +11,8 @@ + + $(SIXLABORS_LICENSE_KEY) + + diff --git a/Directory.Packages.props b/Directory.Packages.props index dced9c55..b068b0dd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -53,9 +53,9 @@ - + - + diff --git a/src/CouponHubBot/CouponHubBot.fsproj b/src/CouponHubBot/CouponHubBot.fsproj index ed38af0f..b17fc197 100644 --- a/src/CouponHubBot/CouponHubBot.fsproj +++ b/src/CouponHubBot/CouponHubBot.fsproj @@ -29,7 +29,7 @@ - + diff --git a/src/CouponHubBot/docs/TESTING.md b/src/CouponHubBot/docs/TESTING.md index 6f0125b2..a0a7cf1f 100644 --- a/src/CouponHubBot/docs/TESTING.md +++ b/src/CouponHubBot/docs/TESTING.md @@ -17,6 +17,13 @@ E2E tests live in `tests/CouponHubBot.Tests/`. They use Testcontainers to spin u All containers run in a shared Docker network. Tests interact via HTTP against mapped host ports. +### Build license + +Set `SIXLABORS_LICENSE_KEY` to the complete contents of the issued Six Labors license for `CouponHubBot`; Release builds validate it. +GitHub Actions uses the repository secret of the same name, and container builds pass it through a build secret mount. + +Bot test images use the Docker or Podman CLI. Set `BOT_CONTAINER_ENGINE` to an executable path when neither CLI is on `PATH`. + ### Running Tests ```bash diff --git a/src/Dockerfile.bot b/src/Dockerfile.bot index 73bf823e..fb08cc00 100644 --- a/src/Dockerfile.bot +++ b/src/Dockerfile.bot @@ -16,7 +16,8 @@ RUN dotnet restore src/${BOT_PROJECT}/${BOT_PROJECT}.fsproj COPY src/BotInfra/ src/BotInfra/ COPY src/${BOT_PROJECT}/ src/${BOT_PROJECT}/ -RUN dotnet publish src/${BOT_PROJECT}/${BOT_PROJECT}.fsproj -c Release -o /publish +RUN --mount=type=secret,id=sixlabors-license \ + SixLaborsLicenseFile=/run/secrets/sixlabors-license dotnet publish src/${BOT_PROJECT}/${BOT_PROJECT}.fsproj -c Release -o /publish FROM mcr.microsoft.com/dotnet/aspnet:10.0.3-noble AS runtime ARG BOT_PROJECT diff --git a/src/VahterBanBot/DB.fs b/src/VahterBanBot/DB.fs index 2db08392..b4ae3323 100644 --- a/src/VahterBanBot/DB.fs +++ b/src/VahterBanBot/DB.fs @@ -18,6 +18,12 @@ type UserMessage = { chat_id: int64 message_id: int64 } +[] +type TriageHistoryMessage = + { chat_id: int64 + text: string | null + created_at: DateTime } + /// Lightweight DTO for TryGetMessageTextAndSender (D2 cache invalidation on the admin-command /// correction paths, which only have a chat/message reference, not the original TgMessage). [] @@ -361,6 +367,29 @@ ON CONFLICT DO NOTHING (Option.ofObj msg.Text) (utcNow()) + member _.GetTriageHistory(userId: int64, chatId: int64, messageId: int64, ct: System.Threading.CancellationToken) : Task = task { + use conn = new NpgsqlConnection(connString) + let sql = """ +SELECT COUNT(*) FROM snapshot_message +WHERE user_id = @userId + AND (chat_id, message_id) <> (@chatId, @messageId) + AND created_at < @now; +SELECT chat_id, LEFT(text, 1001) AS text, created_at +FROM snapshot_message +WHERE user_id = @userId + AND (chat_id, message_id) <> (@chatId, @messageId) + AND created_at < @now +ORDER BY created_at DESC, chat_id DESC, message_id DESC +LIMIT 4 +""" + use! results = conn.QueryMultipleAsync(CommandDefinition( + sql, {| userId = userId; chatId = chatId; messageId = messageId; now = utcNow() |}, + cancellationToken = ct, commandTimeout = 2)) + let! count = results.ReadSingleAsync() + let! rows = results.ReadAsync() + return count, Array.ofSeq rows + } + member _.GetUserMessages(userId: int64) : Task = task { use conn = new NpgsqlConnection(connString) diff --git a/src/VahterBanBot/LlmTriage.fs b/src/VahterBanBot/LlmTriage.fs index a0f74fe1..45fd5518 100644 --- a/src/VahterBanBot/LlmTriage.fs +++ b/src/VahterBanBot/LlmTriage.fs @@ -380,63 +380,38 @@ type AzureLlmTriage(botConf: IOptions, logger: ILogger