diff --git a/.github/workflows/bypass-test.yaml b/.github/workflows/bypass-test.yaml index 3a3102e3e574..93c5ba2869d3 100644 --- a/.github/workflows/bypass-test.yaml +++ b/.github/workflows/bypass-test.yaml @@ -9,6 +9,7 @@ on: - 'mkdocs.yml' - 'LICENSE' - '.release-please-manifest.json' + - 'helm/trivy/Chart.yaml' pull_request: paths: - '**.md' @@ -16,6 +17,7 @@ on: - 'mkdocs.yml' - 'LICENSE' - '.release-please-manifest.json' + - 'helm/trivy/Chart.yaml' jobs: test: name: Test diff --git a/.github/workflows/publish-chart.yaml b/.github/workflows/publish-chart.yaml index 3a7db4970065..15c8d84da31d 100644 --- a/.github/workflows/publish-chart.yaml +++ b/.github/workflows/publish-chart.yaml @@ -4,6 +4,11 @@ name: Publish Helm chart on: workflow_dispatch: pull_request: + types: + - opened + - synchronize + - reopened + - closed branches: - main paths: @@ -18,8 +23,10 @@ env: KIND_VERSION: "v0.14.0" KIND_IMAGE: "kindest/node:v1.23.6@sha256:b1fa224cc6c7ff32455e0b1fd9cbfd3d3bc87ecaa8fcb06961ed1afb3db0f9ae" jobs: + # `test-chart` job starts if a PR with Helm Chart is created, merged etc. test-chart: - runs-on: ubuntu-20.04 + if: github.event_name != 'push' + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4.1.6 @@ -28,11 +35,12 @@ jobs: - name: Install Helm uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 with: - version: v3.5.0 + version: v3.14.4 - name: Set up python uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: '3.x' + check-latest: true - name: Setup Chart Linting id: lint uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 @@ -48,11 +56,39 @@ jobs: sed -i -e '136s,false,'true',g' ./helm/trivy/values.yaml ct lint-and-install --validate-maintainers=false --charts helm/trivy + # `update-chart-version` job starts if a new tag is pushed + update-chart-version: + if: github.event_name == 'push' + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4.1.6 + with: + fetch-depth: 0 + - name: Set up Git user + run: | + git config --global user.email "actions@github.com" + git config --global user.name "GitHub Actions" + + - name: Install tools + uses: aquaproj/aqua-installer@v3.0.1 + with: + aqua_version: v1.25.0 + aqua_opts: "" + + - name: Create a PR with Trivy version + run: mage helm:updateVersion + env: + # Use ORG_REPO_TOKEN instead of GITHUB_TOKEN + # This allows the created PR to trigger tests and other workflows + GITHUB_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + + # `publish-chart` job starts if a PR with a new Helm Chart is merged or manually publish-chart: - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' needs: - test-chart - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4.1.6 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8932a683c5bc..05edadf93331 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,6 +7,7 @@ on: - 'mkdocs.yml' - 'LICENSE' - '.release-please-manifest.json' ## don't run tests for release-please PRs + - 'helm/trivy/Chart.yaml' merge_group: workflow_dispatch: diff --git a/magefiles/helm.go b/magefiles/helm.go new file mode 100644 index 000000000000..443c57eed029 --- /dev/null +++ b/magefiles/helm.go @@ -0,0 +1,117 @@ +//go:build mage_helm + +package main + +import ( + "fmt" + "log" + "os" + + "github.com/aquasecurity/go-version/pkg/semver" + + "github.com/magefile/mage/sh" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" +) + +const chartFile = "./helm/trivy/Chart.yaml" + +func main() { + trivyVersion, err := version() + if err != nil { + log.Fatalf("could not determine Trivy version: %v", err) + } + + newHelmVersion, err := bumpHelmChart(chartFile, trivyVersion) + if err != nil { + log.Fatalf("could not bump Trivy version to %q: %v", trivyVersion, err) + } + + log.Printf("Current helm version will bump up %q with Trivy %q", newHelmVersion, trivyVersion) + + newBranch := fmt.Sprintf("ci/helm-chart/bump-trivy-to-%s", trivyVersion) + title := fmt.Sprintf("ci(helm): bump Trivy version to %s for Trivy Helm Chart %s", trivyVersion, newHelmVersion) + description := fmt.Sprintf("This PR bumps Trivy up to the %s version for the Trivy Helm chart %s.", + trivyVersion, newHelmVersion) + + cmds := [][]string{ + []string{"git", "switch", "-c", newBranch}, + []string{"git", "add", chartFile}, + []string{"git", "commit", "-m", title}, + []string{"git", "push", "origin", newBranch}, + []string{"gh", "pr", "create", "--base", "main", "--head", newBranch, "--title", title, "--body", description, "--repo", "$GITHUB_REPOSITORY"}, + } + + if err := runShCommands(cmds); err != nil { + log.Fatal(err) + } + log.Print("Successfully created PR with a new helm version") +} + +type Chart struct { + Version string `yaml:"version"` + AppVersion string `yaml:"appVersion"` +} + +// bumpHelmChart bumps up helm and trivy versions inside a file (Chart.yaml) +// it returns a new helm version and error +func bumpHelmChart(filename, trivyVersion string) (string, error) { + input, err := os.ReadFile(filename) + if err != nil { + return "", xerrors.Errorf("could not read file %q: %w", filename, err) + } + currentHelmChart := &Chart{} + if err := yaml.Unmarshal(input, currentHelmChart); err != nil { + return "", xerrors.Errorf("could not unmarshal helm chart %q: %w", filename, err) + } + + newHelmVersion, err := buildNewHelmVersion(currentHelmChart.Version, currentHelmChart.AppVersion, trivyVersion) + if err != nil { + return "", xerrors.Errorf("could not build new helm version: %v", err) + } + cmds := [][]string{ + []string{"sed", "-i", "-e", fmt.Sprintf("s/appVersion: %s/appVersion: %s/g", currentHelmChart.AppVersion, trivyVersion), filename}, + []string{"sed", "-i", "-e", fmt.Sprintf("s/version: %s/version: %s/g", currentHelmChart.Version, newHelmVersion), filename}, + } + + if err := runShCommands(cmds); err != nil { + return "", xerrors.Errorf("could not update Helm Chart %q: %w", newHelmVersion, err) + } + return newHelmVersion, nil +} + +func runShCommands(cmds [][]string) error { + for _, cmd := range cmds { + if err := sh.Run(cmd[0], cmd[1:]...); err != nil { + return xerrors.Errorf("failed to run %v: %w", cmd, err) + } + } + return nil +} + +func buildNewHelmVersion(currentHelm, currentTrivy, newTrivy string) (string, error) { + currentHelmVersion, err := semver.Parse(currentHelm) + if err != nil { + return "", xerrors.Errorf("could not parse current helm version: %w", err) + } + + currentTrivyVersion, err := semver.Parse(currentTrivy) + if err != nil { + return "", xerrors.Errorf("could not parse current trivy version: %w", err) + } + + newTrivyVersion, err := semver.Parse(newTrivy) + if err != nil { + return "", xerrors.Errorf("could not parse new trivy version: %w", err) + } + + if newTrivyVersion.Major().Compare(currentTrivyVersion.Major()) > 0 { + return currentHelmVersion.IncMajor().String(), nil + } + + if newTrivyVersion.Minor().Compare(currentTrivyVersion.Minor()) > 0 { + return currentHelmVersion.IncMinor().String(), nil + } + + return currentHelmVersion.IncPatch().String(), nil +} diff --git a/magefiles/helm_test.go b/magefiles/helm_test.go new file mode 100644 index 000000000000..f2e3233d2879 --- /dev/null +++ b/magefiles/helm_test.go @@ -0,0 +1,92 @@ +//go:build mage_helm + +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewVersion(t *testing.T) { + tests := []struct { + name string + currentHelmVersion string + currentTrivyVersion string + newTrivyVersion string + newHelmVersion string + }{ + { + "created the first patch", + "0.1.0", + "0.55.0", + "0.55.1", + "0.1.1", + }, + { + "created the second patch", + "0.1.1", + "0.55.1", + "0.55.2", + "0.1.2", + }, + { + "created the second patch but helm chart was changed", + "0.1.2", + "0.55.1", + "0.55.2", + "0.1.3", + }, + { + "created a new minor version", + "0.1.1", + "0.55.1", + "0.56.0", + "0.2.0", + }, + { + "created a new major version", + "0.1.1", + "0.55.1", + "1.0.0", + "1.0.0", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + newHelmVersion, err := buildNewHelmVersion(test.currentHelmVersion, test.currentTrivyVersion, test.newTrivyVersion) + assert.NoError(t, err) + assert.Equal(t, test.newHelmVersion, newHelmVersion) + }) + } +} + +func TestBumpHelmChart_Success(t *testing.T) { + tempFile, err := os.CreateTemp(t.TempDir(), "Chart-*.yaml") + assert.NoError(t, err) + + content := ` +apiVersion: v2 +name: trivy +version: 0.8.0 +appVersion: 0.55.0 +description: Trivy helm chart +keywords: + - scanner + - trivy + - vulnerability +` + err = os.WriteFile(tempFile.Name(), []byte(content), 0644) + assert.NoError(t, err) + + newVersion, err := bumpHelmChart(tempFile.Name(), "0.55.1") + assert.NoError(t, err) + assert.Equal(t, "0.8.1", newVersion) + + updatedContent, err := os.ReadFile(tempFile.Name()) + assert.NoError(t, err) + assert.Contains(t, string(updatedContent), "appVersion: 0.55.1") + assert.Contains(t, string(updatedContent), "version: 0.8.1") +} diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 06ebd7e665e1..f70491bce2e8 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -489,3 +489,10 @@ func (CloudActions) Generate() error { func VEX(_ context.Context, dir string) error { return sh.RunWith(ENV, "go", "run", "-tags=mage_vex", "./magefiles/vex.go", "--dir", dir) } + +type Helm mg.Namespace + +// UpdateVersion updates a version for Trivy Helm Chart and creates a PR +func (Helm) UpdateVersion() error { + return sh.RunWith(ENV, "go", "run", "-tags=mage_helm", "./magefiles") +}