# chart-releaser-action and the unglamorous work of hosting your own Helm charts

> The helm/chart-releaser-action turns a GitHub repository into a self-hosted Helm chart repository: on every push it packages changed charts, creates a GitHub release per chart version, and maintains an index.yaml on a gh-pages branch. It is a thin wrapper around the chart-releaser CLI, and the interesting decisions are all in the inputs.

**helm/chart-releaser-action** — A GitHub Action to turn a GitHub project into a self-hosted Helm chart repo, using helm/chart-releaser CLI tool

- Repository: https://github.com/helm/chart-releaser-action
- Website: https://github.com/helm/chart-releaser
- Stars: 666 · Forks: 221
- Language: Shell
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/helm-chart-releaser-action

## A wrapper that does one thing on every push

The chart-releaser-action is a GitHub action to turn a GitHub project into a self-hosted Helm chart repo, using the helm/chart-releaser CLI tool.

What it does on every push to main is specific. It checks each chart in your project, and whenever there is a new chart version it creates a corresponding GitHub release named for the chart version, adds the Helm chart artifacts to that release, and creates or updates an index.yaml file with metadata about those releases, which is then hosted on GitHub Pages.

The last sentence of that is the part that saves work. You do not need an index.yaml file in main at all, because it is managed in the gh-pages branch. The index is a derived artefact that most chart publishers end up maintaining by hand, and this action regenerates it on every run.

The repository itself is small, which is expected for an action wrapper. The top level contains an action.yml, a cr.sh script, the LICENSE, the CODE_OF_CONDUCT.md and the.github directory. The project is Apache-2.0 licensed, the default branch is main, and the last push was on 2026-10-21.

The release history shows a slow cadence, and it is worth noting. v1.5.0 came out on 2023-01-06, v1.6.0 on 2023-11-02 and v1.7.0 on 2025-01-20. The default version input is also v1.7.0, so the action pins the CLI it drives to the release it shipped with unless you override it.

The primary language recorded for the repository is Shell, which is a good clue about the implementation. This is not a Go program reimplementing chart publishing. It is a shell script that installs and invokes the chart-releaser binary with the right arguments.

## The pre-requisites assume a specific GitHub layout

The pre-requisites are short enough to enumerate, and they tell you what the action expects to find.

First, a GitHub repo containing a directory with your Helm charts, and the default is a folder named /charts. If you keep your charts somewhere else, you must include a charts_dir input in the workflow. Second, a GitHub branch called gh-pages to store the published charts. Third, in your repo you go to Settings, then Pages, and change the Source Branch to gh-pages. Fourth, you create a workflow yml file in your .github/workflows directory, and an example workflow is provided.

The gh-pages requirement is the significant one. This action publishes charts as GitHub Pages content, which means the branch serves two purposes at once. It holds the index.yaml and the chart packages, and it is a branch in your repository that the action pushes to on every run. That branch has to exist, and Pages has to be pointed at it.

The example workflow shows the whole thing, and the structure of the YAML is the documentation. The permissions block declares contents: write, and the comment above it explains why: depending on the default permission settings for your organisation, whether contents is read-only or read-write for workloads, you will have to add permissions. The checkout step uses actions/checkout@v4 with fetch-depth: 0, and the full history is needed because chart-releaser compares chart versions against git history to decide what changed.

A configure git step sets the user name and email to the GitHub actor, which is what makes the push to gh-pages attributable in the history rather than appearing as a stranger.

## The version input pins the CLI, and the config file is the escape hatch

The inputs list is where the design decisions live, and the first one is the version.

The version input sets the chart-releaser version to use, with a default of v1.7.0. So the action ships with a default CLI version and lets you move it. That is the right way round for a wrapper whose job is to invoke another tool, because the two can be released independently, and the action's own releases are infrequent.

The config input is the other escape hatch. It is an optional config file for chart-releaser, and the documentation for the chart-releaser config file is the reference. The example shows a workflow that points at both a charts_dir and a config file, and the config file itself is a small YAML document:

```yaml
owner: myaccount
git-base-url: https://api.github.com/
```

Those two keys are worth understanding rather than copying. The owner is the GitHub organisation or user that owns the release, and git-base-url points at the GitHub API. The base URL setting is what lets the tool work against a GitHub Enterprise installation, where the API is not at api.github.com. That is the kind of detail that a self-hosted setup hits immediately and a public one never does.

The pattern here is the general one for this action. Almost every behaviour that is not a simple boolean is delegated to the chart-releaser config file, and the action's own inputs are the handful of things that differ between CI runs.

## The skip inputs exist for people who do not want the default packaging

Three inputs let you take a stage out of the pipeline, and each of them is aimed at a specific objection to the default behaviour.

skip_packaging, when populated, skips the packaging step. The documentation explains why you would want that: it allows you to do more advanced packaging of your charts, for example with the helm package command, before this action runs. The action then only handles the indexing and publishing steps. So a team that signs its charts, or that produces them in a particular way, can package them itself and hand the result over.

skip_upload does the same for the other half. It skips the upload step, which allows more advanced uploading of your charts, for example with OCI based repositories, which do not require the index.yaml. The documentation contains a typo here, with exemple and doen't both in one sentence, which is a small reminder that this is a community maintained README.

skip_existing is different in kind. It skips package upload if a release or tag already exists, which is a safety net for a re-run rather than a replacement for a stage.

The remaining two inputs shape the release rather than the pipeline. mark_as_latest, when set to false, marks the created GitHub release as not latest, which matters for a repository that also tags other kinds of release. packages_with_index, when set to true, uploads chart packages directly into the publishing branch, so the packages sit in gh-pages alongside the index rather than only in the GitHub releases.

The pages_branch input names the branch used to push the index and artefacts. The default is gh-pages, and the documentation is careful to say it is not set in the action, it is a default value for the chart-releaser binary. So the fallback belongs to the CLI, not to the wrapper.

## OCI push is a second destination, not a replacement

The oci_registry_url input adds a registry destination to the action, and the documentation describes the mechanics plainly.

When it is set, the action additionally pushes each packaged chart to that OCI registry via cr push, and the example uses an OCI URL of the form oci://ghcr.io/myorg/charts. The wording is careful. Additionally, or in addition to or instead of, the GitHub Pages flow. The action does not switch modes; it adds a destination.

Authentication has two documented routes. The recommended approach is to run docker/login-action before this action so credentials are placed in the local registry credential store, and cr push then picks them up automatically. The example does exactly that, logging in to ghcr.io with the actor as the username and the GitHub token as the password, then running the action with the registry URL.

The alternative is to pass oci_username and oci_password directly as inputs. Both inputs fall back to the local registry credential store when unset, and the documentation names the places that store is populated, including ~/.docker/config.json, docker/login-action, helm registry login and docker login.

That fallback behaviour is the detail worth noticing. Because the credentials are optional and default to whatever is already on the machine, the same workflow works in a development container and in CI without branching, and the explicit inputs exist for the case where you cannot use a credential store at all.

## Two outputs, and one of them depends on how you configured the run

The outputs section describes what a downstream job can read, and the two entries behave differently.

changed_charts is a comma-separated list of the charts that were released on this run. It will be an empty string if no updates were detected, and it will be unset if skip_packaging is used, because in that case your custom packaging step is responsible for setting its own outputs if you need them. That is a sensible contract. The action cannot know what your own packaging produced, so it declines to guess.

chart_version is the version of the most recently generated charts, and it will be set even if no charts have been updated since the last run. That asymmetry is the useful part. A workflow that wants to deploy or notify on every run needs a value to interpolate into a message or a tag, and changed_charts would be empty in the steady state where nothing shipped. chart_version gives you something stable to point at.

The single required environment variable is CR_TOKEN, the GitHub token of the repository, which in practice is the workflow's own GITHUB_TOKEN passed through secrets. The chart-releaser documentation for environment variables is linked for the rest.

Together the inputs, outputs and environment variable tell you the shape of the whole integration. The action is a single step in a workflow, it needs one token, it produces at most two values, and everything else is configuration.

## Where chart-releaser-action is the wrong tool

The first wrong case is a distribution model that is not GitHub Pages. This action publishes an index.yaml on a branch, which is the classic Helm repository model. If your requirement is OCI only, with no index and no Pages, then the action is running a stage you do not want. You can set skip_upload to avoid the upload, but you are then left with a release-and-index workflow whose output you do not use, and the cleaner path is to package and push with the helm tooling directly in your own workflow.

The second wrong case is a chart repository that needs to be a service. The GitHub Pages model gives you a static index with no authentication, no browsing interface and no control over who can read it beyond whatever the repository's visibility allows. A chart museum, with a UI, API and credentials, is a different product for a different need, and no wrapper around a CLI changes that.

The third is a team that does not want release tags accumulating in their source repository. Every chart version produces a GitHub release and a tag. That is the mechanism, and it is a reasonable one, but a repository with many charts ends up with many tags, and if you already tag releases for another reason, the two collide in the same namespace.

The alternative worth naming is a workflow that calls helm package and helm push yourself, plus a static index hosted wherever you like. The difference in approach is that chart-releaser-action packages the decision for you, deciding which charts changed by reading git history, naming releases, and regenerating the index, which is a lot of behaviour to get right and a lot of behaviour to inherit. The manual route gives you exactly the steps you want and nothing else, at the cost of writing the change detection and index generation yourself. For a handful of charts maintained by one person, the manual route is often shorter. For dozens of charts across a team, having the index regenerated correctly on every push is worth the wrapper.

## Maintenance, licence and what the action does not tell you

The action is Apache-2.0 licensed, with the licence file in the repository root, and participation in the Helm community is governed by the CODE_OF_CONDUCT.md in the same directory. The code of conduct reference tells you this action sits inside a larger Helm project structure rather than being a standalone utility.

The maintenance state deserves a second look because the release dates are spread out. The repository is not archived and the last push was on 2026-10-21, which is recent, but the newest tag is v1.7.0 from 2025-01-20, and before that v1.6.0 from November 2023. So the current source contains work that has not been cut into a release. Anyone pinning helm/chart-releaser-action@v1.7.0, as the example workflow does, is running the January 2025 version, and the OCI inputs in particular are recent enough that the documentation and the released behaviour may not be identical.

What the README does not document is worth noticing too. It does not describe the contents of action.yml, so the exact runtime and the steps the shell script performs are not visible here. It does not say how the change detection behaves when a chart version is bumped without a commit to the chart directory, or how it handles a chart that is removed. And it points at the chart-releaser documentation for the config file and for environment variables, which is where the substantive reference lives.

The practical conclusion is that the action is a thin, well-shaped wrapper whose complexity is in the tool it drives, and that evaluating it means reading the chart-releaser documentation as much as this README.

## Conclusion

chart-releaser-action fits a team that wants to publish Helm charts from the same repository as the chart source, on a branch it controls, without standing up a chart museum server. It is the wrong choice if you need OCI as the only distribution path with no GitHub Pages involved, and the wrong choice if you would rather have a hosted registry with authentication and a web UI. Before you adopt it, decide which of the skip and OCI inputs you need, read the chart-releaser config file documentation for the options this wrapper does not expose, and check that your repository's default token permissions allow contents to be written.

## FAQ

### What does the helm chart-releaser-action do?

On every push it checks each chart in the repository and, when there is a new chart version, creates a GitHub release named for that version, adds the chart artifacts to it, and creates or updates an index.yaml with metadata about the releases, which is hosted on GitHub Pages. You do not need an index.yaml in main because it is managed in the gh-pages branch.

### What do I need before using chart-releaser-action?

A GitHub repository with a directory of Helm charts, a gh-pages branch to store the published charts, Pages configured with its Source Branch set to gh-pages, and a workflow file in your .github/workflows directory. The charts directory defaults to /charts, and a different location requires the charts_dir input.

### Can chart-releaser-action push to an OCI registry?

Yes. Setting oci_registry_url additionally pushes each packaged chart to that registry via cr push, using either the local registry credential store populated by docker/login-action or the oci_username and oci_password inputs, which fall back to that store when unset.

### What are the outputs of chart-releaser-action?

changed_charts is a comma-separated list of the charts released on the run, empty when nothing changed and unset when skip_packaging is used. chart_version is the version of the most recently generated charts and is set even when no charts have been updated.

### Which token does chart-releaser-action need?

The required environment variable is CR_TOKEN, the GitHub token of the repository, normally passed as ${{ secrets.GITHUB_TOKEN }}. The example workflow also declares permissions with contents: write, since the default token permissions for an organisation may otherwise be read-only.

## Sources

- [helm/chart-releaser-action on GitHub](https://github.com/helm/chart-releaser-action)
- [License: Apache-2.0](https://github.com/helm/chart-releaser-action/blob/main/LICENSE)
- [Project website](https://github.com/helm/chart-releaser)
- [README](https://github.com/helm/chart-releaser-action/blob/main/README.md)
- [Releases](https://github.com/helm/chart-releaser-action/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/helm-chart-releaser-action
