Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,40 @@ githubWorkflow:
`platforms` configures for which platforms the multi-arch docker image is built. Defaults to `linux/amd64`. Note: emulation is provided by qemu and might take significant time.
`tagStrategy` influences which container tags will be pushed. Currently `edge`, `latest`, `semver` and `sha` are supported.

### `githubWorkflow.pushHelmChartToGhcr`

If `path` is set to a valid Helm chart path inside the repository, the chart is automatically packaged and pushed as an OCI artifact to `oci://ghcr.io/${{ github.repository_owner }}/charts`, i.e. into the `charts` namespace owned by the GitHub user or organization that owns the repository.

```yaml
githubWorkflow:
pushHelmChartToGhcr:
path: charts/my-chart
```

`lint` configures whether the Helm chart should be linted with `helm lint` before packaging and pushing. Defaults to `true`.

`dependencyUpdate` configures whether chart dependencies should be updated with `helm dependency update` before packaging and pushing. Defaults to `true`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is within the lock file constraints, right? Than fine for me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, it's just a regular helm dep update, so only the lock file will be updated. It shouldn't hurt and only matters if the chart has dependency - so I've enabled it by default.


`disableVersioning` disables automatic version detection from the `pushContainerToGhcr` workflow. Has no effect when the `pushContainerToGhcr` workflow is disabled. Defaults to `false`.

When the repository uses the `pushContainerToGhcr` workflow with `semver` or `sha` tag strategy, the Helm chart's single `version` is derived from that strategy: if a valid semver tag is present, it becomes the chart version; otherwise a SHA-based version is used.
Only one version is applied to the chart per workflow run; it does not replicate additional container tags such as semver major/minor aliases. The `semver` strategy takes precedence over `sha` when both are enabled and a valid semver tag exists.

#### Using AppVersion inside the Chart to link Chart version and image

The Chart's AppVersion is automatically set to either the release semver or a SHA-based image tag (for example, `sha-<commit-sha>`) of the commit that triggered the workflow, overriding the version specified in `Chart.yaml`. This allows you to reference the image tag directly inside your chart using `{{ .Chart.AppVersion }}`, for example:

```yaml
kind: Deployment
spec:
...
image: ghcr.io/my-org/my-repo:{{ .Chart.AppVersion }}
```

#### Disabling auto-versioning and using the version from `Chart.yaml` instead

To disable auto-versioning and use the version specified in `Chart.yaml`, set `disableVersioning` to `true`.

### `githubWorkflow.release`

If `release` is enabled a workflow is generated which creates a new GitHub release using goreleaser when a git tag is pushed.
Expand Down
9 changes: 9 additions & 0 deletions internal/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ type GithubWorkflowConfiguration struct {
PushContainerToGhcr PushContainerToGhcrConfig `yaml:"pushContainerToGhcr"`
Release ReleaseWorkflowConfig `yaml:"release"`
SecurityChecks SecurityChecksWorkflowConfig `yaml:"securityChecks"`
PushHelmChartToGhcr PushHelmChartToGhcrConfig `yaml:"pushHelmChartToGhcr"`
}

// CIWorkflowConfig appears in type Configuration.
Expand Down Expand Up @@ -206,6 +207,14 @@ type PushContainerToGhcrConfig struct {
TagStrategy []string `yaml:"tagStrategy"`
}

// PushHelmChartToGhcrConfig appears in type GithubWorkflowConfiguration.
type PushHelmChartToGhcrConfig struct {
Path Option[string] `yaml:"path"`
Lint Option[bool] `yaml:"lint"`
DependencyUpdate Option[bool] `yaml:"dependencyUpdate"`
DisableVersioning bool `yaml:"disableVersioning"`
}

// ReleaseWorkflowConfig appears in type ReleaseWorkflowConfig.
type ReleaseWorkflowConfig struct {
Enabled Option[bool] `yaml:"enabled"`
Expand Down
1 change: 1 addition & 0 deletions internal/core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ const (
GoreleaserAction = "goreleaser/goreleaser-action@v7"
ReuseAction = "fsfe/reuse-action@v6"
TyposAction = "crate-ci/typos@v1"
HelmSetupAction = "azure/setup-helm@v4"
)
1 change: 1 addition & 0 deletions internal/ghworkflow/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func Render(cfg core.Configuration, sr golang.ScanResult) {
ciWorkflow(cfg, sr)
codeQLWorkflow(cfg)
}
helmWorkflow(cfg)
ghcrWorkflow(ghwCfg)
releaseWorkflow(cfg)
}
Expand Down
120 changes: 120 additions & 0 deletions internal/ghworkflow/workflow_helm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0

package ghworkflow

import (
"slices"
"strings"

"github.com/sapcc/go-makefile-maker/internal/core"
)

func helmWorkflow(cfg core.Configuration) {
// https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#publishing-a-package-using-an-action
w := newWorkflow("Helm OCI Package GHCR", cfg.GitHubWorkflow.Global.DefaultBranch, nil)

if w.deleteIf(cfg.GitHubWorkflow.PushHelmChartToGhcr.Path.IsSome() &&
strings.HasPrefix(cfg.Metadata.URL, "https://github.com")) {
return
}

var helmConfig = cfg.GitHubWorkflow.PushHelmChartToGhcr
var chartPath = helmConfig.Path.UnwrapOr(".")

w.Permissions.Contents = tokenScopeRead
w.Permissions.Packages = tokenScopeWrite

w.On.WorkflowDispatch.manualTrigger = true

var helmPackageCmds []string
strategy := cfg.GitHubWorkflow.PushContainerToGhcr.TagStrategy
if !helmConfig.DisableVersioning {
if slices.Contains(strategy, "semver") {
// try to detect a version from the git tags
w.On.Push.Tags = []string{"*"}
helmPackageCmds = append(helmPackageCmds,
`# try to detect a version from the git tags, set APP_VERSION only if this commit has been tagged`,
`APP_VERSION=$(git describe --tags --exact-match ${{ github.sha }} 2>/dev/null || echo "")`,
`if [ -n "$APP_VERSION" ]; then`,
` VERSION=$(echo -n "$APP_VERSION" | sed -E 's/^v//')`,
`else`,
` VERSION=$((git describe --tags --abbrev=0 2>/dev/null | sed -E 's/^v//') || echo "")`,
`fi`,
``,
)
}
if slices.Contains(strategy, "sha") {
w.On.Push.Branches = []string{cfg.GitHubWorkflow.Global.DefaultBranch}
// use the git sha as version, if no version could be detected from the tags
helmPackageCmds = append(helmPackageCmds,
`# use the git sha as app-version, if no version could be detected from the tags`,
`if [ -z "$APP_VERSION" ]; then`,
` APP_VERSION=$(echo -n "sha-${{ github.sha }}")`,
`fi`,
`# use the git sha as helm version suffix, version is semver`,
`if [ -z "$VERSION" ] && [ -n "$APP_VERSION" ]; then`,
` VERSION="$(helm show chart `+chartPath+` | grep -E "^version:" | awk '{print $2}' )+${APP_VERSION:0:11}"`,
`fi`,
``,
)
}
}
w.On.PullRequest.Branches = nil

if !slices.Contains(strategy, "sha") && !slices.Contains(strategy, "semver") {
// Just upload any chart change
w.On.Push.Paths = []string{chartPath + "/**"}
}

if helmConfig.DependencyUpdate.UnwrapOr(true) {
helmPackageCmds = append(helmPackageCmds, `HELM_ARGS=--dependency-update`)
}

const registry = "ghcr.io"
j := baseJob("Build and publish Helm Chart OCI", cfg.GitHubWorkflow)
j.Steps[0].With = map[string]any{
"fetch-depth": 0, // we need the full git history to be able to detect versions from tags
"fetch-tags": true, // we need the tags to be able to detect versions from tags
}
j.addStep(jobStep{
Name: "Install Helm",
Uses: core.HelmSetupAction,
})
if helmConfig.Lint.UnwrapOr(true) {
j.addStep(jobStep{
Name: "Lint Helm Chart",
Run: "helm lint " + chartPath,
})
}
j.addStep(jobStep{
Name: "Package Helm Chart",
Run: makeMultilineYAMLString(append(
helmPackageCmds,
`if [ -n "$APP_VERSION" ]; then`,
` HELM_ARGS="$HELM_ARGS --app-version $APP_VERSION"`,
`fi`,
`if [ -n "$VERSION" ]; then`,
` HELM_ARGS="$HELM_ARGS --version $VERSION"`,
`fi`,
`echo "Running helm package with $HELM_ARGS"`,
`helm package `+chartPath+` --destination ./chart $HELM_ARGS`,
)),
})
j.addStep(jobStep{
Name: "Log in to the Container registry",
Uses: core.DockerLoginAction,
With: map[string]any{ //nolint:gosec // not a hardcoded secret, we are doing templating here
"registry": registry,
"username": "${{ github.actor }}",
"password": "${{ secrets.GITHUB_TOKEN }}",
},
})
j.addStep(jobStep{
Name: "Push Helm Chart to " + registry,
Run: "helm push ./chart/*.tgz oci://" + registry + "/${{ github.repository_owner }}/charts",
})
w.Jobs = map[string]job{"build-and-push-helm-package": j}

writeWorkflowToFile(w)
}
Loading