From 89d039d62504b62c75274f1e82a004ceae5a2571 Mon Sep 17 00:00:00 2001 From: codens-agent Date: Sun, 28 Jun 2026 09:17:06 +0000 Subject: [PATCH 1/3] chore: Add LICENSE, Config branding fields, MetaHandler (/api/meta + /api/licenses) with unit tests --- LICENSE | 201 ++++++++++++++++++ backend/cmd/server/main.go | 27 +++ backend/internal/config/config.go | 8 + backend/internal/config/config_test.go | 21 ++ backend/internal/handler/meta_handler.go | 67 ++++++ backend/internal/handler/meta_handler_test.go | 144 +++++++++++++ backend/licenses.json | 1 + 7 files changed, 469 insertions(+) create mode 100644 LICENSE create mode 100644 backend/internal/handler/meta_handler.go create mode 100644 backend/internal/handler/meta_handler_test.go create mode 100644 backend/licenses.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..c4aa8c75 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 OpenGit Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 2e1eb904..f3f6dc3d 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -6,6 +6,7 @@ import ( "crypto/rsa" "crypto/x509" "database/sql" + "encoding/json" "encoding/pem" "errors" "fmt" @@ -562,6 +563,17 @@ func registerHandlers(e *echo.Echo, cfg config.Config, db *sql.DB) (*sshinfra.SS rateLimitHandler := handler.NewRateLimitHandler() rootHandler := handler.NewRootHandler() + thirdPartyLicenses := loadLicensesFromFile(cfg.LicensesFilePath) + metaHandler := handler.NewMetaHandler(handler.BuildInfo{ + AppName: cfg.AppName, + Version: version, + GitCommit: commit, + BuildDate: buildTime, + LicenseName: cfg.LicenseName, + SourceURL: cfg.SourceURL, + }, thirdPartyLicenses) + metaHandler.RegisterRoutes(e) + gitHTTPHandler := handler.NewGitHTTPHandler( cfg.GitDataRoot, repoGitResolver, @@ -669,6 +681,21 @@ func registerHandlers(e *echo.Echo, cfg config.Config, db *sql.DB) (*sshinfra.SS return sshServer, nil } +func loadLicensesFromFile(path string) []handler.LicenseEntry { + data, err := os.ReadFile(path) + if err != nil { + logger.Global().Warn().Err(err).Str("path", path).Msg("failed to load licenses file") + return []handler.LicenseEntry{} + } + + var entries []handler.LicenseEntry + if err := json.Unmarshal(data, &entries); err != nil { + logger.Global().Warn().Err(err).Str("path", path).Msg("failed to parse licenses file") + return []handler.LicenseEntry{} + } + return entries +} + type noopMCPEnqueuer struct{} func (noopMCPEnqueuer) EnqueueMCPVerification(context.Context, queue.MCPVerificationPayload) error { diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index f46bbc63..f661196e 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -39,6 +39,10 @@ type Config struct { TLSCertFile string TLSKeyFile string TrustedProxyCIDRs string + AppName string + LicenseName string + SourceURL string + LicensesFilePath string } func Load() Config { @@ -86,6 +90,10 @@ func Load() Config { TLSCertFile: os.Getenv("TLS_CERT_FILE"), TLSKeyFile: os.Getenv("TLS_KEY_FILE"), TrustedProxyCIDRs: os.Getenv("TRUSTED_PROXY_CIDRS"), + AppName: getenv("APP_NAME", "OpenGit"), + LicenseName: getenv("LICENSE_NAME", "Apache-2.0"), + SourceURL: getenv("SOURCE_URL", ""), + LicensesFilePath: getenv("LICENSES_FILE_PATH", "./licenses.json"), } } diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index 1c3a160d..8a0edd92 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -242,6 +242,27 @@ func TestMetricsConfig(t *testing.T) { }) } +func TestLoadBrandingDefaults(t *testing.T) { + t.Setenv("APP_NAME", "") + t.Setenv("LICENSE_NAME", "") + t.Setenv("SOURCE_URL", "") + t.Setenv("LICENSES_FILE_PATH", "") + + cfg := config.Load() + if cfg.AppName != "OpenGit" { + t.Fatalf("AppName = %q, want OpenGit", cfg.AppName) + } + if cfg.LicenseName != "Apache-2.0" { + t.Fatalf("LicenseName = %q, want Apache-2.0", cfg.LicenseName) + } + if cfg.SourceURL != "" { + t.Fatalf("SourceURL = %q, want empty", cfg.SourceURL) + } + if cfg.LicensesFilePath != "./licenses.json" { + t.Fatalf("LicensesFilePath = %q, want ./licenses.json", cfg.LicensesFilePath) + } +} + func TestMaskDSN(t *testing.T) { masked := database.MaskDSN("postgres://user:secret@host/db") if strings.Contains(masked, "secret") { diff --git a/backend/internal/handler/meta_handler.go b/backend/internal/handler/meta_handler.go new file mode 100644 index 00000000..60868d73 --- /dev/null +++ b/backend/internal/handler/meta_handler.go @@ -0,0 +1,67 @@ +package handler + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +type BuildInfo struct { + AppName string + Version string + GitCommit string + BuildDate string + LicenseName string + SourceURL string +} + +type LicenseEntry struct { + Name string `json:"name"` + Version string `json:"version"` + License string `json:"license"` + URL string `json:"url"` +} + +type MetaHandler struct { + info BuildInfo + thirdParty []LicenseEntry +} + +func NewMetaHandler(info BuildInfo, thirdParty []LicenseEntry) *MetaHandler { + entries := make([]LicenseEntry, 0) + if thirdParty != nil { + entries = append(entries, thirdParty...) + } + return &MetaHandler{ + info: info, + thirdParty: entries, + } +} + +func (h *MetaHandler) GetMeta(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{ + "app_name": h.info.AppName, + "version": h.info.Version, + "git_commit": h.info.GitCommit, + "build_date": h.info.BuildDate, + "license": h.info.LicenseName, + "source_url": h.info.SourceURL, + }) +} + +type licensesResponse struct { + AppLicense string `json:"app_license"` + ThirdParty []LicenseEntry `json:"third_party"` +} + +func (h *MetaHandler) GetLicenses(c echo.Context) error { + return c.JSON(http.StatusOK, licensesResponse{ + AppLicense: h.info.LicenseName, + ThirdParty: h.thirdParty, + }) +} + +func (h *MetaHandler) RegisterRoutes(e *echo.Echo) { + e.GET("/api/meta", h.GetMeta) + e.GET("/api/licenses", h.GetLicenses) +} diff --git a/backend/internal/handler/meta_handler_test.go b/backend/internal/handler/meta_handler_test.go new file mode 100644 index 00000000..e2f5208d --- /dev/null +++ b/backend/internal/handler/meta_handler_test.go @@ -0,0 +1,144 @@ +package handler_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo/v4" + + "github.com/open-git/backend/internal/handler" +) + +func TestGetMetaOK(t *testing.T) { + e := echo.New() + metaHandler := handler.NewMetaHandler(handler.BuildInfo{ + AppName: "OpenGit", + Version: "1.0.0", + GitCommit: "abc1234", + BuildDate: "2025-01-01T00:00:00Z", + LicenseName: "Apache-2.0", + SourceURL: "https://example.org/org/repo", + }, nil) + metaHandler.RegisterRoutes(e) + + req := httptest.NewRequest(http.MethodGet, "/api/meta", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var body map[string]string + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + for _, key := range []string{"app_name", "version", "git_commit", "build_date", "license", "source_url"} { + if _, ok := body[key]; !ok { + t.Fatalf("%s key missing", key) + } + } +} + +func TestGetLicensesWithEntries(t *testing.T) { + e := echo.New() + entry := handler.LicenseEntry{ + Name: "github.com/labstack/echo/v4", + Version: "v4.11.0", + License: "MIT", + URL: "https://github.com/labstack/echo", + } + metaHandler := handler.NewMetaHandler(handler.BuildInfo{ + LicenseName: "Apache-2.0", + }, []handler.LicenseEntry{entry}) + metaHandler.RegisterRoutes(e) + + req := httptest.NewRequest(http.MethodGet, "/api/licenses", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var body struct { + AppLicense string `json:"app_license"` + ThirdParty []handler.LicenseEntry `json:"third_party"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if body.AppLicense != "Apache-2.0" { + t.Fatalf("app_license = %q, want Apache-2.0", body.AppLicense) + } + if len(body.ThirdParty) != 1 { + t.Fatalf("third_party len = %d, want 1", len(body.ThirdParty)) + } + if body.ThirdParty[0].Name != entry.Name { + t.Fatalf("third_party[0].name = %q, want %q", body.ThirdParty[0].Name, entry.Name) + } +} + +func TestGetLicensesEmpty(t *testing.T) { + e := echo.New() + metaHandler := handler.NewMetaHandler(handler.BuildInfo{ + LicenseName: "Apache-2.0", + }, nil) + metaHandler.RegisterRoutes(e) + + req := httptest.NewRequest(http.MethodGet, "/api/licenses", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + raw := rec.Body.String() + if raw == "" { + t.Fatal("response body is empty") + } + if !json.Valid([]byte(raw)) { + t.Fatalf("response is not valid JSON: %s", raw) + } + + var body map[string]json.RawMessage + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + thirdParty, ok := body["third_party"] + if !ok { + t.Fatal("third_party key missing") + } + if string(thirdParty) != "[]" { + t.Fatalf("third_party = %s, want []", string(thirdParty)) + } +} + +func TestGetMetaDevVersion(t *testing.T) { + e := echo.New() + metaHandler := handler.NewMetaHandler(handler.BuildInfo{ + AppName: "OpenGit", + Version: "dev", + }, nil) + metaHandler.RegisterRoutes(e) + + req := httptest.NewRequest(http.MethodGet, "/api/meta", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var body map[string]string + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if body["version"] != "dev" { + t.Fatalf("version = %q, want dev", body["version"]) + } +} diff --git a/backend/licenses.json b/backend/licenses.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/backend/licenses.json @@ -0,0 +1 @@ +[] From f6c50bd260f33e6ea54e05bd26327b72529fe020 Mon Sep 17 00:00:00 2001 From: codens-agent Date: Sun, 28 Jun 2026 09:33:20 +0000 Subject: [PATCH 2/3] chore: Add LICENSE, Config branding fields, MetaHandler (/api/meta + /api/licenses) with unit tests --- .github/workflows/helm.yml | 2 +- .../repository/i_workflow_job_repository.go | 1 + .../infrastructure/git/git_service.go | 15 ++-- .../infrastructure/git/init_service.go | 20 +++--- .../queue/workflow_job_exec_handler.go | 44 ++++++++---- .../queue/workflow_job_exec_handler_test.go | 70 ++++++++++++++----- .../queue/workflow_schedule_handler.go | 13 +++- .../repository/workflow_job_repository.go | 19 +++++ .../internal/infrastructure/ssh/ssh_server.go | 14 ++-- .../infrastructure/workflow/parser.go | 20 +++--- .../usecase/workflow/get_run_usecase.go | 34 --------- .../workflow/trigger_workflow_usecase.go | 6 +- .../workflow/trigger_workflow_usecase_test.go | 37 +++++----- go.work.sum | 10 +++ 14 files changed, 185 insertions(+), 120 deletions(-) diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 4c042f61..2d21d214 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -28,4 +28,4 @@ jobs: cat /tmp/rendered.yaml | docker run --rm -i ghcr.io/yannh/kubeconform:latest -strict -ignore-missing-schemas - - name: Dry-run install - run: helm install gitforge helm/ --dry-run --generate-name --set ingress.host=test.example.com + run: helm install gitforge helm/ --dry-run=client --set ingress.host=test.example.com diff --git a/backend/internal/domain/repository/i_workflow_job_repository.go b/backend/internal/domain/repository/i_workflow_job_repository.go index d458ff65..f32ea63f 100644 --- a/backend/internal/domain/repository/i_workflow_job_repository.go +++ b/backend/internal/domain/repository/i_workflow_job_repository.go @@ -16,4 +16,5 @@ type IWorkflowJobRepository interface { Complete(ctx context.Context, jobID uuid.UUID, conclusion string, finishedAt time.Time) error Cancel(ctx context.Context, jobID uuid.UUID) error ListQueued(ctx context.Context, orgID uuid.UUID) ([]*entity.WorkflowJob, error) + ListByRunID(ctx context.Context, orgID uuid.UUID, runID uuid.UUID) ([]*entity.WorkflowJob, error) } diff --git a/backend/internal/infrastructure/git/git_service.go b/backend/internal/infrastructure/git/git_service.go index 1cf43a6b..fa480e64 100644 --- a/backend/internal/infrastructure/git/git_service.go +++ b/backend/internal/infrastructure/git/git_service.go @@ -18,6 +18,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/protocol/packp" + "github.com/go-git/go-git/v5/plumbing/format/diff" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/server" domainservice "github.com/open-git/backend/internal/domain/service" @@ -73,7 +74,7 @@ func InitBare(path string) error { return err } -func repoServer(repoPath string) (*server.Server, *transport.Endpoint, error) { +func repoServer(repoPath string) (transport.Transport, *transport.Endpoint, error) { abs, err := filepath.Abs(repoPath) if err != nil { return nil, nil, err @@ -492,7 +493,7 @@ func GetDiff(repoPath, baseRef, headRef string) ([]FileDiff, error) { return nil, err } - changes, err := object.DiffTree(context.Background(), baseTree, headTree) + changes, err := object.DiffTree(baseTree, headTree) if err != nil { return nil, err } @@ -519,18 +520,18 @@ func GetDiff(repoPath, baseRef, headRef string) ([]FileDiff, error) { var patchContent strings.Builder for _, chunk := range fp.Chunks() { switch chunk.Type() { - case object.Add: + case diff.Add: if status == "" { status = "add" } - case object.Delete: + case diff.Delete: status = "delete" - case object.Modify: - if status != "delete" { + default: + if chunk.Type() != diff.Equal && status != "delete" { status = "modify" } } - if chunk.Type() != object.Equal { + if chunk.Type() != diff.Equal { patchContent.WriteString(chunk.Content()) } } diff --git a/backend/internal/infrastructure/git/init_service.go b/backend/internal/infrastructure/git/init_service.go index 68c2725c..800b5d33 100644 --- a/backend/internal/infrastructure/git/init_service.go +++ b/backend/internal/infrastructure/git/init_service.go @@ -49,13 +49,22 @@ func AutoInitRepository(bareRepoPath string, opts AutoInitOpts) error { } defer os.RemoveAll(tmpDir) - repo, err := gogit.PlainInitWithOptions(tmpDir, &gogit.PlainInitOptions{ - InitBranch: plumbing.NewBranchReferenceName("main").Short(), - }) + repo, err := gogit.PlainInit(tmpDir, false) if err != nil { return fmt.Errorf("init working repo: %w", err) } + wt, err := repo.Worktree() + if err != nil { + return fmt.Errorf("open worktree: %w", err) + } + if err := wt.Checkout(&gogit.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName("main"), + Create: true, + }); err != nil { + return fmt.Errorf("create main branch: %w", err) + } + wroteFiles := false if opts.Readme != "" { if err := os.WriteFile(filepath.Join(tmpDir, "README.md"), []byte(readmeContent(opts.Readme)), 0o644); err != nil { @@ -76,11 +85,6 @@ func AutoInitRepository(bareRepoPath string, opts AutoInitOpts) error { wroteFiles = true } - wt, err := repo.Worktree() - if err != nil { - return fmt.Errorf("open worktree: %w", err) - } - if wroteFiles { if _, err := wt.Add("."); err != nil { return fmt.Errorf("stage files: %w", err) diff --git a/backend/internal/infrastructure/queue/workflow_job_exec_handler.go b/backend/internal/infrastructure/queue/workflow_job_exec_handler.go index fd9c5def..e1886595 100644 --- a/backend/internal/infrastructure/queue/workflow_job_exec_handler.go +++ b/backend/internal/infrastructure/queue/workflow_job_exec_handler.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/open-git/backend/internal/domain/entity" @@ -16,10 +17,7 @@ import ( "github.com/open-git/backend/internal/infrastructure/runner" ) -const ( - defaultJobTimeoutMinutes = 360 - conclusionTimedOut = "timed_out" -) +const defaultJobTimeoutMinutes = 360 type workflowStepRepository interface { ListByJobID(ctx context.Context, orgID, jobID string) ([]*runner.Step, error) @@ -96,17 +94,30 @@ func (h *WorkflowJobExecHandler) HandleWorkflowJobExec(ctx context.Context, task return fmt.Errorf("workflow job exec payload missing identifiers: %w", asynq.SkipRetry) } - job, err := h.jobRepo.GetByID(ctx, payload.OrgID, payload.JobID) + orgUUID, err := uuid.Parse(payload.OrgID) + if err != nil { + return fmt.Errorf("parse org id: %v: %w", err, asynq.SkipRetry) + } + runUUID, err := uuid.Parse(payload.RunID) + if err != nil { + return fmt.Errorf("parse run id: %v: %w", err, asynq.SkipRetry) + } + jobUUID, err := uuid.Parse(payload.JobID) + if err != nil { + return fmt.Errorf("parse job id: %v: %w", err, asynq.SkipRetry) + } + + job, err := h.jobRepo.GetByID(ctx, jobUUID) if err != nil { return fmt.Errorf("load workflow job: %w", err) } - if job.OrganizationID != payload.OrgID { + if job.OrganizationID != orgUUID { return fmt.Errorf("job organization mismatch: %w", asynq.SkipRetry) } - if job.RepositoryID == "" { + if job.RepositoryID == uuid.Nil { return fmt.Errorf("job repository id missing: %w", asynq.SkipRetry) } - if job.WorkflowRunID != payload.RunID { + if job.WorkflowRunID == nil || *job.WorkflowRunID != runUUID { return fmt.Errorf("job run mismatch: %w", asynq.SkipRetry) } @@ -115,7 +126,7 @@ func (h *WorkflowJobExecHandler) HandleWorkflowJobExec(ctx context.Context, task return fmt.Errorf("load workflow steps: %w", err) } - if err := h.jobRepo.UpdateStatus(ctx, payload.JobID, entity.WorkflowJobStatusInProgress, "", nil); err != nil { + if err := h.jobRepo.UpdateStatus(ctx, jobUUID, entity.WorkflowJobStatusInProgress, ""); err != nil { return fmt.Errorf("set job in_progress: %w", err) } @@ -125,7 +136,7 @@ func (h *WorkflowJobExecHandler) HandleWorkflowJobExec(ctx context.Context, task secretMap := map[string]string{} if h.secrets != nil { - secretMap, err = h.secrets.GetSecrets(ctx, payload.OrgID, job.RepositoryID) + secretMap, err = h.secrets.GetSecrets(ctx, payload.OrgID, job.RepositoryID.String()) if err != nil { return fmt.Errorf("load secrets: %w", err) } @@ -149,8 +160,15 @@ func (h *WorkflowJobExecHandler) HandleWorkflowJobExec(ctx context.Context, task } completedAt := time.Now().UTC() - if err := h.jobRepo.UpdateStatus(ctx, payload.JobID, status, conclusion, &completedAt); err != nil { - return fmt.Errorf("update job terminal status: %w", err) + switch status { + case entity.WorkflowJobStatusCompleted: + if err := h.jobRepo.Complete(ctx, jobUUID, conclusion, completedAt); err != nil { + return fmt.Errorf("update job terminal status: %w", err) + } + default: + if err := h.jobRepo.UpdateStatus(ctx, jobUUID, status, conclusion); err != nil { + return fmt.Errorf("update job terminal status: %w", err) + } } if err := h.enqueuer.EnqueueSchedule(ctx, WorkflowSchedulePayload{ @@ -178,7 +196,7 @@ func (h *WorkflowJobExecHandler) buildLogCallback( } line := &entity.JobLogLine{ OrganizationID: payload.OrgID, - RepositoryID: job.RepositoryID, + RepositoryID: job.RepositoryID.String(), RunID: payload.RunID, JobID: payload.JobID, LineNumber: offset, diff --git a/backend/internal/infrastructure/queue/workflow_job_exec_handler_test.go b/backend/internal/infrastructure/queue/workflow_job_exec_handler_test.go index c8846e8a..2c9ccdd0 100644 --- a/backend/internal/infrastructure/queue/workflow_job_exec_handler_test.go +++ b/backend/internal/infrastructure/queue/workflow_job_exec_handler_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/open-git/backend/internal/domain/entity" @@ -20,16 +21,27 @@ type mockExecJobRepo struct { statusUpdates []string } +var ( + testExecOrgID = uuid.MustParse("00000000-0000-0000-0000-000000000001") + testExecRunID = uuid.MustParse("00000000-0000-0000-0000-000000000002") + testExecJobID = uuid.MustParse("00000000-0000-0000-0000-000000000003") + testExecRepoID = uuid.MustParse("00000000-0000-0000-0000-000000000004") +) + func (m *mockExecJobRepo) Create(context.Context, *entity.WorkflowJob) error { return nil } -func (m *mockExecJobRepo) GetByID(_ context.Context, _, jobID string) (*entity.WorkflowJob, error) { +func (m *mockExecJobRepo) GetByID(_ context.Context, jobID uuid.UUID) (*entity.WorkflowJob, error) { if m.job == nil || m.job.ID != jobID { return nil, errors.New("not found") } return m.job, nil } -func (m *mockExecJobRepo) UpdateStatus(_ context.Context, jobID, status, conclusion string, _ *time.Time) error { +func (m *mockExecJobRepo) AcquireForRunner(context.Context, uuid.UUID, uuid.UUID, int) (bool, error) { + return false, nil +} + +func (m *mockExecJobRepo) UpdateStatus(_ context.Context, jobID uuid.UUID, status, conclusion string) error { if m.job != nil && m.job.ID == jobID { m.job.Status = status m.job.Conclusion = conclusion @@ -38,7 +50,23 @@ func (m *mockExecJobRepo) UpdateStatus(_ context.Context, jobID, status, conclus return nil } -func (m *mockExecJobRepo) ListByRunID(context.Context, string, string) ([]*entity.WorkflowJob, error) { +func (m *mockExecJobRepo) Complete(_ context.Context, jobID uuid.UUID, conclusion string, finishedAt time.Time) error { + if m.job != nil && m.job.ID == jobID { + m.job.Status = entity.WorkflowJobStatusCompleted + m.job.Conclusion = conclusion + m.job.FinishedAt = &finishedAt + } + m.statusUpdates = append(m.statusUpdates, entity.WorkflowJobStatusCompleted) + return nil +} + +func (m *mockExecJobRepo) Cancel(context.Context, uuid.UUID) error { return nil } + +func (m *mockExecJobRepo) ListQueued(context.Context, uuid.UUID) ([]*entity.WorkflowJob, error) { + return nil, nil +} + +func (m *mockExecJobRepo) ListByRunID(context.Context, uuid.UUID, uuid.UUID) ([]*entity.WorkflowJob, error) { return nil, nil } @@ -125,8 +153,12 @@ func TestJobTimeoutMinutes_UsesReflectionField(t *testing.T) { func TestBuildLogCallback_ConcurrentAppendIsSafe(t *testing.T) { logRepo := &mockExecLogRepo{} handler := &WorkflowJobExecHandler{logRepo: logRepo} - payload := WorkflowJobExecPayload{OrgID: "org-1", RunID: "run-1", JobID: "job-1"} - job := &entity.WorkflowJob{RepositoryID: "repo-1"} + payload := WorkflowJobExecPayload{ + OrgID: testExecOrgID.String(), + RunID: testExecRunID.String(), + JobID: testExecJobID.String(), + } + job := &entity.WorkflowJob{RepositoryID: testExecRepoID} logFn, logErrFn := handler.buildLogCallback(context.Background(), payload, job) @@ -151,10 +183,10 @@ func TestBuildLogCallback_ConcurrentAppendIsSafe(t *testing.T) { func TestWorkflowJobExecHandler_RejectsRunMismatch(t *testing.T) { jobRepo := &mockExecJobRepo{ job: &entity.WorkflowJob{ - ID: "job-1", - WorkflowRunID: "run-1", - OrganizationID: "org-1", - RepositoryID: "repo-1", + ID: testExecJobID, + WorkflowRunID: &testExecRunID, + OrganizationID: testExecOrgID, + RepositoryID: testExecRepoID, }, } handler := NewWorkflowJobExecHandlerWithEnqueuer( @@ -167,9 +199,9 @@ func TestWorkflowJobExecHandler_RejectsRunMismatch(t *testing.T) { ) task, err := newJobExecTask(WorkflowJobExecPayload{ - JobID: "job-1", - RunID: "run-other", - OrgID: "org-1", + JobID: testExecJobID.String(), + RunID: uuid.MustParse("00000000-0000-0000-0000-000000000099").String(), + OrgID: testExecOrgID.String(), }) if err != nil { t.Fatalf("newJobExecTask: %v", err) @@ -184,10 +216,10 @@ func TestWorkflowJobExecHandler_RejectsRunMismatch(t *testing.T) { func TestWorkflowJobExecHandler_MarksTimedOutJobCompleted(t *testing.T) { jobRepo := &mockExecJobRepo{ job: &entity.WorkflowJob{ - ID: "job-1", - WorkflowRunID: "run-1", - OrganizationID: "org-1", - RepositoryID: "repo-1", + ID: testExecJobID, + WorkflowRunID: &testExecRunID, + OrganizationID: testExecOrgID, + RepositoryID: testExecRepoID, }, } handler := NewWorkflowJobExecHandlerWithEnqueuer( @@ -200,9 +232,9 @@ func TestWorkflowJobExecHandler_MarksTimedOutJobCompleted(t *testing.T) { ) task, err := newJobExecTask(WorkflowJobExecPayload{ - JobID: "job-1", - RunID: "run-1", - OrgID: "org-1", + JobID: testExecJobID.String(), + RunID: testExecRunID.String(), + OrgID: testExecOrgID.String(), }) if err != nil { t.Fatalf("newJobExecTask: %v", err) diff --git a/backend/internal/infrastructure/queue/workflow_schedule_handler.go b/backend/internal/infrastructure/queue/workflow_schedule_handler.go index 77e24028..c50cab42 100644 --- a/backend/internal/infrastructure/queue/workflow_schedule_handler.go +++ b/backend/internal/infrastructure/queue/workflow_schedule_handler.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" + "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/open-git/backend/internal/domain/entity" @@ -78,14 +79,22 @@ type domainJobRepoAdapter struct { } func (a *domainJobRepoAdapter) ListByRunID(ctx context.Context, orgID, runID string) ([]*schedulableJob, error) { - jobs, err := a.repo.ListByRunID(ctx, orgID, runID) + orgUUID, err := uuid.Parse(orgID) + if err != nil { + return nil, fmt.Errorf("parse org id: %w", err) + } + runUUID, err := uuid.Parse(runID) + if err != nil { + return nil, fmt.Errorf("parse run id: %w", err) + } + jobs, err := a.repo.ListByRunID(ctx, orgUUID, runUUID) if err != nil { return nil, err } out := make([]*schedulableJob, len(jobs)) for i, job := range jobs { out[i] = &schedulableJob{ - ID: job.ID, + ID: job.ID.String(), Name: job.Name, Status: job.Status, Conclusion: job.Conclusion, diff --git a/backend/internal/infrastructure/repository/workflow_job_repository.go b/backend/internal/infrastructure/repository/workflow_job_repository.go index f1663330..66e8e27d 100644 --- a/backend/internal/infrastructure/repository/workflow_job_repository.go +++ b/backend/internal/infrastructure/repository/workflow_job_repository.go @@ -204,6 +204,25 @@ func (r *sqlxWorkflowJobRepository) ListQueued(ctx context.Context, orgID uuid.U return scanWorkflowJobRows(rows) } +func (r *sqlxWorkflowJobRepository) ListByRunID(ctx context.Context, orgID uuid.UUID, runID uuid.UUID) ([]*entity.WorkflowJob, error) { + const query = ` + SELECT id, workflow_run_id, organization_id, repository_id, name, status, conclusion, + assigned_runner_id, runs_on, acquire_lock_version, started_at, finished_at, + timeout_minutes, created_at + FROM workflow_jobs + WHERE organization_id = ? AND workflow_run_id = ? + ORDER BY created_at ASC + ` + q := r.db.Rebind(query) + rows, err := r.db.QueryxContext(ctx, q, orgID, runID) + if err != nil { + return nil, dbErrors.MapDBError(err) + } + defer rows.Close() + + return scanWorkflowJobRows(rows) +} + func workflowJobRunID(job *entity.WorkflowJob) uuid.UUID { if job.WorkflowRunID != nil && *job.WorkflowRunID != uuid.Nil { return *job.WorkflowRunID diff --git a/backend/internal/infrastructure/ssh/ssh_server.go b/backend/internal/infrastructure/ssh/ssh_server.go index 0ef32c91..cdafdaad 100644 --- a/backend/internal/infrastructure/ssh/ssh_server.go +++ b/backend/internal/infrastructure/ssh/ssh_server.go @@ -11,6 +11,7 @@ import ( gossh "github.com/gliderlabs/ssh" "github.com/google/uuid" + xssh "golang.org/x/crypto/ssh" infragit "github.com/open-git/backend/internal/infrastructure/git" "github.com/open-git/backend/internal/repository" @@ -108,16 +109,13 @@ func (h *SSHServer) Close() error { return h.server.Close() } -func (h *SSHServer) authenticateKey(ctx gossh.Context, key gossh.PublicKey) (*gossh.Permissions, error) { - fingerprint := gossh.FingerprintSHA256(key) +func (h *SSHServer) authenticateKey(ctx gossh.Context, key gossh.PublicKey) bool { + fingerprint := xssh.FingerprintSHA256(key) stored, err := h.keyStore.FindByFingerprint(ctx, fingerprint) - if err != nil { - return nil, fmt.Errorf("lookup ssh key: %w", err) - } - if stored == nil { - return nil, gossh.ErrKeyRejected + if err != nil || stored == nil { + return false } - return nil, nil + return true } func (h *SSHServer) handleSession(s gossh.Session) { diff --git a/backend/internal/infrastructure/workflow/parser.go b/backend/internal/infrastructure/workflow/parser.go index 3ac3ab36..06da4645 100644 --- a/backend/internal/infrastructure/workflow/parser.go +++ b/backend/internal/infrastructure/workflow/parser.go @@ -186,7 +186,7 @@ func validateWorkflowDocument(doc *yaml.Node) []Diagnostic { if onNode == nil { diags = append(diags, Diagnostic{ Line: doc.Line, - Col: doc.Col, + Col: doc.Column, Severity: "error", Message: "workflow must define trigger 'on'", }) @@ -194,7 +194,7 @@ func validateWorkflowDocument(doc *yaml.Node) []Diagnostic { if jobsNode == nil { diags = append(diags, Diagnostic{ Line: doc.Line, - Col: doc.Col, + Col: doc.Column, Severity: "error", Message: "workflow must contain at least one job", }) @@ -231,7 +231,7 @@ func ParseWorkflowFull(data []byte) (*WorkflowIR, []Diagnostic, error) { return nil, diags, nil } if doc.Kind != yaml.MappingNode { - diags = append(diags, Diagnostic{Line: doc.Line, Col: doc.Col, Severity: "error", Message: "workflow root must be a mapping"}) + diags = append(diags, Diagnostic{Line: doc.Line, Col: doc.Column, Severity: "error", Message: "workflow root must be a mapping"}) return nil, diags, nil } @@ -252,10 +252,10 @@ func ParseWorkflowFull(data []byte) (*WorkflowIR, []Diagnostic, error) { } if onNode == nil { - diags = append(diags, Diagnostic{Line: doc.Line, Col: doc.Col, Severity: "error", Message: "workflow must define trigger 'on'"}) + diags = append(diags, Diagnostic{Line: doc.Line, Col: doc.Column, Severity: "error", Message: "workflow must define trigger 'on'"}) } if jobsNode == nil { - diags = append(diags, Diagnostic{Line: doc.Line, Col: doc.Col, Severity: "error", Message: "workflow must contain at least one job"}) + diags = append(diags, Diagnostic{Line: doc.Line, Col: doc.Column, Severity: "error", Message: "workflow must contain at least one job"}) return nil, diags, nil } @@ -266,11 +266,11 @@ func ParseWorkflowFull(data []byte) (*WorkflowIR, []Diagnostic, error) { var rawJobs map[string]Job if err := jobsNode.Decode(&rawJobs); err != nil { - diags = append(diags, Diagnostic{Line: jobsNode.Line, Col: jobsNode.Col, Severity: "error", Message: err.Error()}) + diags = append(diags, Diagnostic{Line: jobsNode.Line, Col: jobsNode.Column, Severity: "error", Message: err.Error()}) return nil, diags, nil } if len(rawJobs) == 0 { - diags = append(diags, Diagnostic{Line: jobsNode.Line, Col: jobsNode.Col, Severity: "error", Message: "workflow must contain at least one job"}) + diags = append(diags, Diagnostic{Line: jobsNode.Line, Col: jobsNode.Column, Severity: "error", Message: "workflow must contain at least one job"}) return nil, diags, nil } @@ -422,7 +422,7 @@ func expandMatrix(node yaml.Node) ([]map[string]any, []Diagnostic) { var diags []Diagnostic var raw map[string]any if err := node.Decode(&raw); err != nil { - diags = append(diags, Diagnostic{Line: node.Line, Col: node.Col, Severity: "error", Message: err.Error()}) + diags = append(diags, Diagnostic{Line: node.Line, Col: node.Column, Severity: "error", Message: err.Error()}) return nil, diags } @@ -449,7 +449,7 @@ func expandMatrix(node yaml.Node) ([]map[string]any, []Diagnostic) { if overflow { diags = append(diags, Diagnostic{ Line: node.Line, - Col: node.Col, + Col: node.Column, Severity: "error", Message: fmt.Sprintf("matrix expansion exceeds maximum of %d combinations", maxMatrixCombinations), }) @@ -461,7 +461,7 @@ func expandMatrix(node yaml.Node) ([]map[string]any, []Diagnostic) { if len(combos) > maxMatrixCombinations { diags = append(diags, Diagnostic{ Line: node.Line, - Col: node.Col, + Col: node.Column, Severity: "error", Message: fmt.Sprintf("matrix expansion exceeds maximum of %d combinations (got %d)", maxMatrixCombinations, len(combos)), }) diff --git a/backend/internal/usecase/workflow/get_run_usecase.go b/backend/internal/usecase/workflow/get_run_usecase.go index 2cc8e83c..a4d4a700 100644 --- a/backend/internal/usecase/workflow/get_run_usecase.go +++ b/backend/internal/usecase/workflow/get_run_usecase.go @@ -31,40 +31,6 @@ func (uc *GetRunUsecase) Execute(ctx context.Context, input GetRunInput) (*entit return uc.runRepo.GetByID(ctx, input.OrganizationID, input.RepositoryID, input.RunID) } -type CancelRunInput struct { - OrganizationID uuid.UUID - RepositoryID uuid.UUID - RunID uuid.UUID - ActorID uuid.UUID -} - -type cancelRunRepository interface { - GetByID(ctx context.Context, orgID, repoID, runID uuid.UUID) (*entity.WorkflowRun, error) - Cancel(ctx context.Context, orgID, repoID, runID, actorID uuid.UUID) error -} - -type CancelRunUsecase struct { - runRepo cancelRunRepository -} - -func NewCancelRunUsecase(runRepo cancelRunRepository) *CancelRunUsecase { - return &CancelRunUsecase{runRepo: runRepo} -} - -func (uc *CancelRunUsecase) Execute(ctx context.Context, input CancelRunInput) error { - run, err := uc.runRepo.GetByID(ctx, input.OrganizationID, input.RepositoryID, input.RunID) - if err != nil { - return err - } - if run == nil { - return apperror.ErrNotFound - } - if isTerminalRun(run) { - return domain.ErrConflict - } - return uc.runRepo.Cancel(ctx, input.OrganizationID, input.RepositoryID, input.RunID, input.ActorID) -} - type RerunRunInput struct { OrganizationID uuid.UUID RepositoryID uuid.UUID diff --git a/backend/internal/usecase/workflow/trigger_workflow_usecase.go b/backend/internal/usecase/workflow/trigger_workflow_usecase.go index adfd7875..3cd42317 100644 --- a/backend/internal/usecase/workflow/trigger_workflow_usecase.go +++ b/backend/internal/usecase/workflow/trigger_workflow_usecase.go @@ -344,8 +344,10 @@ func (uc *TriggerWorkflowUsecase) createQueuedRun( } if len(jobs) > 0 { - if err := uc.jobRepo.CreateBatch(ctx, jobs); err != nil { - return uuid.Nil, err + for _, job := range jobs { + if err := uc.jobRepo.Create(ctx, job); err != nil { + return uuid.Nil, err + } } } if len(steps) > 0 { diff --git a/backend/internal/usecase/workflow/trigger_workflow_usecase_test.go b/backend/internal/usecase/workflow/trigger_workflow_usecase_test.go index c8de5e62..004a8cb1 100644 --- a/backend/internal/usecase/workflow/trigger_workflow_usecase_test.go +++ b/backend/internal/usecase/workflow/trigger_workflow_usecase_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/google/uuid" @@ -62,39 +63,43 @@ func (m *mockWorkflowRunRepo) IncrementRunAttempt(context.Context, uuid.UUID, uu var _ domainrepo.IWorkflowRunRepository = (*mockWorkflowRunRepo)(nil) type mockWorkflowJobRepo struct { - batches [][]*entity.WorkflowJob + created []*entity.WorkflowJob } -func (m *mockWorkflowJobRepo) Create(context.Context, *entity.WorkflowJob) error { +func (m *mockWorkflowJobRepo) Create(_ context.Context, job *entity.WorkflowJob) error { + copyJob := *job + m.created = append(m.created, ©Job) return nil } -func (m *mockWorkflowJobRepo) CreateBatch(_ context.Context, jobs []*entity.WorkflowJob) error { - copied := make([]*entity.WorkflowJob, len(jobs)) - for i, job := range jobs { - copyJob := *job - copied[i] = ©Job - } - m.batches = append(m.batches, copied) - return nil +func (m *mockWorkflowJobRepo) GetByID(context.Context, uuid.UUID) (*entity.WorkflowJob, error) { + return nil, domain.ErrNotFound } -func (m *mockWorkflowJobRepo) ListByRunID(context.Context, uuid.UUID, uuid.UUID) ([]*entity.WorkflowJob, error) { - return nil, nil +func (m *mockWorkflowJobRepo) AcquireForRunner(context.Context, uuid.UUID, uuid.UUID, int) (bool, error) { + return false, nil } -func (m *mockWorkflowJobRepo) Update(context.Context, *entity.WorkflowJob) error { +func (m *mockWorkflowJobRepo) UpdateStatus(context.Context, uuid.UUID, string, string) error { return nil } -func (m *mockWorkflowJobRepo) CancelInProgressByRunID(context.Context, uuid.UUID, uuid.UUID) error { +func (m *mockWorkflowJobRepo) Complete(context.Context, uuid.UUID, string, time.Time) error { return nil } -func (m *mockWorkflowJobRepo) ResetQueuedByRunID(context.Context, uuid.UUID) error { +func (m *mockWorkflowJobRepo) Cancel(context.Context, uuid.UUID) error { return nil } +func (m *mockWorkflowJobRepo) ListQueued(context.Context, uuid.UUID) ([]*entity.WorkflowJob, error) { + return nil, nil +} + +func (m *mockWorkflowJobRepo) ListByRunID(context.Context, uuid.UUID, uuid.UUID) ([]*entity.WorkflowJob, error) { + return nil, nil +} + var _ domainrepo.IWorkflowJobRepository = (*mockWorkflowJobRepo)(nil) type mockWorkflowStepRepo struct { @@ -236,7 +241,7 @@ func TestTrigger_PushMatchesBranchFilter_CreatesRun(t *testing.T) { if enqueuer.payloads[0].RunID != out.RunID.String() { t.Fatalf("expected enqueued run ID %s, got %s", out.RunID, enqueuer.payloads[0].RunID) } - if len(jobRepo.batches) != 1 || len(jobRepo.batches[0]) != 1 { + if len(jobRepo.created) != 1 { t.Fatalf("expected 1 workflow job to be created") } } diff --git a/go.work.sum b/go.work.sum index 38d4b008..164368a6 100644 --- a/go.work.sum +++ b/go.work.sum @@ -90,6 +90,8 @@ github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFA github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= @@ -118,6 +120,8 @@ github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 h1:N/MD/sr6o61X+iZBAT2 github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= @@ -194,6 +198,9 @@ github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMAS github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/matryer/moq v0.4.0 h1:HsZIdEsj8+9nE940WW7FFxMgrgSxGfMkNXhVTHUhfMU= github.com/matryer/moq v0.4.0/go.mod h1:kUfalaLk7TcyXhrhonBYQ2Ewun63+/xGbZ7/MzzzC4Y= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU= github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= @@ -226,6 +233,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6O github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79 h1:V7x0hCAgL8lNGezuex1RW1sh7VXXCqfw8nXZti66iFg= github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -269,6 +277,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= From 2402858b49f2a459368d439a258790ecad986388 Mon Sep 17 00:00:00 2001 From: zoetaka38 Date: Mon, 29 Jun 2026 20:34:33 +0200 Subject: [PATCH 3/3] fix: address build error in license loader logger call after merge --- backend/cmd/server/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 5f8341ca..0679acaa 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -936,15 +936,16 @@ func registerHandlers(e *echo.Echo, cfg config.Config, db *sql.DB) (*sshinfra.SS } func loadLicensesFromFile(path string) []handler.LicenseEntry { + log := logger.Global() data, err := os.ReadFile(path) if err != nil { - logger.Global().Warn().Err(err).Str("path", path).Msg("failed to load licenses file") + log.Warn().Err(err).Str("path", path).Msg("failed to load licenses file") return []handler.LicenseEntry{} } var entries []handler.LicenseEntry if err := json.Unmarshal(data, &entries); err != nil { - logger.Global().Warn().Err(err).Str("path", path).Msg("failed to parse licenses file") + log.Warn().Err(err).Str("path", path).Msg("failed to parse licenses file") return []handler.LicenseEntry{} } return entries