From 74ee8fcf0a83ce3f7c9fb3533e9a6e20d2a2ed05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Fri, 28 Nov 2025 23:02:37 +0100 Subject: [PATCH 01/12] add internal functionality for webhook sending --- internal/discord/discord.go | 38 +++++++++++++++ internal/discord/discord_test.go | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 internal/discord/discord.go create mode 100644 internal/discord/discord_test.go diff --git a/internal/discord/discord.go b/internal/discord/discord.go new file mode 100644 index 0000000..3a9782d --- /dev/null +++ b/internal/discord/discord.go @@ -0,0 +1,38 @@ +package discord + +import ( + "fmt" + "net/http" + "strings" + + "github.com/rs/zerolog/log" +) + +type DiscordNotificationData struct { + WebhookURL string + Content string +} + +func SendDiscordNotification(data DiscordNotificationData) error { + payload := fmt.Sprintf(`{"content": "%s"}`, data.Content) + + req, err := http.NewRequest("POST", data.WebhookURL, strings.NewReader(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to send Discord notification, status code: %d", resp.StatusCode) + } + + log.Info().Msg("Discord notification sent successfully") + return nil +} \ No newline at end of file diff --git a/internal/discord/discord_test.go b/internal/discord/discord_test.go new file mode 100644 index 0000000..ec96210 --- /dev/null +++ b/internal/discord/discord_test.go @@ -0,0 +1,83 @@ +package discord + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSendDiscordNotification_Success(t *testing.T) { + require := require.New(t) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal("POST", r.Method) + require.Equal("application/json", r.Header.Get("Content-Type")) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + err := SendDiscordNotification(DiscordNotificationData{ + WebhookURL: server.URL, + Content: "Test notification", + }) + require.NoError(err) +} + +func TestSendDiscordNotification_StatusCodes(t *testing.T) { + tests := []struct { + name string + statusCode int + wantError bool + }{ + {"200 OK", 200, false}, + {"204 No Content", 204, false}, + {"299 Max Success", 299, false}, + {"300 Redirect", 300, true}, + {"400 Bad Request", 400, true}, + {"500 Server Error", 500, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + })) + defer server.Close() + + err := SendDiscordNotification(DiscordNotificationData{ + WebhookURL: server.URL, + Content: "Test", + }) + + if tt.wantError { + require.Error(err) + } else { + require.NoError(err) + } + }) + } +} + +func TestSendDiscordNotification_InvalidURL(t *testing.T) { + require := require.New(t) + + err := SendDiscordNotification(DiscordNotificationData{ + WebhookURL: "invalid url", + Content: "Test", + }) + require.Error(err) +} + +func TestSendDiscordNotification_NetworkError(t *testing.T) { + require := require.New(t) + + err := SendDiscordNotification(DiscordNotificationData{ + WebhookURL: "http://nonexistent.invalid", + Content: "Test", + }) + require.Error(err) +} From fcbf705b5c3a838897599118b45ce20ac8ce0192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Fri, 28 Nov 2025 23:09:11 +0100 Subject: [PATCH 02/12] add webhook url env loader --- internal/discord/discord.go | 16 +++++++++++++--- internal/discord/discord_test.go | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/internal/discord/discord.go b/internal/discord/discord.go index 3a9782d..d4f19cd 100644 --- a/internal/discord/discord.go +++ b/internal/discord/discord.go @@ -3,6 +3,7 @@ package discord import ( "fmt" "net/http" + "os" "strings" "github.com/rs/zerolog/log" @@ -10,12 +11,12 @@ import ( type DiscordNotificationData struct { WebhookURL string - Content string + Content string } func SendDiscordNotification(data DiscordNotificationData) error { payload := fmt.Sprintf(`{"content": "%s"}`, data.Content) - + req, err := http.NewRequest("POST", data.WebhookURL, strings.NewReader(payload)) if err != nil { return err @@ -35,4 +36,13 @@ func SendDiscordNotification(data DiscordNotificationData) error { log.Info().Msg("Discord notification sent successfully") return nil -} \ No newline at end of file +} + +func LoadWebhookURL() string { + webhookUrl := os.Getenv("DISCORD_WEBHOOK_URL") + + if webhookUrl == "" { + log.Error().Msg("DISCORD_WEBHOOK_URL environment variable is not set") + } + return webhookUrl +} diff --git a/internal/discord/discord_test.go b/internal/discord/discord_test.go index ec96210..d0d32ea 100644 --- a/internal/discord/discord_test.go +++ b/internal/discord/discord_test.go @@ -81,3 +81,22 @@ func TestSendDiscordNotification_NetworkError(t *testing.T) { }) require.Error(err) } + +func TestLoadWebhookURL_Success(t *testing.T) { + require := require.New(t) + + webhookURL := "https://discord.com/api/webhooks/123/abc" + t.Setenv("DISCORD_WEBHOOK_URL", webhookURL) + + result := LoadWebhookURL() + require.Equal(webhookURL, result) +} + +func TestLoadWebhookURL_Missing(t *testing.T) { + require := require.New(t) + + t.Setenv("DISCORD_WEBHOOK_URL", "") + + result := LoadWebhookURL() + require.Equal("", result) +} From 55abc353c660ead38febb51042f71f4103ccf47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Fri, 28 Nov 2025 23:11:05 +0100 Subject: [PATCH 03/12] add basic discord logging to cli --- internal/cli/cli.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index f2124de..e6e5919 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -14,6 +14,7 @@ import ( "github.com/VATSIM-UK/ukcp-srd-tools/internal/airac" "github.com/VATSIM-UK/ukcp-srd-tools/internal/db" + "github.com/VATSIM-UK/ukcp-srd-tools/internal/discord" "github.com/VATSIM-UK/ukcp-srd-tools/internal/download" "github.com/VATSIM-UK/ukcp-srd-tools/internal/excel" "github.com/VATSIM-UK/ukcp-srd-tools/internal/file" @@ -222,6 +223,11 @@ func importProcess(ctx context.Context, filePath string, cycle string, envPath s // Get the filename from the command line path, _ := filepath.Abs(filePath) + discord.SendDiscordNotification(discord.DiscordNotificationData{ + WebhookURL: discord.LoadWebhookURL(), + Content: "Starting SRD import for AIRAC cycle " + cycle, + }) + file, err := loadSrdFile(path) if err != nil { return err @@ -292,6 +298,11 @@ func importProcess(ctx context.Context, filePath string, cycle string, envPath s // Print the stats printStats(file.Stats()) + discord.SendDiscordNotification(discord.DiscordNotificationData{ + WebhookURL: discord.LoadWebhookURL(), + Content: "SRD import complete for AIRAC cycle " + cycle, + }) + return nil } @@ -335,6 +346,11 @@ func doDownload(ctx context.Context, force bool, forceCycle string, envPath stri return err } + discord.SendDiscordNotification(discord.DiscordNotificationData{ + WebhookURL: discord.LoadWebhookURL(), + Content: "Starting SRD download for AIRAC cycle " + cycleToDownload.Ident, + }) + // Download the SRD file downloadUrl := download.DownloadUrl(cycleToDownload) if CLI.Download.Url != "" { @@ -353,6 +369,11 @@ func doDownload(ctx context.Context, force bool, forceCycle string, envPath stri return err } + discord.SendDiscordNotification(discord.DiscordNotificationData{ + WebhookURL: discord.LoadWebhookURL(), + Content: "SRD download complete for AIRAC cycle " + cycleToDownload.Ident, + }) + // Download happened, so now we do the import return importProcess(ctx, downloader.LatestFileLocation(), cycleToDownload.Ident, envPath, fileDir) } From 9bbea6e796684d8420355a2cb9711e0a42cdbd7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Fri, 28 Nov 2025 23:15:07 +0100 Subject: [PATCH 04/12] extend cli tests to cover discord --- internal/cli/cli_test.go | 75 +++++++++++++++++++++++++------- internal/discord/discord.go | 2 +- internal/discord/discord_test.go | 27 ++++++++++++ 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 8170583..0203b40 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -338,6 +338,10 @@ func TestRun_ImportSuccess(t *testing.T) { testDir := t.TempDir() envFilePath := fmt.Sprintf("%s/%s", testDir, "test.env") + // Setup Discord webhook mock + discordMock := getDiscordWebhookMock() + defer discordMock.server.Close() + // If there's a setup function, run it if tt.setupFunc != nil { test := getCliTestWithTempDir(tt.extraArgs, testDir) @@ -358,21 +362,27 @@ func TestRun_ImportSuccess(t *testing.T) { containerPort, err := mysqlContainer.container.MappedPort(ctx, "3306") require.NoError(err) - // Now write the env file with our database credentaisl - err = godotenv.Write( + // Now write the env file with our database credentials and Discord webhook + require.NoError(godotenv.Write( map[string]string{ - "DB_HOST": containerHost, - "DB_PORT": containerPort.Port(), - "DB_USERNAME": TestUsername, - "DB_DATABASE": TestDatabase, - "DB_PASSWORD": TestPassword, + "DB_HOST": containerHost, + "DB_PORT": containerPort.Port(), + "DB_USERNAME": TestUsername, + "DB_DATABASE": TestDatabase, + "DB_PASSWORD": TestPassword, + "DISCORD_WEBHOOK_URL": discordMock.server.URL, }, envFilePath, - ) + )) // Run the CLI test require.NoError(cli.Run(testDir)) + // Check Discord webhook was called (start and complete notifications) + require.Equal(2, discordMock.callCount, "expected Discord webhook to be called twice (start and complete)") + require.Contains(discordMock.messages[0], "Starting SRD import for AIRAC cycle 2404") + require.Contains(discordMock.messages[1], "SRD import complete for AIRAC cycle 2404") + // Check the logs for _, msg := range tt.expectedLogMessages { test.logRecorder.AssertHasString(require, msg) @@ -673,6 +683,10 @@ func TestDownloadSuccess(t *testing.T) { testDir := t.TempDir() envFilePath := fmt.Sprintf("%s/%s", testDir, "test.env") + // Setup Discord webhook mock + discordMock := getDiscordWebhookMock() + defer discordMock.server.Close() + // Download file path fileName := testDataFile(tt.fileName) @@ -718,21 +732,29 @@ func TestDownloadSuccess(t *testing.T) { containerPort, err := mysqlContainer.container.MappedPort(ctx, "3306") require.NoError(err) - // Now write the env file with our database credentaisl - err = godotenv.Write( + // Now write the env file with our database credentials and Discord webhook + require.NoError(godotenv.Write( map[string]string{ - "DB_HOST": containerHost, - "DB_PORT": containerPort.Port(), - "DB_USERNAME": TestUsername, - "DB_DATABASE": TestDatabase, - "DB_PASSWORD": TestPassword, + "DB_HOST": containerHost, + "DB_PORT": containerPort.Port(), + "DB_USERNAME": TestUsername, + "DB_DATABASE": TestDatabase, + "DB_PASSWORD": TestPassword, + "DISCORD_WEBHOOK_URL": discordMock.server.URL, }, envFilePath, - ) + )) // Run the CLI test require.NoError(cli.Run(testDir)) + // Check Discord webhook was called (download start, download complete, import start, import complete) + require.Equal(4, discordMock.callCount, "expected Discord webhook to be called four times (download start/complete and import start/complete)") + require.Contains(discordMock.messages[0], "Starting SRD download for AIRAC cycle") + require.Contains(discordMock.messages[1], "SRD download complete for AIRAC cycle") + require.Contains(discordMock.messages[2], "Starting SRD import for AIRAC cycle") + require.Contains(discordMock.messages[3], "SRD import complete for AIRAC cycle") + // Check the logs for _, msg := range tt.expectedLogMessages { test.logRecorder.AssertHasString(require, msg) @@ -889,6 +911,12 @@ type testServer struct { wrapInZip bool } +type discordWebhookMock struct { + callCount int + messages []string + server *httptest.Server +} + func (t *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { t.callCount++ @@ -971,3 +999,18 @@ func getTestServer(statusCode int, pathToServe string) *testServer { return testServer } + +func getDiscordWebhookMock() *discordWebhookMock { + mock := &discordWebhookMock{ + messages: []string{}, + } + mock.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mock.callCount++ + body, err := io.ReadAll(r.Body) + if err == nil { + mock.messages = append(mock.messages, string(body)) + } + w.WriteHeader(http.StatusOK) + })) + return mock +} diff --git a/internal/discord/discord.go b/internal/discord/discord.go index d4f19cd..fc379a2 100644 --- a/internal/discord/discord.go +++ b/internal/discord/discord.go @@ -15,7 +15,7 @@ type DiscordNotificationData struct { } func SendDiscordNotification(data DiscordNotificationData) error { - payload := fmt.Sprintf(`{"content": "%s"}`, data.Content) + payload := fmt.Sprintf(`{"content": "%s", "username": "UKCP SRD Tools"}`, data.Content) req, err := http.NewRequest("POST", data.WebhookURL, strings.NewReader(payload)) if err != nil { diff --git a/internal/discord/discord_test.go b/internal/discord/discord_test.go index d0d32ea..939bb81 100644 --- a/internal/discord/discord_test.go +++ b/internal/discord/discord_test.go @@ -1,6 +1,8 @@ package discord import ( + "encoding/json" + "io" "net/http" "net/http/httptest" "testing" @@ -100,3 +102,28 @@ func TestLoadWebhookURL_Missing(t *testing.T) { result := LoadWebhookURL() require.Equal("", result) } + +func TestSendDiscordNotification_PayloadContent(t *testing.T) { + require := require.New(t) + + var receivedPayload map[string]string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(err) + + err = json.Unmarshal(body, &receivedPayload) + require.NoError(err) + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + testContent := "Test import for cycle 2404" + err := SendDiscordNotification(DiscordNotificationData{ + WebhookURL: server.URL, + Content: testContent, + }) + require.NoError(err) + + require.Equal(testContent, receivedPayload["content"]) +} From 478318f6327c1571a5bebc2f8ae1bdafe0221a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Fri, 28 Nov 2025 23:22:46 +0100 Subject: [PATCH 05/12] handle discord errors in cli --- internal/cli/cli.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index e6e5919..c60da3d 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -223,10 +223,13 @@ func importProcess(ctx context.Context, filePath string, cycle string, envPath s // Get the filename from the command line path, _ := filepath.Abs(filePath) - discord.SendDiscordNotification(discord.DiscordNotificationData{ + err := discord.SendDiscordNotification(discord.DiscordNotificationData{ WebhookURL: discord.LoadWebhookURL(), Content: "Starting SRD import for AIRAC cycle " + cycle, }) + if err != nil { + log.Error().Err(err).Msg("failed to send Discord notification") + } file, err := loadSrdFile(path) if err != nil { @@ -298,10 +301,13 @@ func importProcess(ctx context.Context, filePath string, cycle string, envPath s // Print the stats printStats(file.Stats()) - discord.SendDiscordNotification(discord.DiscordNotificationData{ + err = discord.SendDiscordNotification(discord.DiscordNotificationData{ WebhookURL: discord.LoadWebhookURL(), Content: "SRD import complete for AIRAC cycle " + cycle, }) + if err != nil { + log.Error().Err(err).Msg("failed to send Discord notification") + } return nil } @@ -346,10 +352,13 @@ func doDownload(ctx context.Context, force bool, forceCycle string, envPath stri return err } - discord.SendDiscordNotification(discord.DiscordNotificationData{ + err = discord.SendDiscordNotification(discord.DiscordNotificationData{ WebhookURL: discord.LoadWebhookURL(), Content: "Starting SRD download for AIRAC cycle " + cycleToDownload.Ident, }) + if err != nil { + log.Error().Err(err).Msg("failed to send Discord notification") + } // Download the SRD file downloadUrl := download.DownloadUrl(cycleToDownload) From 709cd6d34e88b8d002d90b3c8c3930a02e061aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Fri, 28 Nov 2025 23:23:55 +0100 Subject: [PATCH 06/12] one more error left to handle --- internal/cli/cli.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index c60da3d..e6733a3 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -378,10 +378,13 @@ func doDownload(ctx context.Context, force bool, forceCycle string, envPath stri return err } - discord.SendDiscordNotification(discord.DiscordNotificationData{ + err = discord.SendDiscordNotification(discord.DiscordNotificationData{ WebhookURL: discord.LoadWebhookURL(), Content: "SRD download complete for AIRAC cycle " + cycleToDownload.Ident, }) + if err != nil { + log.Error().Err(err).Msg("failed to send Discord notification") + } // Download happened, so now we do the import return importProcess(ctx, downloader.LatestFileLocation(), cycleToDownload.Ident, envPath, fileDir) From 4a7651cb52ec29f823ad6247b04d1520860d6692 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:35:45 +0000 Subject: [PATCH 07/12] Initial plan From 41781bab810b6971cfa85a17369889f8c8627c48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:39:26 +0000 Subject: [PATCH 08/12] fix: load .env before sending first Discord notification in importProcess Co-authored-by: kristiankunc <26331401+kristiankunc@users.noreply.github.com> --- internal/cli/cli.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index e6733a3..c8549d8 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -223,7 +223,14 @@ func importProcess(ctx context.Context, filePath string, cycle string, envPath s // Get the filename from the command line path, _ := filepath.Abs(filePath) - err := discord.SendDiscordNotification(discord.DiscordNotificationData{ + // Load the .env file before sending Discord notifications so the webhook URL is available + err := godotenv.Overload(envPath) + if err != nil { + log.Error().Err(err).Msg("failed to load environment file") + return ErrCannotLoadDotenv + } + + err = discord.SendDiscordNotification(discord.DiscordNotificationData{ WebhookURL: discord.LoadWebhookURL(), Content: "Starting SRD import for AIRAC cycle " + cycle, }) @@ -251,13 +258,6 @@ func importProcess(ctx context.Context, filePath string, cycle string, envPath s log.Info().Msgf("importing SRD file %v for cycle %v", path, airacCycle.Ident) - // Load the .env file - err = godotenv.Overload(envPath) - if err != nil { - log.Error().Err(err).Msg("failed to load environment file") - return ErrCannotLoadDotenv - } - // Get the database connection parameters dbParams, err := getDatabaseConnectionParams() if err != nil { From 991509f324b0d031c166ba8224f080dda861efb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:40:44 +0000 Subject: [PATCH 09/12] Fix Discord start notification missing webhook URL on import Co-authored-by: kristiankunc <26331401+kristiankunc@users.noreply.github.com> --- go.mod | 9 ++++----- go.sum | 6 ++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 6c46b09..95d8398 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,14 @@ go 1.23.1 require ( github.com/alecthomas/kong v1.2.1 + github.com/alexflint/go-filemutex v1.3.0 github.com/benbjohnson/clock v1.3.5 github.com/go-sql-driver/mysql v1.8.1 + github.com/joho/godotenv v1.5.1 + github.com/rs/zerolog v1.33.0 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.33.0 + github.com/xuri/excelize/v2 v2.8.1 github.com/youkuang/xls v0.0.1 ) @@ -16,7 +20,6 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/alexflint/go-filemutex v1.3.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect @@ -34,9 +37,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/renameio v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -56,7 +57,6 @@ require ( github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect - github.com/rs/zerolog v1.33.0 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -65,7 +65,6 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect - github.com/xuri/excelize/v2 v2.8.1 // indirect github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect diff --git a/go.sum b/go.sum index 09261c8..effac56 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= -github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= @@ -189,6 +187,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -214,8 +214,6 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= From 92e4c05156c3a8e2f9149ea1db7c5c02b17b534a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Tue, 24 Feb 2026 21:45:20 +0100 Subject: [PATCH 10/12] revert mod changes --- go.mod | 9 +++++---- go.sum | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 95d8398..6c46b09 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,10 @@ go 1.23.1 require ( github.com/alecthomas/kong v1.2.1 - github.com/alexflint/go-filemutex v1.3.0 github.com/benbjohnson/clock v1.3.5 github.com/go-sql-driver/mysql v1.8.1 - github.com/joho/godotenv v1.5.1 - github.com/rs/zerolog v1.33.0 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.33.0 - github.com/xuri/excelize/v2 v2.8.1 github.com/youkuang/xls v0.0.1 ) @@ -20,6 +16,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/alexflint/go-filemutex v1.3.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect @@ -37,7 +34,9 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/renameio v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -57,6 +56,7 @@ require ( github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -65,6 +65,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect + github.com/xuri/excelize/v2 v2.8.1 // indirect github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect diff --git a/go.sum b/go.sum index effac56..09261c8 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= +github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= @@ -187,8 +189,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= -golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -214,6 +214,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= From 67ab53ecfd870d895e3540fde0a7b75a57f4c12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Wed, 13 May 2026 16:28:44 +0200 Subject: [PATCH 11/12] fix failing env test --- internal/cli/cli_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 0203b40..df6e464 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -157,8 +157,8 @@ func TestRun_ImportErrors(t *testing.T) { { "missing file", "missing.xlsx", - "", - nil, + "test.env", + map[string]string{}, fmt.Errorf("failed to open excel extended file: open %s: no such file or directory", testDataFile("missing.xlsx")), []string{ "failed to open excel extended file", @@ -167,8 +167,8 @@ func TestRun_ImportErrors(t *testing.T) { { "non excel file", "invalid.txt", - "", - nil, + "test.env", + map[string]string{}, cli.ErrUnknownFileExtension, []string{ "unknown file extension .txt", From 543cdca6aafdc43e32e2827cecc091d044f7befd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristi=C3=A1n=20Kunc?= Date: Wed, 13 May 2026 16:42:19 +0200 Subject: [PATCH 12/12] remove duplicate notifications --- internal/cli/cli.go | 16 ---------------- internal/cli/cli_test.go | 15 ++++++--------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index c8549d8..dd498b4 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -230,14 +230,6 @@ func importProcess(ctx context.Context, filePath string, cycle string, envPath s return ErrCannotLoadDotenv } - err = discord.SendDiscordNotification(discord.DiscordNotificationData{ - WebhookURL: discord.LoadWebhookURL(), - Content: "Starting SRD import for AIRAC cycle " + cycle, - }) - if err != nil { - log.Error().Err(err).Msg("failed to send Discord notification") - } - file, err := loadSrdFile(path) if err != nil { return err @@ -378,14 +370,6 @@ func doDownload(ctx context.Context, force bool, forceCycle string, envPath stri return err } - err = discord.SendDiscordNotification(discord.DiscordNotificationData{ - WebhookURL: discord.LoadWebhookURL(), - Content: "SRD download complete for AIRAC cycle " + cycleToDownload.Ident, - }) - if err != nil { - log.Error().Err(err).Msg("failed to send Discord notification") - } - // Download happened, so now we do the import return importProcess(ctx, downloader.LatestFileLocation(), cycleToDownload.Ident, envPath, fileDir) } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index df6e464..886678a 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -378,10 +378,9 @@ func TestRun_ImportSuccess(t *testing.T) { // Run the CLI test require.NoError(cli.Run(testDir)) - // Check Discord webhook was called (start and complete notifications) - require.Equal(2, discordMock.callCount, "expected Discord webhook to be called twice (start and complete)") - require.Contains(discordMock.messages[0], "Starting SRD import for AIRAC cycle 2404") - require.Contains(discordMock.messages[1], "SRD import complete for AIRAC cycle 2404") + // Check Discord webhook was called (complete notification only) + require.Equal(1, discordMock.callCount, "expected Discord webhook to be called once (complete only)") + require.Contains(discordMock.messages[0], "SRD import complete for AIRAC cycle 2404") // Check the logs for _, msg := range tt.expectedLogMessages { @@ -748,12 +747,10 @@ func TestDownloadSuccess(t *testing.T) { // Run the CLI test require.NoError(cli.Run(testDir)) - // Check Discord webhook was called (download start, download complete, import start, import complete) - require.Equal(4, discordMock.callCount, "expected Discord webhook to be called four times (download start/complete and import start/complete)") + // Check Discord webhook was called (download start and import complete) + require.Equal(2, discordMock.callCount, "expected Discord webhook to be called twice (download start and import complete)") require.Contains(discordMock.messages[0], "Starting SRD download for AIRAC cycle") - require.Contains(discordMock.messages[1], "SRD download complete for AIRAC cycle") - require.Contains(discordMock.messages[2], "Starting SRD import for AIRAC cycle") - require.Contains(discordMock.messages[3], "SRD import complete for AIRAC cycle") + require.Contains(discordMock.messages[1], "SRD import complete for AIRAC cycle") // Check the logs for _, msg := range tt.expectedLogMessages {