diff --git a/internal/services/gateway/action/badge.go b/internal/services/gateway/action/badge.go
new file mode 100644
index 0000000..3386b47
--- /dev/null
+++ b/internal/services/gateway/action/badge.go
@@ -0,0 +1,81 @@
+// Copyright 2019 Sorint.lab
+//
+// 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.
+
+package action
+
+import (
+ "context"
+ "net/url"
+ "path"
+
+ "github.com/sorintlab/agola/internal/services/gateway/common"
+ rstypes "github.com/sorintlab/agola/internal/services/runservice/types"
+)
+
+// GetBadge return a badge for a project branch
+// TODO(sgotti) also handle tags and PRs
+func (h *ActionHandler) GetBadge(ctx context.Context, projectRef, branch string) (string, error) {
+ project, resp, err := h.configstoreClient.GetProject(ctx, projectRef)
+ if err != nil {
+ return "", ErrFromRemote(resp, err)
+ }
+
+ // if branch is empty we get the latest run for every branch.
+ group := path.Join("/", string(common.GroupTypeProject), project.ID, string(common.GroupTypeBranch), url.PathEscape(branch))
+ runResp, resp, err := h.runserviceClient.GetGroupLastRun(ctx, group, nil)
+ if err != nil {
+ return "", ErrFromRemote(resp, err)
+ }
+ if len(runResp.Runs) == 0 {
+ return badgeUnknown, nil
+ }
+ run := runResp.Runs[0]
+
+ var badge string
+ switch run.Result {
+ case rstypes.RunResultUnknown:
+ switch run.Phase {
+ case rstypes.RunPhaseSetupError:
+ badge = badgeError
+ case rstypes.RunPhaseQueued:
+ badge = badgeInProgress
+ case rstypes.RunPhaseRunning:
+ badge = badgeInProgress
+ case rstypes.RunPhaseCancelled:
+ badge = badgeFailed
+ }
+ case rstypes.RunResultSuccess:
+ badge = badgeSuccess
+ case rstypes.RunResultFailed:
+ badge = badgeFailed
+ case rstypes.RunResultStopped:
+ badge = badgeFailed
+ }
+
+ return badge, nil
+}
+
+// svg images generated from shields.io
+const (
+ // https://img.shields.io/badge/run-unknown-inactive.svg
+ badgeUnknown = ``
+ // https://img.shields.io/badge/run-success-success.svg
+ badgeSuccess = ``
+ // https://img.shields.io/badge/run-failed-critical.svg
+ badgeFailed = ``
+ // https://img.shields.io/badge/run-inprogress-informational.svg
+ badgeInProgress = ``
+ // https://img.shields.io/badge/run-error-yellow.svg
+ badgeError = ``
+)
diff --git a/internal/services/gateway/api/badge.go b/internal/services/gateway/api/badge.go
new file mode 100644
index 0000000..c99bdc0
--- /dev/null
+++ b/internal/services/gateway/api/badge.go
@@ -0,0 +1,65 @@
+// Copyright 2019 Sorint.lab
+//
+// 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.
+
+package api
+
+import (
+ "net/http"
+ "net/url"
+
+ "github.com/gorilla/mux"
+ "github.com/sorintlab/agola/internal/services/gateway/action"
+ "github.com/sorintlab/agola/internal/util"
+ "go.uber.org/zap"
+)
+
+type BadgeRequest struct {
+ Name string `json:"name"`
+}
+
+type BadgeHandler struct {
+ log *zap.SugaredLogger
+ ah *action.ActionHandler
+}
+
+func NewBadgeHandler(logger *zap.Logger, ah *action.ActionHandler) *BadgeHandler {
+ return &BadgeHandler{log: logger.Sugar(), ah: ah}
+}
+
+func (h *BadgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ vars := mux.Vars(r)
+ query := r.URL.Query()
+
+ projectRef, err := url.PathUnescape(vars["projectref"])
+ if err != nil {
+ httpError(w, util.NewErrBadRequest(err))
+ return
+ }
+ branch := query.Get("branch")
+
+ badge, err := h.ah.GetBadge(ctx, projectRef, branch)
+ if httpError(w, err) {
+ h.log.Errorf("err: %+v", err)
+ return
+ }
+
+ // TODO(sgotti) return some caching headers
+ w.Header().Set("Content-Type", "image/svg+xml")
+ w.Header().Set("Cache-Control", "no-cache")
+
+ if _, err := w.Write([]byte(badge)); err != nil {
+ h.log.Errorf("err: %+v", err)
+ }
+}
diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go
index 4e311ac..5c4115f 100644
--- a/internal/services/gateway/gateway.go
+++ b/internal/services/gateway/gateway.go
@@ -200,9 +200,12 @@ func (g *Gateway) Run(ctx context.Context) error {
logsHandler := api.NewLogsHandler(logger, g.ah)
- reposHandler := api.NewReposHandler(logger, g.c.GitserverURL)
userRemoteReposHandler := api.NewUserRemoteReposHandler(logger, g.ah, g.configstoreClient)
+ badgeHandler := api.NewBadgeHandler(logger, g.ah)
+
+ reposHandler := api.NewReposHandler(logger, g.c.GitserverURL)
+
loginUserHandler := api.NewLoginUserHandler(logger, g.ah)
authorizeHandler := api.NewAuthorizeHandler(logger, g.ah)
registerHandler := api.NewRegisterUserHandler(logger, g.ah)
@@ -275,11 +278,13 @@ func (g *Gateway) Run(ctx context.Context) error {
apirouter.Handle("/runs/{runid}/tasks/{taskid}/actions", authForcedHandler(runTaskActionsHandler)).Methods("PUT")
apirouter.Handle("/runs", authForcedHandler(runsHandler)).Methods("GET")
+ apirouter.Handle("/user/remoterepos/{remotesourceref}", authForcedHandler(userRemoteReposHandler)).Methods("GET")
+
+ apirouter.Handle("/badges/{projectref}", badgeHandler).Methods("GET")
+
// TODO(sgotti) add auth to these requests
router.Handle("/repos/{rest:.*}", reposHandler).Methods("GET", "POST")
- apirouter.Handle("/user/remoterepos/{remotesourceref}", authForcedHandler(userRemoteReposHandler)).Methods("GET")
-
router.Handle("/login", loginUserHandler).Methods("POST")
router.Handle("/authorize", authorizeHandler).Methods("POST")
router.Handle("/register", registerHandler).Methods("POST")
diff --git a/internal/services/runservice/api/client.go b/internal/services/runservice/api/client.go
index 070fdd2..e2a4f16 100644
--- a/internal/services/runservice/api/client.go
+++ b/internal/services/runservice/api/client.go
@@ -214,6 +214,10 @@ func (c *Client) GetGroupFirstQueuedRuns(ctx context.Context, group string, chan
return c.GetRuns(ctx, []string{"queued"}, []string{group}, false, changeGroups, "", 1, true)
}
+func (c *Client) GetGroupLastRun(ctx context.Context, group string, changeGroups []string) (*GetRunsResponse, *http.Response, error) {
+ return c.GetRuns(ctx, nil, []string{group}, false, changeGroups, "", 1, false)
+}
+
func (c *Client) CreateRun(ctx context.Context, req *RunCreateRequest) (*RunResponse, *http.Response, error) {
reqj, err := json.Marshal(req)
if err != nil {