From 6344a8ca9d8bf0b4c788f111a3db3aadbdfbf302 Mon Sep 17 00:00:00 2001 From: tuti Date: Wed, 12 Feb 2025 15:25:40 -0800 Subject: [PATCH] updates --- release/cmd/flags.go | 7 ++- release/cmd/hashrelease.go | 12 +---- release/internal/ci/semaphore.go | 85 +++++++++++++++++++++++++++++--- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/release/cmd/flags.go b/release/cmd/flags.go index e18e8d1e8f7..8cdee4475b3 100644 --- a/release/cmd/flags.go +++ b/release/cmd/flags.go @@ -258,7 +258,7 @@ var ( // External flags are flags used to interact with external services var ( // CI flags for interacting with CI services (Semaphore) - ciFlags = []cli.Flag{ciFlag, ciBaseURLFlag, ciJobIDFlag, ciTokenFlag} + ciFlags = []cli.Flag{ciFlag, ciBaseURLFlag, ciJobIDFlag, ciPipelineIDFlag, ciTokenFlag} semaphoreCI = "semaphore" ciFlag = &cli.BoolFlag{ Name: "ci", @@ -282,6 +282,11 @@ var ( Usage: fmt.Sprintf("The job ID for the %s CI job", semaphoreCI), EnvVars: []string{"SEMAPHORE_JOB_ID"}, } + ciPipelineIDFlag = &cli.StringFlag{ + Name: "ci-pipeline-id", + Usage: fmt.Sprintf("The pipeline ID for the %s CI pipeline", semaphoreCI), + EnvVars: []string{"SEMAPHORE_PIPELINE_ID"}, + } ciTokenFlag = &cli.StringFlag{ Name: "ci-token", Usage: fmt.Sprintf("The token for interacting with %s API", semaphoreCI), diff --git a/release/cmd/hashrelease.go b/release/cmd/hashrelease.go index 45a1ed7bfe2..10d5d68f94f 100644 --- a/release/cmd/hashrelease.go +++ b/release/cmd/hashrelease.go @@ -16,9 +16,7 @@ package main import ( "fmt" - "os" "path/filepath" - "strconv" "github.com/sirupsen/logrus" cli "github.com/urfave/cli/v2" @@ -402,15 +400,7 @@ func validateCIBuildRequirements(c *cli.Context, repoRootDir string) error { } orgURL := c.String(ciBaseURLFlag.Name) token := c.String(ciTokenFlag.Name) - pipelineID := c.String(ciJobIDFlag.Name) - if promotion, err := strconv.ParseBool(os.Getenv("SEMAPHORE_PIPELINE_PROMOTION")); err != nil { - return fmt.Errorf("failed to parse promotion environment variable: %v", err) - } else if promotion { - logrus.Info("This is a promotion pipeline, checking if all images promotion pipelines have passed...") - pipelineID = os.Getenv("SEMAPHORE_PIPELINE_0_ARTEFACT_ID") - } else { - logrus.Info("This is a regular pipeline, skipping images promotions check...") - } + pipelineID := c.String(ciPipelineIDFlag.Name) promotionsDone, err := ci.ImagePromotionsDone(repoRootDir, orgURL, pipelineID, token) if err != nil { return fmt.Errorf("failed to check if images promotions are done: %v", err) diff --git a/release/internal/ci/semaphore.go b/release/internal/ci/semaphore.go index 55072e37af1..3eba72b0621 100644 --- a/release/internal/ci/semaphore.go +++ b/release/internal/ci/semaphore.go @@ -21,7 +21,12 @@ type promotion struct { } type pipeline struct { - Result string `json:"result"` + Result string `json:"result"` + PromotionOf string `json:"promotion_of"` +} + +type pipelineDetails struct { + Pipeline pipeline `json:"pipeline"` } func apiURL(orgURL, path string) string { @@ -30,7 +35,7 @@ func apiURL(orgURL, path string) string { return fmt.Sprintf("%s/api/v1alpha/%s", orgURL, path) } -func fetchPromotions(orgURL, pipelineID, token string) ([]promotion, error) { +func fetchImagePromotions(orgURL, pipelineID, token string) ([]promotion, error) { url := apiURL(orgURL, "/promotions") req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -57,17 +62,30 @@ func fetchPromotions(orgURL, pipelineID, token string) ([]promotion, error) { return nil, fmt.Errorf("failed to parse promotions: %s", err.Error()) } - var imagesPromotions []promotion + imagesPromotionsMap := make(map[string]promotion) for _, p := range promotions { if strings.HasPrefix(strings.ToLower(p.Name), "push ") { - imagesPromotions = append(imagesPromotions, p) + if currentP, ok := imagesPromotionsMap[p.Name]; ok { + // If the promotion is already in the map, + // only if the staus for the promotion in the map is not passed. + if currentP.Status != passed { + imagesPromotionsMap[p.Name] = p + } + } else { + imagesPromotionsMap[p.Name] = p + } } } + + imagesPromotions := make([]promotion, 0, len(imagesPromotionsMap)) + for _, p := range imagesPromotionsMap { + imagesPromotions = append(imagesPromotions, p) + } return imagesPromotions, nil } func getPipelineResult(orgURL, pipelineID, token string) (*pipeline, error) { - url := apiURL(orgURL, fmt.Sprintf("/pipeline/%s", pipelineID)) + url := apiURL(orgURL, fmt.Sprintf("/pipelines/%s", pipelineID)) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create %s request: %s", url, err.Error()) @@ -85,14 +103,51 @@ func getPipelineResult(orgURL, pipelineID, token string) (*pipeline, error) { return nil, fmt.Errorf("failed to fetch pipeline details") } - var p pipeline + var p pipelineDetails if err := json.NewDecoder(resp.Body).Decode(&p); err != nil { return nil, fmt.Errorf("failed to parse pipeline: %s", err.Error()) } - return &p, err + return &p.Pipeline, err } +func fetchParentPipelineID(orgURL, pipelineID, token string) (string, error) { + url := apiURL(orgURL, fmt.Sprintf("/pipelines/%s", pipelineID)) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", fmt.Errorf("failed to create %s request: %s", url, err.Error()) + } + req.Header.Set("Authorization", fmt.Sprintf("Token %s", token)) + + logrus.WithField("url", req.URL.String()).Debug("get pipeline details") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to request pipeline details: %s", err.Error()) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch pipeline details") + } + + var p pipelineDetails + if err := json.NewDecoder(resp.Body).Decode(&p); err != nil { + return "", fmt.Errorf("failed to parse pipeline: %s", err.Error()) + } + + return p.Pipeline.PromotionOf, err +} + +// ImagePromotionsDone checks if all the promotion pipelines have passed. +// +// As it is checking in the hashrelease pipeline, it tries to get the pipeline that triggered the hashrelease promotion. +// If the pipeline that triggered the hashrelease promotion is not found, +// this means that the hashrelease pipeline was not triggered by a promotion (likely triggered from a task). +// In this case, it skips the image promotions check. +// +// Once the pipeline that triggered the hashrelease promotion is found, it checks if all the expected image promotions have passed. +// Since the API only return promotions that have been triggered, it is possible that some promotions are not triggered. +// This is why it checks that the number of promotions is equal or greater than the expected number from the semaphore.yml. func ImagePromotionsDone(repoRootDir, orgURL, pipelineID, token string) (bool, error) { expectPromotionCountStr, err := command.Run("grep", []string{"-c", `"name: Push "`, fmt.Sprintf("%s/.semaphore/semaphore.yml.d/03-promotions.yml", repoRootDir)}) if err != nil { @@ -102,11 +157,25 @@ func ImagePromotionsDone(repoRootDir, orgURL, pipelineID, token string) (bool, e if err != nil { return false, fmt.Errorf("unable to convert expected promotions to int") } - promotions, err := fetchPromotions(orgURL, pipelineID, token) + logrus.WithField("count", expectedPromotionCount).Debug("expected number of image promotions") + parentPipelineID, err := fetchParentPipelineID(orgURL, pipelineID, token) + if err != nil { + return false, err + } + if parentPipelineID == "" { + logrus.Info("no parent pipeline found, skipping image promotions check") + return true, nil + } + logrus.WithField("pipeline_id", parentPipelineID).Debug("found pipeline that triggered image promotions") + promotions, err := fetchImagePromotions(orgURL, pipelineID, token) if err != nil { return false, err } promotionsCount := len(promotions) + logrus.WithFields(logrus.Fields{ + "expected": expectedPromotionCount, + "actual": promotionsCount, + }).Debug("number of image promotions") if promotionsCount < expectedPromotionCount { return false, fmt.Errorf("number of promotions do not match: expected %d, got %d", expectedPromotionCount, promotionsCount) }