├── Makefile ├── git ├── clone.go ├── container_test.go └── github.go ├── cmd ├── choice_flag.go ├── flags │ ├── join.go │ ├── platform.go │ ├── default.go │ ├── defaults_darwin_amd64.go │ ├── defaults_darwin_arm64.go │ ├── defaults_linux_amd64.go │ ├── defaults_linux_arm64.go │ ├── defaults_windows_amd64.go │ ├── defaults_windows_arm64.go │ ├── concurrency.go │ └── publish.go ├── gcom.go ├── pro_image.go ├── npm_publish.go ├── gcom_publish.go ├── package_publish.go ├── docker_publish.go ├── artifacts.go ├── main.go └── app.go ├── artifacts ├── storage.go ├── registerer.go ├── grafana_dir.go ├── sync_writer.go ├── parse_args_test.go ├── packages.go ├── flags.go ├── parse_args.go ├── version.go ├── plugins_bundled.go └── package_zip.go ├── containers ├── file_targz.go ├── package_validate.go ├── docs.go ├── sha256.go ├── withenv.go ├── ops_gcp.go ├── extracted_package.go ├── test_backend.go ├── version.go ├── exit_error.go ├── with_embedded_fs.go ├── publish.go ├── opts_pro_image.go ├── publish_dir.go └── package_input.go ├── arguments ├── flag_value_func.go ├── join.go ├── docs.go ├── golang.go ├── gpg.go ├── yarn.go ├── go_build_cache.go ├── packages.go ├── hg_docker.go └── docker.go ├── pipelines ├── package_test.go ├── publish.go ├── package_publish.go ├── npm_publish.go ├── package_names.go ├── pro_image.go └── docker_publish_test.go ├── .dockerignore ├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md ├── workflows │ ├── publish-techdocs.yml │ ├── publish-docker-image.yml │ ├── pr.yml │ ├── codeowners-validator.yml │ └── pr-integration-tests.yml └── zizmor.yml ├── .gitignore ├── msi ├── embed.go ├── resources │ ├── grafana_icon.ico │ ├── grafana_top_banner.bmp │ ├── grafana_top_banner.png │ ├── grafana_dialog_background.bmp │ ├── grafana_dialog_background.png │ └── grafana_top_banner_white.bmp ├── wxs_test.go ├── builder.go └── build.go ├── embed.go ├── scripts ├── packaging │ └── windows │ │ ├── winimg │ │ ├── grafana_icon.ico │ │ ├── grafana_top_banner.bmp │ │ ├── grafana_top_banner.png │ │ ├── grafana_dialog_background.bmp │ │ ├── grafana_dialog_background.png │ │ └── grafana_top_banner_white.bmp │ │ └── grafana-svc.xml ├── move_packages_npm_test.go ├── move_packages_storybook_test.go ├── move_packages_zip_test.go ├── move_packages_exe_test.go ├── move_packages_msi_test.go ├── move_packages_cdn_test.go ├── drone_publish_nightly_enterprise.sh ├── drone_build_main.sh ├── drone_build_main_pro.sh ├── drone_build_main_enterprise.sh ├── drone_build_tag_pro.sh ├── drone_build_nightly_grafana.sh ├── drone_build_tag_grafana.sh ├── all.sh ├── drone_build_tag_all.sh ├── drone_build_tag_enterprise.sh ├── drone_build_nightly_enterprise.sh ├── drone_publish_nightly_grafana.sh └── move_packages_rpm_test.go ├── backend ├── doc.go ├── vcsinfo.go ├── build.go └── env.go ├── docs ├── artifact-types │ ├── deb.md │ ├── index.md │ ├── tarball.md │ ├── zip.md │ ├── windows-installer.md │ ├── docker-image.md │ └── rpm.md ├── index.md ├── meta │ └── docs.md └── guides │ ├── tracing.md │ └── building.md ├── cliutil └── context.go ├── flags ├── join.go ├── docker.go ├── docs.go ├── distro.go └── packages.go ├── README.md ├── frontend ├── storybook.go ├── yarn.go ├── node.go ├── build.go ├── builder.go └── npm.go ├── stringutil └── random.go ├── fpm ├── builder.go └── verify.go ├── catalog-info.yaml ├── daggerutil └── hostdir.go ├── zip └── builder.go ├── ruleguard.rules.go ├── gpg ├── verify.go └── sign.go ├── golang └── cache.go ├── gcom ├── opts.go └── publish.go ├── e2e ├── validate_license.go └── validate_package.go ├── Dockerfile ├── mkdocs.yml ├── docker ├── opts.go ├── verify.go ├── publish.go ├── tags.go └── build.go ├── packages ├── names.go └── names_test.go ├── targz └── build.go ├── go.mod ├── pipeline ├── state_log.go ├── flag.go ├── artifact_store.go └── artifact_store_logger.go ├── versions ├── opts_test.go └── opts.go └── .golangci.toml /Makefile: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /git/clone.go: -------------------------------------------------------------------------------- 1 | package git 2 | -------------------------------------------------------------------------------- /cmd/choice_flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /artifacts/storage.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | -------------------------------------------------------------------------------- /containers/file_targz.go: -------------------------------------------------------------------------------- 1 | package containers 2 | -------------------------------------------------------------------------------- /arguments/flag_value_func.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | -------------------------------------------------------------------------------- /containers/package_validate.go: -------------------------------------------------------------------------------- 1 | package containers 2 | -------------------------------------------------------------------------------- /pipelines/package_test.go: -------------------------------------------------------------------------------- 1 | package pipelines_test 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | grafana 2 | grafana-* 3 | dist 4 | .git 5 | .github 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/grafana-release-guild @grafana/grafana-backend-services-squad 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .grafana/ 2 | bin/ 3 | *.tar.gz 4 | *.txt 5 | .idea/ 6 | dist/ 7 | grafana/ 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /msi/embed.go: -------------------------------------------------------------------------------- 1 | package msi 2 | 3 | import "embed" 4 | 5 | //go:embed resources/* 6 | var resources embed.FS 7 | -------------------------------------------------------------------------------- /msi/resources/grafana_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/msi/resources/grafana_icon.ico -------------------------------------------------------------------------------- /containers/docs.go: -------------------------------------------------------------------------------- 1 | // package containers holds functions to make it easier to work with dagger containers. 2 | package containers 3 | -------------------------------------------------------------------------------- /msi/resources/grafana_top_banner.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/msi/resources/grafana_top_banner.bmp -------------------------------------------------------------------------------- /msi/resources/grafana_top_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/msi/resources/grafana_top_banner.png -------------------------------------------------------------------------------- /msi/resources/grafana_dialog_background.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/msi/resources/grafana_dialog_background.bmp -------------------------------------------------------------------------------- /msi/resources/grafana_dialog_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/msi/resources/grafana_dialog_background.png -------------------------------------------------------------------------------- /msi/resources/grafana_top_banner_white.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/msi/resources/grafana_top_banner_white.bmp -------------------------------------------------------------------------------- /embed.go: -------------------------------------------------------------------------------- 1 | package grafanabuild 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed scripts/packaging/windows/* 8 | var WindowsPackaging embed.FS 9 | -------------------------------------------------------------------------------- /scripts/packaging/windows/winimg/grafana_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/scripts/packaging/windows/winimg/grafana_icon.ico -------------------------------------------------------------------------------- /backend/doc.go: -------------------------------------------------------------------------------- 1 | // Package backend holds the functions that create containers, files, and directories for building Grafana's backend binaries. 2 | package backend 3 | -------------------------------------------------------------------------------- /scripts/packaging/windows/winimg/grafana_top_banner.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/scripts/packaging/windows/winimg/grafana_top_banner.bmp -------------------------------------------------------------------------------- /scripts/packaging/windows/winimg/grafana_top_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/scripts/packaging/windows/winimg/grafana_top_banner.png -------------------------------------------------------------------------------- /scripts/packaging/windows/winimg/grafana_dialog_background.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/scripts/packaging/windows/winimg/grafana_dialog_background.bmp -------------------------------------------------------------------------------- /scripts/packaging/windows/winimg/grafana_dialog_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/scripts/packaging/windows/winimg/grafana_dialog_background.png -------------------------------------------------------------------------------- /scripts/packaging/windows/winimg/grafana_top_banner_white.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-build/HEAD/scripts/packaging/windows/winimg/grafana_top_banner_white.bmp -------------------------------------------------------------------------------- /docs/artifact-types/deb.md: -------------------------------------------------------------------------------- 1 | # Debian artifact (.deb) 2 | 3 | ``` 4 | $ dagger run go run ./cmd artifacts -a deb:enterprise:linux/amd64 5 | # Produces dist/grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.deb 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/artifact-types/index.md: -------------------------------------------------------------------------------- 1 | # Artifact types 2 | 3 | grafana-build supports the generation of various platform specific packages/artifacts: 4 | 5 | - Debian (.deb) 6 | - RPM 7 | - Windows installer 8 | - Docker images 9 | -------------------------------------------------------------------------------- /cliutil/context.go: -------------------------------------------------------------------------------- 1 | package cliutil 2 | 3 | type CLIContext interface { 4 | Bool(string) bool 5 | String(string) string 6 | Set(string, string) error 7 | StringSlice(string) []string 8 | Path(string) string 9 | Int64(string) int64 10 | } 11 | -------------------------------------------------------------------------------- /cmd/flags/join.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | func Join(f ...[]cli.Flag) []cli.Flag { 6 | flags := []cli.Flag{} 7 | for _, v := range f { 8 | flags = append(flags, v...) 9 | } 10 | 11 | return flags 12 | } 13 | -------------------------------------------------------------------------------- /cmd/gcom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | var GCOMCommand = &cli.Command{ 8 | Name: "gcom", 9 | Description: "Executes requests to grafana.com", 10 | Subcommands: []*cli.Command{GCOMPublishCommand}, 11 | } 12 | -------------------------------------------------------------------------------- /docs/artifact-types/tarball.md: -------------------------------------------------------------------------------- 1 | # Grafana tarball artifact 2 | 3 | This is mostly an intermitted artifact that can then be used as source to generate Debian, RPM, or other artifacts. 4 | 5 | ``` 6 | $ dagger run go run ./cmd artifacts -a targz:grafana:linux/amd64 7 | ``` 8 | -------------------------------------------------------------------------------- /cmd/flags/platform.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | var Platform = &cli.StringFlag{ 6 | Name: "platform", 7 | Usage: "The buildkit / dagger platform to run containers when building the backend", 8 | Value: DefaultPlatform, 9 | } 10 | -------------------------------------------------------------------------------- /flags/join.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import "github.com/grafana/grafana-build/pipeline" 4 | 5 | func JoinFlags(f ...[]pipeline.Flag) []pipeline.Flag { 6 | r := []pipeline.Flag{} 7 | for _, v := range f { 8 | r = append(r, v...) 9 | } 10 | 11 | return r 12 | } 13 | -------------------------------------------------------------------------------- /docs/artifact-types/zip.md: -------------------------------------------------------------------------------- 1 | # ZIP artifact 2 | 3 | This is basically just a simple repackaging of the tarball. 4 | 5 | ``` 6 | $ dagger run go run ./cmd zip artifacts -a zip:enterprise:linux/amd64 7 | # Produces dist/grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.zip 8 | ``` 9 | -------------------------------------------------------------------------------- /arguments/join.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import "github.com/grafana/grafana-build/pipeline" 4 | 5 | func Join(f ...[]pipeline.Argument) []pipeline.Argument { 6 | r := []pipeline.Argument{} 7 | for _, v := range f { 8 | r = append(r, v...) 9 | } 10 | 11 | return r 12 | } 13 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Grafana Build 2 | 3 | Grafana Build is a build pipeline for Grafana using Dagger. 4 | The goal of this project is to make it as easy as possible to generate builds as part of the overall release pipeline. 5 | 6 | To **get started**, take a look at the [Building & Packaging guide](./guides/building.md). 7 | 8 | -------------------------------------------------------------------------------- /scripts/move_packages_npm_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var npmMapping = map[string]m{ 4 | "Grafana data": { 5 | input: "file://dist/tag/grafana-10.2.0-pre/npm-packages", 6 | output: []string{ 7 | "artifacts/npm/v10.2.0-pre/npm-artifacts", 8 | }, 9 | env: map[string]string{"DRONE_TAG": "10.2.0-pre"}, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /docs/meta/docs.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | The documentation for this project is written using Mkdocs. 4 | To get a local preview of it (e.g. if you want to test-run some changes), run the following command: 5 | 6 | ``` 7 | $ go run ./ci docs serve 8 | ``` 9 | 10 | You can then browse the rendered documentation on . -------------------------------------------------------------------------------- /docs/artifact-types/windows-installer.md: -------------------------------------------------------------------------------- 1 | # Windows installer artifact (.exe) 2 | 3 | grafana-build can create a Windows installer out of a [Grafana tarball][pkg] using [NSIS][]. 4 | 5 | ``` 6 | $ dagger run go run ./cmd artifacts -a exe:grafana:windows/amd64 7 | ``` 8 | 9 | [nsis]: https://nsis.sourceforge.io/Main_Page 10 | [pkg]: ./tarball.md 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | - package-ecosystem: docker 12 | directory: / 13 | schedule: 14 | interval: weekly 15 | -------------------------------------------------------------------------------- /scripts/packaging/windows/grafana-svc.xml: -------------------------------------------------------------------------------- 1 | 2 | Grafana 3 | Grafana Server 4 | This service runs Grafana 5 | %BASE%\bin\grafana.exe 6 | server --homepath="%BASE%" 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grafana-build 2 | 3 | GitHub actions and Go packages for building Grafana using [Dagger](https://dagger.io). 4 | To get started, executed the following command in your terminal: 5 | 6 | ```shell 7 | go run ./cmd --help 8 | ``` 9 | 10 | You can find further information in the [docs 📖](https://github.com/grafana/grafana-build/tree/main/docs)! 11 | -------------------------------------------------------------------------------- /artifacts/registerer.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import "github.com/grafana/grafana-build/pipeline" 4 | 5 | type Initializer struct { 6 | InitializerFunc pipeline.ArtifactInitializer 7 | Arguments []pipeline.Argument 8 | } 9 | 10 | type Registerer interface { 11 | Register(string, Initializer) error 12 | Initializers() map[string]Initializer 13 | } 14 | -------------------------------------------------------------------------------- /cmd/flags/default.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | var Verbose = &cli.BoolFlag{ 6 | Name: "verbose", 7 | Aliases: []string{"v"}, 8 | Usage: "Increase log verbosity. WARNING: This setting could potentially log sensitive data", 9 | Value: false, 10 | } 11 | 12 | var DefaultFlags = []cli.Flag{ 13 | Platform, 14 | Verbose, 15 | } 16 | -------------------------------------------------------------------------------- /cmd/flags/defaults_darwin_amd64.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | // DefaultDistros are distributions that can quickly be built in an ideal scenario for the operating system on the above build tag. 4 | var DefaultDistros = []string{"linux/amd64"} 5 | 6 | // DefaultPlatform is the docker platform that will natively / most efficiently run on the OS/arch filtered by the above tag. 7 | var DefaultPlatform = "linux/amd64" 8 | -------------------------------------------------------------------------------- /cmd/flags/defaults_darwin_arm64.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | // DefaultDistros are distributions that can quickly be built in an ideal scenario for the operating system on the above build tag. 4 | var DefaultDistros = []string{"linux/arm64"} 5 | 6 | // DefaultPlatform is the docker platform that will natively / most efficiently run on the OS/arch filtered by the above tag. 7 | var DefaultPlatform = "linux/arm64" 8 | -------------------------------------------------------------------------------- /cmd/flags/defaults_linux_amd64.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | // DefaultDistros are distributions that can quickly be built in an ideal scenario for the operating system on the above build tag. 4 | var DefaultDistros = []string{"linux/amd64"} 5 | 6 | // DefaultPlatform is the docker platform that will natively / most efficiently run on the OS/arch filtered by the above tag. 7 | var DefaultPlatform = "linux/amd64" 8 | -------------------------------------------------------------------------------- /cmd/flags/defaults_linux_arm64.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | // DefaultDistros are distributions that can quickly be built in an ideal scenario for the operating system on the above build tag. 4 | var DefaultDistros = []string{"linux/arm64"} 5 | 6 | // DefaultPlatform is the docker platform that will natively / most efficiently run on the OS/arch filtered by the above tag. 7 | var DefaultPlatform = "linux/arm64" 8 | -------------------------------------------------------------------------------- /cmd/flags/defaults_windows_amd64.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | // DefaultDistros are distributions that can quickly be built in an ideal scenario for the operating system on the above build tag. 4 | var DefaultDistros = []string{"linux/amd64"} 5 | 6 | // DefaultPlatform is the docker platform that will natively / most efficiently run on the OS/arch filtered by the above tag. 7 | var DefaultPlatform = "linux/amd64" 8 | -------------------------------------------------------------------------------- /cmd/flags/defaults_windows_arm64.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | // DefaultDistros are distributions that can quickly be built in an ideal scenario for the operating system on the above build tag. 4 | var DefaultDistros = []string{"linux/arm64"} 5 | 6 | // DefaultPlatform is the docker platform that will natively / most efficiently run on the OS/arch filtered by the above tag. 7 | var DefaultPlatform = "linux/arm64" 8 | -------------------------------------------------------------------------------- /flags/docker.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import "github.com/grafana/grafana-build/pipeline" 4 | 5 | var ( 6 | Ubuntu pipeline.FlagOption = "docker-ubuntu" 7 | DockerRepositories pipeline.FlagOption = "docker-repos" 8 | ) 9 | 10 | var DockerFlags = []pipeline.Flag{ 11 | { 12 | Name: "ubuntu", 13 | Options: map[pipeline.FlagOption]any{ 14 | Ubuntu: true, 15 | }, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /frontend/storybook.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import "dagger.io/dagger" 4 | 5 | // Storybook returns a dagger.Directory which contains the built storybook server. 6 | func Storybook(builder *dagger.Container, src *dagger.Directory, version string) *dagger.Directory { 7 | return builder. 8 | WithExec([]string{"yarn", "run", "storybook:build"}). 9 | Directory("./packages/grafana-ui/dist/storybook") 10 | } 11 | -------------------------------------------------------------------------------- /stringutil/random.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 9 | 10 | func RandomString(n int) string { 11 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 12 | b := make([]rune, n) 13 | for i := range b { 14 | b[i] = letters[r.Intn(len(letters))] 15 | } 16 | return string(b) 17 | } 18 | -------------------------------------------------------------------------------- /cmd/pro_image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/pipelines" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | var ProImageCommand = &cli.Command{ 9 | Name: "pro-image", 10 | Action: PipelineActionWithPackageInput(pipelines.ProImage), 11 | Description: "Creates a hosted grafana pro image", 12 | Flags: JoinFlagsWithDefault(ProImageFlags, GCPFlags, PackageInputFlags), 13 | } 14 | -------------------------------------------------------------------------------- /arguments/docs.go: -------------------------------------------------------------------------------- 1 | // Package arguments holds globally-defined arguments that are used throughout the program for shared data. 2 | // A good candidate for an argument is a directory whose contents that may be used in the creation of multiple artifacts, like the Grafana source directory. 3 | // Arguments are different than flags; a flag is a boolean argument in an artifact string which can set one or multiple preset values. 4 | package arguments 5 | -------------------------------------------------------------------------------- /fpm/builder.go: -------------------------------------------------------------------------------- 1 | package fpm 2 | 3 | import "dagger.io/dagger" 4 | 5 | const RubyContainer = "ruby:3.2.2-bullseye" 6 | 7 | func Builder(d *dagger.Client) *dagger.Container { 8 | return d.Container(). 9 | From(RubyContainer). 10 | WithEntrypoint(nil). 11 | WithExec([]string{"gem", "install", "fpm"}). 12 | WithExec([]string{"apt-get", "update"}). 13 | WithExec([]string{"apt-get", "install", "-yq", "rpm", "gnupg2"}) 14 | } 15 | -------------------------------------------------------------------------------- /frontend/yarn.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import "dagger.io/dagger" 4 | 5 | func YarnInstall(c *dagger.Client, src *dagger.Directory, version string, cache *dagger.CacheVolume, platform dagger.Platform) *dagger.Container { 6 | return WithYarnCache(NodeContainer(c, NodeImage(version), platform), cache). 7 | WithMountedDirectory("/src", src). 8 | WithWorkdir("/src"). 9 | WithExec([]string{"yarn", "install", "--immutable", "--inline-builds"}) 10 | } 11 | -------------------------------------------------------------------------------- /containers/sha256.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "dagger.io/dagger" 5 | ) 6 | 7 | // Sha256 returns a dagger.File which contains the sha256 for the provided file. 8 | func Sha256(d *dagger.Client, file *dagger.File) *dagger.File { 9 | return d.Container().From("busybox"). 10 | WithFile("/src/file", file). 11 | WithExec([]string{"/bin/sh", "-c", "sha256sum /src/file | awk '{print $1}' > /src/file.sha256"}). 12 | File("/src/file.sha256") 13 | } 14 | -------------------------------------------------------------------------------- /containers/withenv.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "dagger.io/dagger" 5 | ) 6 | 7 | type Env struct { 8 | Name string 9 | Value string 10 | } 11 | 12 | func EnvVar(name, value string) Env { 13 | return Env{Name: name, Value: value} 14 | } 15 | 16 | func WithEnv(c *dagger.Container, env []Env) *dagger.Container { 17 | container := c 18 | for _, v := range env { 19 | container = container.WithEnvVariable(v.Name, v.Value) 20 | } 21 | 22 | return container 23 | } 24 | -------------------------------------------------------------------------------- /docs/artifact-types/docker-image.md: -------------------------------------------------------------------------------- 1 | # Docker image artifact 2 | 3 | ``` 4 | $ dagger run go run ./cmd artifacts -a docker:enterprise:linux/amd64 -a docker:enterprise:linux/amd64:ubuntu 5 | # Produces dist/grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.docker.tar.gz (Alpine) 6 | # Produces dist/grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.ubuntu.docker.tar.gz (Ubuntu) 7 | ``` 8 | 9 | You can then load these files into your Docker engine using the `docker load` command. 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | The `main` branch of `grafana-build` should be compatible with all active versions of Grafana **and Grafana-Enterprise**. To 4 | run integration tests, add a comment to this PR with the following: 5 | 6 | ``` 7 | /grafana-integration-tests 8 | ``` 9 | 10 | * [ ] I have ran the integraiton tests `gh workflow run --ref=${THIS_BRANCH} --repo=grafana/grafana-build pr-integration-tests.yml` 11 | * [ ] All integration tests have passed 12 | 13 | -------------------------------------------------------------------------------- /artifacts/grafana_dir.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "context" 5 | 6 | "dagger.io/dagger" 7 | "github.com/grafana/grafana-build/arguments" 8 | "github.com/grafana/grafana-build/pipeline" 9 | ) 10 | 11 | func GrafanaDir(ctx context.Context, state pipeline.StateHandler, enterprise bool) (*dagger.Directory, error) { 12 | if enterprise { 13 | return state.Directory(ctx, arguments.EnterpriseDirectory) 14 | } 15 | return state.Directory(ctx, arguments.GrafanaDirectory) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/flags/concurrency.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | var ConcurrencyFlags = []cli.Flag{ 10 | &cli.Int64Flag{ 11 | Name: "parallel", 12 | Usage: "The number of parallel pipelines to run. This can be particularly useful for building for multiple distributions at the same time", 13 | DefaultText: "Just like with 'go test', this defaults to GOMAXPROCS", 14 | Value: int64(runtime.GOMAXPROCS(0)), 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | title: Grafana Build 5 | name: grafana-build 6 | description: Tooling for building Grafana 7 | annotations: 8 | github.com/project-slug: grafana/grafana-build 9 | backstage.io/source-location: url:https://github.com/grafana/grafana-build/ 10 | backstage.io/techdocs-ref: dir:. 11 | spec: 12 | type: library 13 | lifecycle: experimental 14 | owner: "group:grafana-release-guild" 15 | system: grafana 16 | -------------------------------------------------------------------------------- /msi/wxs_test.go: -------------------------------------------------------------------------------- 1 | package msi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/grafana-build/msi" 7 | ) 8 | 9 | func TestVersion(t *testing.T) { 10 | tests := map[string]string{ 11 | "1.2.3+security-01": "1.2.3.01", 12 | "1.2.3-beta1": "1.2.3.1", 13 | "1.2.3": "1.2.3.0", 14 | } 15 | 16 | for input, expect := range tests { 17 | res := msi.WxsVersion(input) 18 | if res != expect { 19 | t.Fatalf("for '%s' got '%s', expected '%s'", input, res, expect) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pipelines/publish.go: -------------------------------------------------------------------------------- 1 | package pipelines 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | type SyncWriter struct { 10 | Writer io.Writer 11 | 12 | mutex *sync.Mutex 13 | } 14 | 15 | func NewSyncWriter(w io.Writer) *SyncWriter { 16 | return &SyncWriter{ 17 | Writer: w, 18 | mutex: &sync.Mutex{}, 19 | } 20 | } 21 | 22 | func (w *SyncWriter) Write(b []byte) (int, error) { 23 | w.mutex.Lock() 24 | defer w.mutex.Unlock() 25 | 26 | return w.Writer.Write(b) 27 | } 28 | 29 | var Stdout = NewSyncWriter(os.Stdout) 30 | -------------------------------------------------------------------------------- /containers/ops_gcp.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import "github.com/grafana/grafana-build/cliutil" 4 | 5 | // GCPOpts are options used when using Google Cloud Platform / the Google Cloud SDK. 6 | type GCPOpts struct { 7 | ServiceAccountKey string 8 | ServiceAccountKeyBase64 string 9 | } 10 | 11 | func GCPOptsFromFlags(c cliutil.CLIContext) *GCPOpts { 12 | return &GCPOpts{ 13 | ServiceAccountKeyBase64: c.String("gcp-service-account-key-base64"), 14 | ServiceAccountKey: c.String("gcp-service-account-key"), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /daggerutil/hostdir.go: -------------------------------------------------------------------------------- 1 | package daggerutil 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "dagger.io/dagger" 8 | ) 9 | 10 | // HostDir checks that the directory at 'path' exists and returns the dagger.Directory at 'path'. 11 | func HostDir(d *dagger.Client, path string) (*dagger.Directory, error) { 12 | info, err := os.Stat(path) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | if !info.IsDir() { 18 | return nil, errors.New("given hostdir is not a directory") 19 | } 20 | 21 | return d.Host().Directory(path), nil 22 | } 23 | -------------------------------------------------------------------------------- /cmd/npm_publish.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/pipelines" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | var PublishNPMCommand = &cli.Command{ 9 | Name: "publish", 10 | Action: PipelineActionWithPackageInput(pipelines.PublishNPM), 11 | Usage: "Using a grafana.tar.gz as input (ideally one built using the 'package' command), take the npm artifacts and publish them on NPM.", 12 | Flags: JoinFlagsWithDefault( 13 | PackageInputFlags, 14 | NPMFlags, 15 | GCPFlags, 16 | ConcurrencyFlags, 17 | ), 18 | } 19 | -------------------------------------------------------------------------------- /git/container_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInjectURLCredentials(t *testing.T) { 8 | expected := "https://username:password@example.org/somepath?query=param" 9 | input := "https://example.org/somepath?query=param" 10 | output, err := injectURLCredentials(input, "username", "password") 11 | if err != nil { 12 | t.Fatal("Unexpected error from injectURLCredentials:", err) 13 | } 14 | if expected != output { 15 | t.Fatalf("Unexpected output. Expected '%s', got '%s'", expected, output) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /zip/builder.go: -------------------------------------------------------------------------------- 1 | package zip 2 | 3 | import "dagger.io/dagger" 4 | 5 | func Builder(d *dagger.Client) *dagger.Container { 6 | return d.Container().From("alpine"). 7 | WithExec([]string{"apk", "add", "--update", "zip", "tar"}) 8 | } 9 | 10 | func Build(c *dagger.Container, targz *dagger.File) *dagger.File { 11 | return c.WithFile("/src/grafana.tar.gz", targz). 12 | WithExec([]string{"/bin/sh", "-c", "tar xzf /src/grafana.tar.gz"}). 13 | WithExec([]string{"/bin/sh", "-c", "zip /src/grafana.zip $(tar tf /src/grafana.tar.gz)"}). 14 | File("/src/grafana.zip") 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/publish-techdocs.yml: -------------------------------------------------------------------------------- 1 | # This workflow calls a reusable workflow to publish TechDocs to the Backstage ops GCS bucket. 2 | name: Publish TechDocs 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' 9 | - 'mkdocs.yml' 10 | - '.github/workflows/publish-techdocs.yml' 11 | 12 | jobs: 13 | publish-docs: 14 | uses: grafana/shared-workflows/.github/workflows/publish-techdocs.yaml@main 15 | secrets: inherit 16 | with: 17 | namespace: default 18 | kind: component 19 | name: grafana-build 20 | -------------------------------------------------------------------------------- /cmd/gcom_publish.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/pipelines" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | var GCOMPublishCommand = &cli.Command{ 9 | Name: "publish", 10 | Action: PipelineActionWithPackageInput(pipelines.PublishGCOM), 11 | Description: "Publishes a grafana.tar.gz (ideally one built using the 'package' command) to grafana.com (--destination will be the download path)", 12 | Flags: JoinFlagsWithDefault( 13 | GCOMFlags, 14 | PackageInputFlags, 15 | PublishFlags, 16 | ConcurrencyFlags, 17 | ), 18 | } 19 | -------------------------------------------------------------------------------- /cmd/package_publish.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/pipelines" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | var PackagePublishCommand = &cli.Command{ 9 | Name: "publish", 10 | Action: PipelineActionWithPackageInput(pipelines.PublishPackage), 11 | Description: "Publishes a grafana.tar.gz (ideally one built using the 'package' command) in the destination directory (--destination)", 12 | Flags: JoinFlagsWithDefault( 13 | PackageInputFlags, 14 | PublishFlags, 15 | GCPFlags, 16 | ConcurrencyFlags, 17 | ), 18 | } 19 | -------------------------------------------------------------------------------- /cmd/docker_publish.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/pipelines" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | var DockerPublishCommand = &cli.Command{ 9 | Name: "publish", 10 | Action: PipelineActionWithPackageInput(pipelines.PublishDocker), 11 | Usage: "Using a grafana.docker.tar.gz as input (ideally one built using the 'package' command), publish a docker image and manifest", 12 | Flags: JoinFlagsWithDefault( 13 | PackageInputFlags, 14 | DockerFlags, 15 | DockerPublishFlags, 16 | GCPFlags, 17 | ConcurrencyFlags, 18 | ), 19 | } 20 | -------------------------------------------------------------------------------- /cmd/flags/publish.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | var PublishFlags = []cli.Flag{ 6 | &cli.StringFlag{ 7 | Name: "destination", 8 | Usage: "full URL to upload the artifacts to (examples: '/tmp/package.tar.gz', 'file://package.tar.gz', 'file:///tmp/package.tar.gz', 'gs://bucket/grafana/')", 9 | Aliases: []string{"d"}, 10 | Value: "dist", 11 | }, 12 | &cli.BoolFlag{ 13 | Name: "checksum", 14 | Usage: "When enabled, also creates a `.sha256' checksum file in the destination that matches the checksum of the artifact(s) produced", 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /flags/docs.go: -------------------------------------------------------------------------------- 1 | // Package flags defines the "flags" that are used in various artifacts throughout the application. 2 | // A flag is an artifact-specific string alias to a set of options. 3 | // Examples: 4 | // - the 'boringcrypto' flag, when used in an artifact string like `boringcrypto:targz:linux/amd64`, informs the `targz` artifact that 5 | // the package name is 'grafana-boringcrypto', and that when it is built, the GOEXPERIMENT=boringcrypto flag must be set. 6 | // - the 'targz' flag forces the use of the 'targz' artifact, whose exention will end in `tar.gz`, and will require the compiled 'backend' and 'frontend'. 7 | package flags 8 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | unpinned-uses: 3 | config: 4 | policies: 5 | "*": hash-pin 6 | actions/*: any 7 | github/*: any 8 | grafana/*: any 9 | forbidden-uses: 10 | config: 11 | deny: 12 | # Policy-banned by our security team due to CVE-2025-30066 & CVE-2025-30154. 13 | # https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction 14 | # https://nvd.nist.gov/vuln/detail/cve-2025-30066 15 | # https://nvd.nist.gov/vuln/detail/cve-2025-30154 16 | - reviewdog/* 17 | -------------------------------------------------------------------------------- /ruleguard.rules.go: -------------------------------------------------------------------------------- 1 | //go:build ruleguard 2 | 3 | package gorules 4 | 5 | import "github.com/quasilyte/go-ruleguard/dsl" 6 | 7 | //doc:summary *cli.Context instances should have the variable name `c` or `cliCtx` 8 | func correctNameForCLIContext(m dsl.Matcher) { 9 | m.Import("github.com/urfave/cli/v2") 10 | m.Match( 11 | `func $_($varname $vartype) error { $*_ }`, 12 | `func ($_ $_) $_($varname $vartype) error { $*_ }`, 13 | ). 14 | Where(m["vartype"].Type.Is("*v2.Context") && (m["varname"].Text != "c" && m["varname"].Text != "cliCtx")). 15 | Report("*cli.Context arguments should have the name c or cliCtx but was $varname") 16 | } 17 | -------------------------------------------------------------------------------- /containers/extracted_package.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import "dagger.io/dagger" 4 | 5 | // ExtractedActive returns a directory that holds an extracted tar.gz 6 | func ExtractedArchive(d *dagger.Client, f *dagger.File) *dagger.Directory { 7 | return d.Container().From("busybox"). 8 | // Workaround for now (maybe unnecessary?): set a FILE environment variable so that we don't accidentally cache 9 | WithFile("/src/archive.tar.gz", f). 10 | WithExec([]string{"mkdir", "-p", "/src/archive"}). 11 | WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/archive.tar.gz", "-C", "/src/archive"}). 12 | Directory("/src/archive") 13 | } 14 | -------------------------------------------------------------------------------- /gpg/verify.go: -------------------------------------------------------------------------------- 1 | package gpg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "dagger.io/dagger" 8 | "github.com/grafana/grafana-build/containers" 9 | ) 10 | 11 | func VerifySignature(ctx context.Context, d *dagger.Client, file *dagger.File, pubKey, privKey, passphrase string) error { 12 | container := Signer(d, pubKey, privKey, passphrase). 13 | WithFile("/src/package.rpm", file). 14 | WithExec([]string{"/bin/sh", "-c", "rpm --checksig /src/package.rpm"}) 15 | 16 | if _, err := containers.ExitError(ctx, container); err != nil { 17 | return fmt.Errorf("failed to validate gpg signature for rpm package: %w", err) 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /artifacts/sync_writer.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | // SyncWriter wraps a writer and makes its writes synchronous, preventing multiple threads writing to the same writer 10 | // from creating wacky looking output. 11 | type SyncWriter struct { 12 | Writer io.Writer 13 | 14 | mutex *sync.Mutex 15 | } 16 | 17 | func NewSyncWriter(w io.Writer) *SyncWriter { 18 | return &SyncWriter{ 19 | Writer: w, 20 | mutex: &sync.Mutex{}, 21 | } 22 | } 23 | 24 | func (w *SyncWriter) Write(b []byte) (int, error) { 25 | w.mutex.Lock() 26 | defer w.mutex.Unlock() 27 | 28 | return w.Writer.Write(b) 29 | } 30 | 31 | var Stdout = NewSyncWriter(os.Stdout) 32 | -------------------------------------------------------------------------------- /containers/test_backend.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | // func BackendTestShort(d *dagger.Client, platform dagger.Platform, dir *dagger.Directory) *dagger.Container { 4 | // return GrafanaContainer(d, platform, GetGoImageAlpine("1.21.0"), dir). 5 | // WithExec([]string{"go", "test", "-tags", "requires_buildifer", "-short", "-covermode", "atomic", "-timeout", "5m", "./pkg/..."}) 6 | // } 7 | // 8 | // func BackendTestIntegration(d *dagger.Client, platform dagger.Platform, dir *dagger.Directory) *dagger.Container { 9 | // return GrafanaContainer(d, platform, GetGoImageAlpine("1.21.0"), dir). 10 | // WithExec([]string{"go", "test", "-run", "Integration", "-covermode", "atomic", "-timeout", "5m", "./pkg/..."}) 11 | // } 12 | -------------------------------------------------------------------------------- /containers/version.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "dagger.io/dagger" 9 | ) 10 | 11 | // GetJSONValue gets the value of a JSON field from a JSON file in the 'src' directory. 12 | func GetJSONValue(ctx context.Context, d *dagger.Client, src *dagger.Directory, file string, field string) (string, error) { 13 | c := d.Container().From("alpine"). 14 | WithExec([]string{"apk", "--update", "add", "jq"}). 15 | WithMountedDirectory("/src", src). 16 | WithWorkdir("/src"). 17 | WithExec([]string{"/bin/sh", "-c", fmt.Sprintf("cat %s | jq -r .%s", file, field)}) 18 | 19 | if stdout, err := c.Stdout(ctx); err == nil { 20 | return strings.TrimSpace(stdout), nil 21 | } 22 | 23 | return c.Stderr(ctx) 24 | } 25 | -------------------------------------------------------------------------------- /scripts/move_packages_storybook_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var storybookMapping = map[string]m{ 4 | "OSS": { 5 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_amd64/storybook", 6 | output: []string{ 7 | "artifacts/storybook/v1.2.3", 8 | }, 9 | env: map[string]string{"DRONE_TAG": "1.2.3"}, 10 | }, 11 | "ENT": { 12 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_amd64/storybook", 13 | output: []string{ 14 | "artifacts/storybook/v1.2.3", 15 | }, 16 | env: map[string]string{"DRONE_TAG": "1.2.3"}, 17 | }, 18 | "PRO": { 19 | input: "gs://bucket/tag/grafana-pro_v1.2.3_102_linux_amd64/storybook", 20 | output: []string{ 21 | "artifacts/storybook/v1.2.3", 22 | }, 23 | env: map[string]string{"DRONE_TAG": "1.2.3"}, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: {} 7 | 8 | permissions: 9 | contents: read 10 | id-token: write 11 | 12 | jobs: 13 | publish-docker-image: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out 17 | uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3 18 | - id: push-to-dockerhub 19 | name: Build and push 20 | uses: grafana/shared-workflows/actions/build-push-to-dockerhub@main 21 | with: 22 | context: . 23 | repository: ${{ github.repository }} 24 | push: ${{ github.event_name != 'pull_request' }} 25 | tags: | 26 | type=raw,value=latest 27 | type=raw,value=main 28 | 29 | -------------------------------------------------------------------------------- /arguments/golang.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/pipeline" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | const ( 9 | DefaultGoVersion = "1.23.1" 10 | DefaultViceroyVersion = "v0.4.0" 11 | ) 12 | 13 | var GoVersionFlag = &cli.StringFlag{ 14 | Name: "go-version", 15 | Usage: "The Go version to use when compiling Grafana", 16 | Value: DefaultGoVersion, 17 | } 18 | 19 | var GoVersion = pipeline.NewStringFlagArgument(GoVersionFlag) 20 | 21 | var ViceroyVersionFlag = &cli.StringFlag{ 22 | Name: "viceroy-version", 23 | Usage: "This flag sets the base image of the container used to build the Grafana backend binaries for non-Linux distributions", 24 | Value: DefaultViceroyVersion, 25 | } 26 | 27 | var ViceroyVersion = pipeline.NewStringFlagArgument(ViceroyVersionFlag) 28 | -------------------------------------------------------------------------------- /scripts/move_packages_zip_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var zipMapping = map[string]m{ 4 | "OSS: Windows AMD64": { 5 | input: "gs://bucket/tag/grafana_v1.2.3-test.1_102_windows_amd64.zip", 6 | output: []string{ 7 | "artifacts/downloads/v1.2.3-test.1/oss/release/grafana-1.2.3-test.1.windows-amd64.zip", 8 | }, 9 | }, 10 | "OSS: Windows AMD64 from file://": { 11 | input: "file://bucket/tag/grafana_v1.2.3-test.1_102_windows_amd64.zip", 12 | output: []string{ 13 | "artifacts/downloads/v1.2.3-test.1/oss/release/grafana-1.2.3-test.1.windows-amd64.zip", 14 | }, 15 | }, 16 | "OSS: Windows AMD64 main from file://": { 17 | input: "file://bucket/tag/grafana_main_102_windows_amd64.zip", 18 | output: []string{ 19 | "artifacts/downloads/main/oss/release/grafana-main.windows-amd64.zip", 20 | }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /golang/cache.go: -------------------------------------------------------------------------------- 1 | package golang 2 | 3 | import ( 4 | "fmt" 5 | 6 | "dagger.io/dagger" 7 | ) 8 | 9 | func DownloadURL(version, arch string) string { 10 | return fmt.Sprintf("https://go.dev/dl/go%s.linux-%s.tar.gz", version, arch) 11 | } 12 | 13 | func Container(d *dagger.Client, platform dagger.Platform, version string) *dagger.Container { 14 | opts := dagger.ContainerOpts{ 15 | Platform: platform, 16 | } 17 | 18 | goImage := fmt.Sprintf("golang:%s-alpine", version) 19 | 20 | return d.Container(opts).From(goImage) 21 | } 22 | 23 | func WithCachedGoDependencies(container *dagger.Container, cache *dagger.CacheVolume) *dagger.Container { 24 | return container. 25 | WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). 26 | WithMountedCache("/go/pkg/mod", cache). 27 | WithExec([]string{"ls", "-al", "/go/pkg/mod"}) 28 | } 29 | -------------------------------------------------------------------------------- /containers/exit_error.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "dagger.io/dagger" 9 | ) 10 | 11 | var ( 12 | ErrorNonZero = errors.New("container exited with non-zero exit code") 13 | ) 14 | 15 | // ExitError functionally replaces '(*container).ExitCode' in a more usable way. 16 | // It will return an error with the container's stderr and stdout if the exit code is not zero. 17 | func ExitError(ctx context.Context, container *dagger.Container) (*dagger.Container, error) { 18 | container, err := container.Sync(ctx) 19 | if err == nil { 20 | return container, nil 21 | } 22 | 23 | var e *dagger.ExecError 24 | if errors.As(err, &e) { 25 | return container, fmt.Errorf("%w\nstdout: %s\nstderr: %s", ErrorNonZero, e.Stdout, e.Stderr) 26 | } 27 | 28 | return container, err 29 | } 30 | -------------------------------------------------------------------------------- /gcom/opts.go: -------------------------------------------------------------------------------- 1 | package gcom 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/grafana/grafana-build/cliutil" 7 | ) 8 | 9 | // GCOMOpts are options used when making requests to grafana.com. 10 | type GCOMOpts struct { 11 | URL *url.URL 12 | DownloadURL *url.URL 13 | ApiKey string 14 | Beta bool 15 | Nightly bool 16 | } 17 | 18 | func GCOMOptsFromFlags(c cliutil.CLIContext) (*GCOMOpts, error) { 19 | apiUrl, err := url.Parse(c.String("api-url")) 20 | if err != nil { 21 | return nil, err 22 | } 23 | downloadUrl, err := url.Parse(c.String("download-url")) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &GCOMOpts{ 28 | URL: apiUrl, 29 | DownloadURL: downloadUrl, 30 | ApiKey: c.String("api-key"), 31 | Beta: c.Bool("beta"), 32 | Nightly: c.Bool("nightly"), 33 | }, nil 34 | } 35 | -------------------------------------------------------------------------------- /cmd/artifacts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/artifacts" 5 | ) 6 | 7 | var Artifacts = map[string]artifacts.Initializer{ 8 | "backend": artifacts.BackendInitializer, 9 | "frontend": artifacts.FrontendInitializer, 10 | "npm": artifacts.NPMPackagesInitializer, 11 | "targz": artifacts.TargzInitializer, 12 | "zip": artifacts.ZipInitializer, 13 | "deb": artifacts.DebInitializer, 14 | "rpm": artifacts.RPMInitializer, 15 | "docker": artifacts.DockerInitializer, 16 | "docker-pro": artifacts.ProDockerInitializer, 17 | "docker-enterprise": artifacts.EntDockerInitializer, 18 | "storybook": artifacts.StorybookInitializer, 19 | "msi": artifacts.MSIInitializer, 20 | "version": artifacts.VersionInitializer, 21 | } 22 | -------------------------------------------------------------------------------- /containers/with_embedded_fs.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "path/filepath" 7 | 8 | "dagger.io/dagger" 9 | ) 10 | 11 | func WithEmbeddedFS(client *dagger.Client, c *dagger.Container, path string, e embed.FS) (*dagger.Container, error) { 12 | dir := client.Directory() 13 | 14 | err := fs.WalkDir(e, ".", func(path string, entry fs.DirEntry, err error) error { 15 | if entry.IsDir() { 16 | return nil 17 | } 18 | if err != nil { 19 | return err 20 | } 21 | 22 | content, err := e.ReadFile(path) 23 | if err != nil { 24 | return err 25 | } 26 | rel, err := filepath.Rel("scripts/packaging/windows", path) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | dir = dir.WithNewFile(rel, string(content)) 32 | return nil 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return c.WithDirectory(path, dir), nil 39 | } 40 | -------------------------------------------------------------------------------- /e2e/validate_license.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "dagger.io/dagger" 9 | ) 10 | 11 | // validateLicense uses the given container and license path to validate the license for each edition (enterprise or oss) 12 | func ValidateLicense(ctx context.Context, service *dagger.Container, licensePath string, enterprise bool) error { 13 | license, err := service.File(licensePath).Contents(ctx) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | if enterprise { 19 | if !strings.Contains(license, "Grafana Enterprise") { 20 | return fmt.Errorf("license in package is not the Grafana Enterprise license agreement") 21 | } 22 | 23 | return nil 24 | } 25 | 26 | if !strings.Contains(license, "GNU AFFERO GENERAL PUBLIC LICENSE") { 27 | return fmt.Errorf("license in package is not the Grafana open-source license agreement") 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /docs/artifact-types/rpm.md: -------------------------------------------------------------------------------- 1 | # RPM artifact (.rpm) 2 | 3 | ``` 4 | $ dagger run go run ./cmd artifacts -a rpm:enterprise:linux/amd64 5 | # Produces dist/grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.rpm (Unisnged) 6 | 7 | $ dagger run go run ./cmd artifacts -a rpm:enterprise:linux/amd64:sign 8 | # Produces dist/grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.rpm (Unisnged) 9 | ``` 10 | 11 | If `GPG_PRIVATE_KEY`, `GPG_PUBLIC_KEY`, `GPG_PASSPHRASE` environment variables are set (and are base64 encoded), then the RPM will be signed if the `:sign` flag is added. 12 | 13 | Example: 14 | 15 | ``` 16 | export GPG_PRIVATE_KEY=$(cat ./key.private | base64 -w 0) 17 | export GPG_PUBLIC_KEY=$(cat ./key.public | base64 -w 0) 18 | export GPG_PASSPHRASE=grafana 19 | 20 | 21 | dagger run go run ./cmd artifacts -a rpm:enterprise:linux/amd64:sign 22 | # Produces dist/grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.rpm (Signed) 23 | ``` 24 | -------------------------------------------------------------------------------- /pipelines/package_publish.go: -------------------------------------------------------------------------------- 1 | package pipelines 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "dagger.io/dagger" 10 | "github.com/grafana/grafana-build/containers" 11 | ) 12 | 13 | // PublishPackage takes one or multiple grafana.tar.gz as input and publishes it to a set destination. 14 | func PublishPackage(ctx context.Context, d *dagger.Client, args PipelineArgs) error { 15 | packages, err := containers.GetPackages(ctx, d, args.PackageInputOpts, args.GCPOpts) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | c := d.Container().From("alpine") 21 | for i, name := range args.PackageInputOpts.Packages { 22 | c = c.WithFile("/dist/"+filepath.Base(name), packages[i]) 23 | } 24 | 25 | dst, err := containers.PublishDirectory(ctx, d, c.Directory("dist"), args.GCPOpts, args.PublishOpts.Destination) 26 | if err != nil { 27 | return err 28 | } 29 | fmt.Fprintln(os.Stdout, dst) 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/equinix-labs/otel-cli:v0.4.5 as otel-cli 2 | 3 | FROM alpine:3.20 AS dagger 4 | 5 | # TODO: pull the binary from registry.dagger.io/cli:v0.9.8 (or similar) when 6 | # https://github.com/dagger/dagger/issues/6887 is resolved 7 | ARG DAGGER_VERSION=v0.13.3 8 | ADD https://github.com/dagger/dagger/releases/download/${DAGGER_VERSION}/dagger_${DAGGER_VERSION}_linux_amd64.tar.gz /tmp 9 | RUN tar zxf /tmp/dagger_${DAGGER_VERSION}_linux_amd64.tar.gz -C /tmp 10 | RUN mv /tmp/dagger /bin/dagger 11 | 12 | FROM golang:1.23-alpine 13 | 14 | ARG DAGGER_VERSION=v0.13.3 15 | 16 | WORKDIR /src 17 | RUN apk add --no-cache git wget bash jq 18 | RUN apk add --no-cache docker --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community 19 | 20 | COPY --from=otel-cli /otel-cli /usr/bin/otel-cli 21 | COPY --from=dagger /bin/dagger /bin/dagger 22 | 23 | ADD . . 24 | RUN go build -o /src/grafana-build ./cmd 25 | 26 | ENTRYPOINT ["dagger", "run", "/src/grafana-build"] 27 | -------------------------------------------------------------------------------- /arguments/gpg.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/pipeline" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | var ( 9 | GPGPublicKeyFlag = &cli.StringFlag{ 10 | Name: "gpg-public-key-base64", 11 | Usage: "Provides a public key encoded in base64 for GPG signing", 12 | EnvVars: []string{"GPG_PUBLIC_KEY"}, 13 | } 14 | GPGPrivateKeyFlag = &cli.StringFlag{ 15 | Name: "gpg-private-key-base64", 16 | Usage: "Provides a private key encoded in base64 for GPG signing", 17 | EnvVars: []string{"GPG_PRIVATE_KEY"}, 18 | } 19 | GPGPassphraseFlag = &cli.StringFlag{ 20 | Name: "gpg-passphrase", 21 | Usage: "Provides a private key passphrase encoded in base64 for GPG signing", 22 | EnvVars: []string{"GPG_PASSPHRASE"}, 23 | } 24 | 25 | GPGPublicKey = pipeline.NewStringFlagArgument(GPGPublicKeyFlag) 26 | GPGPrivateKey = pipeline.NewStringFlagArgument(GPGPrivateKeyFlag) 27 | GPGPassphrase = pipeline.NewStringFlagArgument(GPGPassphraseFlag) 28 | ) 29 | -------------------------------------------------------------------------------- /scripts/move_packages_exe_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var exeMapping = map[string]m{ 4 | "ENT": { 5 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_windows_amd64.exe", 6 | output: []string{ 7 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3.windows-amd64.exe", 8 | }, 9 | }, 10 | "ENT SHA256": { 11 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_windows_amd64.exe.sha256", 12 | output: []string{ 13 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3.windows-amd64.exe.sha256", 14 | }, 15 | }, 16 | "OSS": { 17 | input: "gs://bucket/tag/grafana_v1.2.3_102_windows_amd64.exe", 18 | output: []string{ 19 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3.windows-amd64.exe", 20 | }, 21 | }, 22 | "OSS SHA256": { 23 | input: "gs://bucket/tag/grafana_v1.2.3_102_windows_amd64.exe.sha256", 24 | output: []string{ 25 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3.windows-amd64.exe.sha256", 26 | }, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /scripts/move_packages_msi_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var msiMapping = map[string]m{ 4 | "ENT": { 5 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_windows_amd64.msi", 6 | output: []string{ 7 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3.windows-amd64.msi", 8 | }, 9 | }, 10 | "ENT SHA256": { 11 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_windows_amd64.msi.sha256", 12 | output: []string{ 13 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3.windows-amd64.msi.sha256", 14 | }, 15 | }, 16 | "OSS": { 17 | input: "gs://bucket/tag/grafana_v1.2.3_102_windows_amd64.msi", 18 | output: []string{ 19 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3.windows-amd64.msi", 20 | }, 21 | }, 22 | "OSS SHA256": { 23 | input: "gs://bucket/tag/grafana_v1.2.3_102_windows_amd64.msi.sha256", 24 | output: []string{ 25 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3.windows-amd64.msi.sha256", 26 | }, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: {} 5 | 6 | jobs: 7 | golang-ci: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | id-token: write 11 | contents: read 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: stable 17 | cache: true 18 | - uses: golangci/golangci-lint-action@d6238b002a20823d52840fda27e2d4891c5952dc 19 | with: 20 | version: latest 21 | args: --max-same-issues=0 --max-issues-per-linter=0 --verbose 22 | only-new-issues: true 23 | skip-cache: true 24 | install-mode: binary 25 | go-test: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | id-token: write 29 | contents: read 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-go@v5 33 | with: 34 | go-version: stable 35 | cache: true 36 | - run: "go test ./... -v" 37 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | docs_dir: docs 2 | edit_uri: edit/main/docs/ 3 | markdown_extensions: 4 | - pymdownx.superfences: 5 | custom_fences: 6 | - name: mermaid 7 | class: mermaid 8 | format: !!python/name:pymdownx.superfences.fence_code_format 9 | nav: 10 | - index.md 11 | - "Why Dagger?": why-dagger.md 12 | - "Guides": 13 | - guides/building.md 14 | - guides/tracing.md 15 | - "Artifact types": 16 | - "Overview": artifact-types/index.md 17 | - "Tarball": artifact-types/tarball.md 18 | - "RPM": artifact-types/rpm.md 19 | - "Debian": artifact-types/deb.md 20 | - "Windows installer": artifact-types/windows-installer.md 21 | - "Docker image": artifact-types/docker-image.md 22 | - "ZIP": artifact-types/zip.md 23 | - "Meta": 24 | - meta/docs.md 25 | repo_url: https://github.com/grafana/grafana-build 26 | site_name: Grafana Build 27 | theme: 28 | features: 29 | - content.action.edit 30 | - content.code.copy 31 | - navigation.footer 32 | name: material 33 | -------------------------------------------------------------------------------- /artifacts/parse_args_test.go: -------------------------------------------------------------------------------- 1 | package artifacts_test 2 | 3 | // var TestArtifact struct { 4 | // } 5 | // 6 | // func TestParse(t *testing.T) { 7 | // v := "artifact:flag1:flag2" 8 | // 9 | // exampleArtifact := &pipeline.Artifact{ 10 | // Name: "example", 11 | // } 12 | // 13 | // argument1 := &pipeline.Argument{ 14 | // Name: "argument1", 15 | // } 16 | // 17 | // argument2 := &pipeline.Argument{ 18 | // Name: "argument2", 19 | // } 20 | // 21 | // res, err := artifacts.Parse(v, map[string]artifacts.ArgumentOption{ 22 | // "artifact": {Artifact: exampleArtifact}, 23 | // "argument1": {Arguments: []*pipeline.Argument{argument1}}, 24 | // "argument2": {Arguments: []*pipeline.Argument{argument2}}, 25 | // }) 26 | // 27 | // if err != nil { 28 | // t.Fatal(err) 29 | // } 30 | // 31 | // if res.Artifact.Name != exampleArtifact.Name { 32 | // t.Fatal("Parse should return the example artifact") 33 | // } 34 | // 35 | // if len(res.Arguments) != 2 { 36 | // t.Fatal("Parse should return 2 Arguments") 37 | // } 38 | // } 39 | -------------------------------------------------------------------------------- /scripts/move_packages_cdn_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var cdnMapping = map[string]m{ 4 | "OSS: Linux AMD64": { 5 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_amd64/public", 6 | output: []string{ 7 | "artifacts/static-assets/grafana/1.2.3/public", 8 | }, 9 | env: map[string]string{"DRONE_TAG": "1.2.3"}, 10 | }, 11 | "ENT: Linux AMD64": { 12 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_amd64/public", 13 | output: []string{ 14 | "artifacts/static-assets/grafana/1.2.3/public", 15 | }, 16 | env: map[string]string{"DRONE_TAG": "1.2.3"}, 17 | }, 18 | "PRO: Linux AMD64": { 19 | input: "gs://bucket/tag/grafana-pro_v1.2.3_102_linux_amd64/public", 20 | output: []string{ 21 | "artifacts/static-assets/grafana/1.2.3/public", 22 | }, 23 | env: map[string]string{"DRONE_TAG": "1.2.3"}, 24 | }, 25 | "main": { 26 | input: "dist/10.3.0-62960/grafana-enterprise/public", 27 | output: []string{ 28 | "grafana/10.3.0-62960/public", 29 | }, 30 | env: map[string]string{"IS_MAIN": "true"}, 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /msi/builder.go: -------------------------------------------------------------------------------- 1 | package msi 2 | 3 | import ( 4 | "dagger.io/dagger" 5 | "github.com/grafana/grafana-build/containers" 6 | ) 7 | 8 | func Builder(d *dagger.Client) (*dagger.Container, error) { 9 | nssm := d.Container().From("busybox"). 10 | WithExec([]string{"wget", "https://nssm.cc/release/nssm-2.24.zip"}). 11 | WithExec([]string{"unzip", "nssm-2.24.zip"}). 12 | Directory("nssm-2.24") 13 | 14 | wix3 := d.Container().From("busybox"). 15 | WithWorkdir("/src"). 16 | WithExec([]string{"wget", "https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip"}). 17 | WithExec([]string{"unzip", "wix314-binaries.zip"}). 18 | WithExec([]string{"rm", "wix314-binaries.zip"}). 19 | Directory("/src") 20 | 21 | builder := d.Container().From("scottyhardy/docker-wine:stable-9.0"). 22 | WithEntrypoint([]string{}). 23 | WithMountedDirectory("/src/nssm-2.24", nssm). 24 | WithMountedDirectory("/src/wix3", wix3). 25 | WithWorkdir("/src") 26 | 27 | return containers.WithEmbeddedFS(d, builder, "/src/resources", resources) 28 | } 29 | -------------------------------------------------------------------------------- /docs/guides/tracing.md: -------------------------------------------------------------------------------- 1 | # Tracing with OpenTelemetry 2 | 3 | grafana-build also supports OpenTelemetry for tracing using either `http/protobuf` or `grpc` as protocols. 4 | 5 | ``` 6 | # Enable gRPC exporter (explicit via protocol) 7 | export OTEL_EXPORTER_OTLP_PROTOCOL=grpc 8 | export OTEL_EXPORTER_OTLP_ENDPOINT=https://tempo-somewhere.grafana.net:443 9 | export OTEL_EXPORTER_OTLP_HEADERS=Authorization=... 10 | 11 | # Enable gRPC exporter (implicit via no protocol in endpoint) 12 | export OTEL_EXPORTER_OTLP_ENDPOINT=tempo-somewhere.grafana.net:443 13 | export OTEL_EXPORTER_OTLP_HEADERS=Authorization=... 14 | 15 | # Enable HTTP exporter (explicit via protocol) 16 | export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf 17 | export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-somewhere.grafana.net/otlp 18 | export OTEL_EXPORTER_OTLP_HEADERS=Authorization=... 19 | 20 | # Enable HTTP exporter (implicit via protocol in endpoint) 21 | export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-somewhere.grafana.net/otlp 22 | export OTEL_EXPORTER_OTLP_HEADERS=Authorization=... 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /containers/publish.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "errors" 5 | 6 | "dagger.io/dagger" 7 | "github.com/grafana/grafana-build/cliutil" 8 | ) 9 | 10 | // PublishOpts fields are selectively used based on the protocol field of the destination. 11 | // Be sure to fill out the applicable fields (or all of them) when calling a 'Publish' func. 12 | type PublishOpts struct { 13 | // Destination is any URL to publish an artifact(s) to. 14 | // Examples: 15 | // * '/tmp/package.tar.gz' 16 | // * 'file:///tmp/package.tar.gz' 17 | // * 'gcs://bucket/package.tar.gz' 18 | Destination string 19 | 20 | // Checksum defines if the PublishFile function should also produce / publish a checksum of the given `*dagger.File' 21 | Checksum bool 22 | } 23 | 24 | func PublishOptsFromFlags(c cliutil.CLIContext) *PublishOpts { 25 | return &PublishOpts{ 26 | Destination: c.String("destination"), 27 | Checksum: c.Bool("checksum"), 28 | } 29 | } 30 | 31 | var ErrorUnrecognizedScheme = errors.New("unrecognized scheme") 32 | 33 | type PublishFileOpts struct { 34 | File *dagger.File 35 | PublishOpts *PublishOpts 36 | GCPOpts *GCPOpts 37 | Destination string 38 | } 39 | -------------------------------------------------------------------------------- /e2e/validate_package.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "dagger.io/dagger" 5 | "github.com/grafana/grafana-build/frontend" 6 | ) 7 | 8 | func CypressImage(version string) string { 9 | return "cypress/included:13.1.0" 10 | } 11 | 12 | // CypressContainer returns a docker container with everything set up that is needed to build or run e2e tests. 13 | func CypressContainer(d *dagger.Client, base string) *dagger.Container { 14 | container := d.Container().From(base).WithEntrypoint([]string{}) 15 | 16 | return container 17 | } 18 | 19 | func ValidatePackage(d *dagger.Client, service *dagger.Service, src *dagger.Directory, yarnCacheVolume *dagger.CacheVolume, nodeVersion string) *dagger.Container { 20 | // The cypress container should never be cached 21 | c := CypressContainer(d, CypressImage(nodeVersion)) 22 | 23 | c = frontend.WithYarnCache(c, yarnCacheVolume) 24 | 25 | return c.WithDirectory("/src", src). 26 | WithWorkdir("/src"). 27 | WithServiceBinding("grafana", service). 28 | WithEnvVariable("HOST", "grafana"). 29 | WithEnvVariable("PORT", "3000"). 30 | WithExec([]string{"yarn", "install", "--immutable"}). 31 | WithExec([]string{"/bin/sh", "-c", "/src/e2e/verify-release"}) 32 | } 33 | -------------------------------------------------------------------------------- /frontend/node.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "dagger.io/dagger" 8 | ) 9 | 10 | // NodeVersionContainer returns a container whose `stdout` will return the node version from the '.nvmrc' file in the directory 'src'. 11 | func NodeVersion(d *dagger.Client, src *dagger.Directory) *dagger.Container { 12 | return d.Container().From("alpine:3.17"). 13 | WithMountedDirectory("/src", src). 14 | WithWorkdir("/src"). 15 | WithExec([]string{"cat", ".nvmrc"}) 16 | } 17 | 18 | func NodeImage(version string) string { 19 | return fmt.Sprintf("node:%s-slim", strings.TrimPrefix(strings.TrimSpace(version), "v")) 20 | } 21 | 22 | // NodeContainer returns a docker container with everything set up that is needed to build or run frontend tests. 23 | func NodeContainer(d *dagger.Client, base string, platform dagger.Platform) *dagger.Container { 24 | container := d.Container(dagger.ContainerOpts{ 25 | Platform: platform, 26 | }).From(base). 27 | WithExec([]string{"apt-get", "update", "-yq"}). 28 | WithExec([]string{"apt-get", "install", "-yq", "make", "git", "g++", "python3"}). 29 | WithEnvVariable("NODE_OPTIONS", "--max_old_space_size=8000") 30 | 31 | return container 32 | } 33 | -------------------------------------------------------------------------------- /scripts/drone_publish_nightly_enterprise.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | local_dir="${DRONE_WORKSPACE}/dist" 4 | 5 | # Publish the docker images present in the bucket 6 | dagger run --silent go run ./cmd docker publish \ 7 | $(find $local_dir | grep docker.tar.gz | grep -v sha256 | awk '{print "--package=file://"$0}') \ 8 | --username=${DOCKER_USERNAME} \ 9 | --password=${DOCKER_PASSWORD} \ 10 | --latest \ 11 | --repo="grafana-enterprise-dev" 12 | 13 | # Publish packages to the downloads bucket 14 | dagger run --silent go run ./cmd package publish \ 15 | $(find $local_dir | grep -e .rpm -e .tar.gz -e .exe -e .zip -e .deb | awk '{print "--package=file://"$0}') \ 16 | --gcp-service-account-key-base64=${GCP_KEY_BASE64} \ 17 | --destination="${DOWNLOADS_DESTINATION}/enterprise/release" 18 | 19 | # Publish packages to grafana.com 20 | dagger run --silent go run ./cmd gcom publish \ 21 | $(find $local_dir | grep -e .rpm -e .tar.gz -e .exe -e .zip -e .deb | grep -v sha256 | grep -v docker | awk '{print "--package=file://"$0}') \ 22 | --api-key=${GCOM_API_KEY} \ 23 | --api-url="https://grafana.com/api/grafana-enterprise" \ 24 | --download-url="https://dl.grafana.com/enterprise/release" \ 25 | --nightly 26 | -------------------------------------------------------------------------------- /containers/opts_pro_image.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import "github.com/grafana/grafana-build/cliutil" 4 | 5 | type ProImageOpts struct { 6 | // Github token used to clone private repositories. 7 | GitHubToken string 8 | 9 | // The path to a Grafana debian package. 10 | Deb string 11 | 12 | // The Grafana version. 13 | GrafanaVersion string 14 | 15 | // The docker image tag. 16 | ImageTag string 17 | 18 | // The docker image repo. 19 | Repo string 20 | 21 | // The release type. 22 | ReleaseType string 23 | 24 | // True if the pro image should be pushed to the container registry. 25 | Push bool 26 | 27 | // The container registry that the image should be pushed to. Required if Push is true. 28 | ContainerRegistry string 29 | } 30 | 31 | func ProImageOptsFromFlags(c cliutil.CLIContext) *ProImageOpts { 32 | return &ProImageOpts{ 33 | GitHubToken: c.String("github-token"), 34 | Deb: c.String("deb"), 35 | GrafanaVersion: c.String("grafana-version"), 36 | ImageTag: c.String("image-tag"), 37 | Repo: c.String("repo"), 38 | ReleaseType: c.String("release-type"), 39 | Push: c.Bool("push"), 40 | ContainerRegistry: c.String("registry"), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/build.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "dagger.io/dagger" 5 | ) 6 | 7 | func Build(builder *dagger.Container) *dagger.Directory { 8 | public := builder. 9 | WithExec([]string{"yarn", "run", "build"}). 10 | WithExec([]string{"/bin/sh", "-c", "find /src/public -type d -name node_modules -print0 | xargs -0 rm -rf"}). 11 | Directory("/src/public") 12 | 13 | return public 14 | } 15 | 16 | func BuildPlugins(builder *dagger.Container) *dagger.Directory { 17 | public := builder. 18 | WithExec([]string{"yarn", "install", "--immutable"}). 19 | WithExec([]string{"/bin/sh", "-c", `if [ -d /src/plugins-bundled ]; then yarn run plugins:build-bundled; else mkdir /src/plugins-bundled; fi`}). 20 | WithExec([]string{"/bin/sh", "-c", "find /src/plugins-bundled -type d -name node_modules -print0 | xargs -0 rm -rf"}). 21 | Directory("/src/plugins-bundled") 22 | 23 | return public 24 | } 25 | 26 | // WithYarnCache mounts the given YarnCacheDir in the provided container 27 | func WithYarnCache(container *dagger.Container, vol *dagger.CacheVolume) *dagger.Container { 28 | yarnCacheDir := "/yarn/cache" 29 | c := container.WithEnvVariable("YARN_CACHE_FOLDER", yarnCacheDir) 30 | return c.WithMountedCache(yarnCacheDir, vol) 31 | } 32 | -------------------------------------------------------------------------------- /docker/opts.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | type DockerOpts struct { 4 | // Registry is the docker Registry for the image. 5 | // If using '--save', then this will have no effect. 6 | // Uses docker hub by default. 7 | // Example: us.gcr.io/12345 8 | Registry string 9 | 10 | // AlpineBase is supplied as a build-arg when building the Grafana docker image. 11 | // When building alpine versions of Grafana it uses this image as its base. 12 | AlpineBase string 13 | 14 | // UbuntuBase is supplied as a build-arg when building the Grafana docker image. 15 | // When building ubuntu versions of Grafana it uses this image as its base. 16 | UbuntuBase string 17 | 18 | // Username is supplied to login to the docker registry when publishing images. 19 | Username string 20 | 21 | // Password is supplied to login to the docker registry when publishing images. 22 | Password string 23 | 24 | // Org overrides the organization when when publishing images. 25 | Org string 26 | 27 | // Repository overrides the repository when when publishing images. 28 | Repository string 29 | 30 | // Latest is supplied to also tag as latest when publishing images. 31 | Latest bool 32 | 33 | // TagFormat and UbuntuTagFormat should be formatted using go template tags. 34 | TagFormat string 35 | UbuntuTagFormat string 36 | } 37 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | 8 | "dagger.io/dagger" 9 | "github.com/grafana/grafana-build/pipelines" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func PipelineActionWithPackageInput(pf pipelines.PipelineFuncWithPackageInput) cli.ActionFunc { 14 | return func(c *cli.Context) error { 15 | var ( 16 | ctx = c.Context 17 | opts = []dagger.ClientOpt{} 18 | ) 19 | if c.Bool("verbose") { 20 | opts = append(opts, dagger.WithLogOutput(os.Stderr)) 21 | } 22 | client, err := dagger.Connect(ctx, opts...) 23 | if err != nil { 24 | return err 25 | } 26 | defer client.Close() 27 | 28 | args, err := pipelines.PipelineArgsFromContext(ctx, c) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | if len(args.PackageInputOpts.Packages) == 0 { 34 | return errors.New("expected at least one package from a '--package' flag") 35 | } 36 | 37 | if err := pf(ctx, client, args); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | } 43 | 44 | func main() { 45 | ctx := context.Background() 46 | 47 | // TODO change the registerer if the user is running using a JSON file etc 48 | for k, v := range Artifacts { 49 | if err := globalCLI.Register(k, v); err != nil { 50 | panic(err) 51 | } 52 | } 53 | 54 | app := globalCLI.App() 55 | 56 | if err := app.RunContext(ctx, os.Args); err != nil { 57 | panic(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docker/verify.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "dagger.io/dagger" 8 | "github.com/grafana/grafana-build/backend" 9 | "github.com/grafana/grafana-build/containers" 10 | "github.com/grafana/grafana-build/e2e" 11 | "github.com/grafana/grafana-build/frontend" 12 | ) 13 | 14 | // Verify uses the given package (.docker.tar.gz) and grafana source code (src) to run the e2e smoke tests. 15 | // the returned directory is the e2e artifacts created by cypress (screenshots and videos). 16 | func Verify( 17 | ctx context.Context, 18 | d *dagger.Client, 19 | image *dagger.File, 20 | src *dagger.Directory, 21 | yarnCache *dagger.CacheVolume, 22 | distro backend.Distribution, 23 | ) error { 24 | nodeVersion, err := frontend.NodeVersion(d, src).Stdout(ctx) 25 | if err != nil { 26 | return fmt.Errorf("failed to get node version from source code: %w", err) 27 | } 28 | 29 | var ( 30 | platform = backend.Platform(distro) 31 | ) 32 | 33 | // This grafana service runs in the background for the e2e tests 34 | service := d.Container(dagger.ContainerOpts{ 35 | Platform: platform, 36 | }). 37 | WithMountedTemp("/var/lib/grafana/plugins"). 38 | Import(image). 39 | WithExposedPort(3000) 40 | 41 | // TODO: Add LICENSE to containers and implement validation 42 | container := e2e.ValidatePackage(d, service.AsService(), src, yarnCache, nodeVersion) 43 | _, err = containers.ExitError(ctx, container) 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /git/github.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | // LookupGitHubToken will try to find a GitHub access token that can then be used for various API calls but also cloning of private repositories. 14 | func LookupGitHubToken(ctx context.Context) (string, error) { 15 | log.Print("Looking for a GitHub token") 16 | 17 | // First try: Check if it's in the environment. This can override everything! 18 | token := os.Getenv("GITHUB_TOKEN") 19 | if token != "" { 20 | log.Print("Using GitHub token provided via environment variable") 21 | return token, nil 22 | } 23 | 24 | // Next, check if the user has gh installed and *it* has a token set: 25 | var data bytes.Buffer 26 | var errData bytes.Buffer 27 | ghPath, err := exec.LookPath("gh") 28 | if err != nil { 29 | return "", fmt.Errorf("GitHub CLI not installed (expected a --github-token flag, a GITHUB_TOKEN environment variable, or a configured GitHub CLI)") 30 | } 31 | 32 | //nolint:gosec 33 | cmd := exec.CommandContext(ctx, ghPath, "auth", "token") 34 | cmd.Stdout = &data 35 | cmd.Stderr = &errData 36 | 37 | if err := cmd.Run(); err != nil { 38 | log.Printf("Querying gh for an access token failed: %s", errData.String()) 39 | return "", fmt.Errorf("lookup in gh failed: %w", err) 40 | } 41 | 42 | log.Print("Using GitHub token provided via gh") 43 | return strings.TrimSpace(data.String()), nil 44 | } 45 | -------------------------------------------------------------------------------- /packages/names.go: -------------------------------------------------------------------------------- 1 | package packages 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/grafana/grafana-build/backend" 8 | ) 9 | 10 | type Name string 11 | 12 | const ( 13 | PackageGrafana Name = "grafana" 14 | PackageEnterprise Name = "grafana-enterprise" 15 | PackageEnterpriseBoring Name = "grafana-enterprise-boringcrypto" 16 | PackagePro Name = "grafana-pro" 17 | PackageNightly Name = "grafana-nightly" 18 | ) 19 | 20 | type NameOpts struct { 21 | // Name is the name of the product in the package. 99% of the time, this will be "grafana" or "grafana-enterprise". 22 | Name Name 23 | Version string 24 | BuildID string 25 | Distro backend.Distribution 26 | Extension string 27 | } 28 | 29 | // FileName returns a file name that matches this format: {grafana|grafana-enterprise}_{version}_{os}_{arch}_{build_number}.tar.gz 30 | func FileName(name Name, version, buildID string, distro backend.Distribution, extension string) (string, error) { 31 | var ( 32 | // This should return something like "linux", "arm" 33 | os, arch = backend.OSAndArch(distro) 34 | // If applicable this will be set to something like "7" (for arm7) 35 | archv = backend.ArchVersion(distro) 36 | ) 37 | 38 | if archv != "" { 39 | arch = strings.Join([]string{arch, archv}, "-") 40 | } 41 | 42 | p := []string{string(name), version, buildID, os, arch} 43 | 44 | return fmt.Sprintf("%s.%s", strings.Join(p, "_"), extension), nil 45 | } 46 | -------------------------------------------------------------------------------- /scripts/drone_build_main.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | local_dst="dist/${DRONE_BUILD_EVENT}" 4 | set -e 5 | 6 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 7 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 8 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 9 | 10 | dagger run --silent go run ./cmd \ 11 | artifacts \ 12 | -a targz:grafana:linux/amd64 \ 13 | -a targz:grafana:linux/arm64 \ 14 | -a targz:grafana:linux/arm/v6 \ 15 | -a targz:grafana:linux/arm/v7 \ 16 | -a targz:grafana:windows/amd64 \ 17 | -a targz:grafana:darwin/amd64 \ 18 | -a deb:grafana:linux/amd64 \ 19 | -a deb:grafana:linux/arm64 \ 20 | -a deb:grafana:linux/arm/v6 \ 21 | -a deb:grafana:linux/arm/v7 \ 22 | -a docker:grafana:linux/amd64 \ 23 | -a docker:grafana:linux/arm64 \ 24 | -a docker:grafana:linux/arm/v7 \ 25 | --yarn-cache=${YARN_CACHE_FOLDER} \ 26 | --checksum \ 27 | --verify \ 28 | --build-id=${DRONE_BUILD_NUMBER} \ 29 | --grafana-dir=${GRAFANA_DIR} \ 30 | --github-token=${GITHUB_TOKEN} \ 31 | --go-version=${GO_VERSION} \ 32 | --ubuntu-base=${UBUNTU_BASE} \ 33 | --alpine-base=${ALPINE_BASE} \ 34 | --destination=${local_dst} > assets.txt 35 | 36 | echo "Final list of artifacts:" 37 | cat assets.txt 38 | 39 | # Move the tar.gz packages to their expected locations 40 | cat assets.txt | IS_MAIN=true go run ./scripts/move_packages.go ./dist/main 41 | -------------------------------------------------------------------------------- /artifacts/packages.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/grafana-build/arguments" 7 | "github.com/grafana/grafana-build/backend" 8 | "github.com/grafana/grafana-build/flags" 9 | "github.com/grafana/grafana-build/packages" 10 | "github.com/grafana/grafana-build/pipeline" 11 | ) 12 | 13 | type PackageDetails struct { 14 | Name packages.Name 15 | Enterprise bool 16 | Version string 17 | BuildID string 18 | Distribution backend.Distribution 19 | } 20 | 21 | func GetPackageDetails(ctx context.Context, options *pipeline.OptionsHandler, state pipeline.StateHandler) (PackageDetails, error) { 22 | distro, err := options.String(flags.Distribution) 23 | if err != nil { 24 | return PackageDetails{}, err 25 | } 26 | version, err := state.String(ctx, arguments.Version) 27 | if err != nil { 28 | return PackageDetails{}, err 29 | } 30 | buildID, err := state.String(ctx, arguments.BuildID) 31 | if err != nil { 32 | return PackageDetails{}, err 33 | } 34 | 35 | name, err := options.String(flags.PackageName) 36 | if err != nil { 37 | return PackageDetails{}, err 38 | } 39 | 40 | enterprise, err := options.Bool(flags.Enterprise) 41 | if err != nil { 42 | return PackageDetails{}, err 43 | } 44 | 45 | return PackageDetails{ 46 | Name: packages.Name(name), 47 | Version: version, 48 | BuildID: buildID, 49 | Distribution: backend.Distribution(distro), 50 | Enterprise: enterprise, 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /scripts/drone_build_main_pro.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | local_dst="./dist/${DRONE_BUILD_EVENT}" 3 | set -e 4 | 5 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 6 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 7 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 8 | # Build all of the grafana.tar.gz packages. 9 | dagger run --silent go run ./cmd \ 10 | artifacts \ 11 | -a targz:pro:linux/amd64 \ 12 | -a targz:pro:linux/arm64 \ 13 | -a deb:pro:linux/amd64 \ 14 | -a deb:pro:linux/arm64 \ 15 | -a frontend:enterprise \ 16 | --yarn-cache=${YARN_CACHE_FOLDER} \ 17 | --checksum \ 18 | --verify \ 19 | --build-id=${DRONE_BUILD_NUMBER} \ 20 | --grafana-ref=${SOURCE_COMMIT} \ 21 | --grafana-repo="https://github.com/grafana/grafana.git" \ 22 | --enterprise-ref=${DRONE_COMMIT} \ 23 | --github-token=${GITHUB_TOKEN} \ 24 | --go-version=${GO_VERSION} \ 25 | --ubuntu-base=${UBUNTU_BASE} \ 26 | --alpine-base=${ALPINE_BASE} \ 27 | --patches-repo=${PATCHES_REPO} \ 28 | --patches-path=${PATCHES_PATH} \ 29 | --destination=${local_dst} > assets.txt 30 | 31 | echo "Final list of artifacts:" 32 | # Move the tar.gz packages to their expected locations 33 | cat assets.txt | grep -v "public" | DESTINATION=gs://grafana-downloads-enterprise2 IS_MAIN=true go run ./scripts/move_packages.go ./dist/main 34 | cat assets.txt | grep "public" | DESTINATION=gs://grafana-static-assets IS_MAIN=true go run ./scripts/move_packages.go ./dist/cdn 35 | -------------------------------------------------------------------------------- /scripts/drone_build_main_enterprise.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | local_dst="dist/${DRONE_BUILD_EVENT}" 3 | set -e 4 | 5 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 6 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 7 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 8 | dagger run --silent go run ./cmd \ 9 | artifacts \ 10 | -a targz:enterprise:linux/amd64 \ 11 | -a targz:enterprise:linux/arm64 \ 12 | -a targz:enterprise:linux/arm/v6 \ 13 | -a targz:enterprise:linux/arm/v7 \ 14 | -a deb:enterprise:linux/amd64 \ 15 | -a deb:enterprise:linux/arm64 \ 16 | -a deb:enterprise:linux/arm/v6 \ 17 | -a deb:enterprise:linux/arm/v7 \ 18 | -a docker:enterprise:linux/amd64 \ 19 | -a docker:enterprise:linux/arm64 \ 20 | --yarn-cache=${YARN_CACHE_FOLDER} \ 21 | --checksum \ 22 | --verify \ 23 | --build-id=${DRONE_BUILD_NUMBER} \ 24 | --grafana-ref=${SOURCE_COMMIT} \ 25 | --grafana-repo="https://github.com/grafana/grafana.git" \ 26 | --enterprise-ref=${DRONE_COMMIT} \ 27 | --github-token=${GITHUB_TOKEN} \ 28 | --go-version=${GO_VERSION} \ 29 | --ubuntu-base=${UBUNTU_BASE} \ 30 | --alpine-base=${ALPINE_BASE} \ 31 | --patches-repo=${PATCHES_REPO} \ 32 | --patches-path=${PATCHES_PATH} \ 33 | --destination=${local_dst} > assets.txt 34 | 35 | cat assets.txt 36 | 37 | # Move the tar.gz packages to their expected locations 38 | cat assets.txt | DESTINATION=gs://grafana-downloads IS_MAIN=true go run ./scripts/move_packages.go ./dist/main 39 | -------------------------------------------------------------------------------- /arguments/yarn.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/grafana/grafana-build/pipeline" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | var YarnCacheDirFlag = &cli.StringFlag{ 12 | Name: "yarn-cache-dir", 13 | Aliases: []string{"yarn-cache"}, 14 | Usage: "Path to the yarn cache directory to mount during 'yarn install' commands (if there is one)", 15 | EnvVars: []string{"YARN_CACHE_FOLDER", "YARN_CACHE_DIR"}, 16 | Value: "", 17 | } 18 | 19 | var YarnCacheDirectory = pipeline.Argument{ 20 | Name: "yarn-cache-dir", 21 | Description: YarnCacheDirFlag.Usage, 22 | ArgumentType: pipeline.ArgumentTypeCacheVolume, 23 | Flags: []cli.Flag{ 24 | YarnCacheDirFlag, 25 | }, 26 | ValueFunc: func(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) { 27 | vol := opts.CLIContext.String(YarnCacheDirFlag.Name) 28 | 29 | // Prepopulate the cache with what's defined in YARN_CACHE_FOLDER 30 | // or in the CLI 31 | if val, ok := os.LookupEnv("YARN_CACHE_FOLDER"); ok { 32 | vol = val 33 | } 34 | 35 | cache := opts.Client.CacheVolume("yarn-cache-dir") 36 | if vol == "" { 37 | return cache, nil 38 | } 39 | 40 | dir := opts.Client.Host().Directory(vol) 41 | _, err := opts.Client.Container(). 42 | From("alpine"). 43 | WithMountedCache("/cache", cache). 44 | WithMountedDirectory("/data", dir). 45 | WithExec([]string{"/bin/sh", "-c", "cp -r /data/* /cache || return 0"}). 46 | Sync(ctx) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return cache, nil 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /flags/distro.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/backend" 5 | "github.com/grafana/grafana-build/pipeline" 6 | ) 7 | 8 | const ( 9 | FlagDistribution = "distro" 10 | ) 11 | 12 | var StaticDistributions = []backend.Distribution{ 13 | backend.DistLinuxAMD64, 14 | backend.DistLinuxARM64, 15 | backend.DistLinuxARMv7, 16 | backend.DistLinuxRISCV64, 17 | backend.DistLinuxS390X, 18 | } 19 | 20 | var DynamicDistributions = []backend.Distribution{ 21 | backend.DistDarwinAMD64, 22 | backend.DistDarwinARM64, 23 | backend.DistWindowsAMD64, 24 | backend.DistWindowsARM64, 25 | backend.DistLinuxAMD64Dynamic, 26 | backend.DistLinuxAMD64DynamicMusl, 27 | } 28 | 29 | func DistroFlags() []pipeline.Flag { 30 | // These distributions have specific options that set some stuff. 31 | f := []pipeline.Flag{ 32 | { 33 | Name: string(backend.DistLinuxARMv6), 34 | Options: map[pipeline.FlagOption]any{ 35 | Distribution: string(backend.DistLinuxARMv6), 36 | Static: true, 37 | RPI: true, 38 | }, 39 | }, 40 | } 41 | 42 | for _, v := range StaticDistributions { 43 | d := string(v) 44 | f = append(f, pipeline.Flag{ 45 | Name: d, 46 | Options: map[pipeline.FlagOption]any{ 47 | Distribution: d, 48 | Static: true, 49 | }, 50 | }) 51 | } 52 | for _, v := range DynamicDistributions { 53 | d := string(v) 54 | f = append(f, pipeline.Flag{ 55 | Name: d, 56 | Options: map[pipeline.FlagOption]any{ 57 | Distribution: d, 58 | Static: false, 59 | }, 60 | }) 61 | } 62 | 63 | return f 64 | } 65 | -------------------------------------------------------------------------------- /backend/vcsinfo.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "dagger.io/dagger" 8 | ) 9 | 10 | type VCSInfo struct { 11 | Version string 12 | Commit *dagger.File 13 | EnterpriseCommit *dagger.File 14 | Branch *dagger.File 15 | } 16 | 17 | func WithVCSInfo(c *dagger.Container, info *VCSInfo, enterprise bool) *dagger.Container { 18 | c = c. 19 | WithFile(".buildinfo.commit", info.Commit). 20 | WithFile(".buildinfo.branch", info.Branch) 21 | 22 | if enterprise { 23 | return c.WithFile(".buildinfo.enterprise-commit", info.EnterpriseCommit) 24 | } 25 | 26 | return c 27 | } 28 | 29 | // VCSInfo gets the VCS data from the directory 'src', writes them to a file on the given container, and returns the files which can be used in other containers. 30 | func GetVCSInfo(src *dagger.Directory, version string, enterprise bool) *VCSInfo { 31 | info := &VCSInfo{ 32 | Version: version, 33 | Commit: src.File(".buildinfo.commit"), 34 | Branch: src.File(".buildinfo.branch"), 35 | } 36 | 37 | if enterprise { 38 | info.EnterpriseCommit = src.File(".buildinfo.enterprise-commit") 39 | } 40 | 41 | return info 42 | } 43 | 44 | func (v *VCSInfo) X() []string { 45 | flags := []string{ 46 | fmt.Sprintf("main.version=%s", strings.TrimPrefix(v.Version, "v")), 47 | `main.commit=$(cat ./.buildinfo.commit)`, 48 | `main.buildBranch=$(cat ./.buildinfo.branch)`, 49 | } 50 | 51 | if v.EnterpriseCommit != nil { 52 | flags = append(flags, `main.enterpriseCommit=$(cat ./.buildinfo.enterprise-commit)`) 53 | } 54 | 55 | return flags 56 | } 57 | -------------------------------------------------------------------------------- /scripts/drone_build_tag_pro.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | local_dst="dist/${DRONE_BUILD_EVENT}" 3 | set -e 4 | 5 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 6 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 7 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 8 | 9 | # Build all of the grafana.tar.gz packages. 10 | dagger run --silent go run ./cmd \ 11 | artifacts \ 12 | -a frontend:enterprise \ 13 | -a targz:pro:linux/amd64 \ 14 | -a targz:pro:linux/arm64 \ 15 | -a targz:pro:linux/arm/v6 \ 16 | -a targz:pro:linux/arm/v7 \ 17 | -a deb:pro:linux/amd64 \ 18 | -a deb:pro:linux/arm64 \ 19 | -a targz:pro:darwin/amd64 \ 20 | -a targz:pro:windows/amd64 \ 21 | -a docker:pro:linux/amd64 \ 22 | -a docker:pro:linux/arm64 \ 23 | -a docker:pro:linux/arm/v7 \ 24 | -a docker:pro:linux/amd64:ubuntu \ 25 | -a docker:pro:linux/arm64:ubuntu \ 26 | -a docker:pro:linux/arm/v7:ubuntu \ 27 | --checksum \ 28 | --parallel=2 \ 29 | --yarn-cache=${YARN_CACHE_FOLDER} \ 30 | --build-id=${DRONE_BUILD_NUMBER} \ 31 | --enterprise-ref=${DRONE_TAG} \ 32 | --grafana-ref=${DRONE_TAG} \ 33 | --grafana-repo=https://github.com/grafana/grafana-security-mirror.git \ 34 | --github-token=${GITHUB_TOKEN} \ 35 | --version=${DRONE_TAG} \ 36 | --go-version=${GO_VERSION} \ 37 | --ubuntu-base="${UBUNTU_BASE}" \ 38 | --alpine-base="${ALPINE_BASE}" \ 39 | --destination=${local_dst} > assets.txt 40 | 41 | # Move the tar.gz packages to their expected locations 42 | cat assets.txt | go run ./scripts/move_packages.go ./dist/prerelease 43 | -------------------------------------------------------------------------------- /cmd/app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/artifacts" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | type CLI struct { 9 | artifacts map[string]artifacts.Initializer 10 | } 11 | 12 | func (c *CLI) ArtifactsCommand() *cli.Command { 13 | f := artifacts.ArtifactFlags(c) 14 | flags := make([]cli.Flag, len(f)) 15 | copy(flags, f) 16 | return &cli.Command{ 17 | Name: "artifacts", 18 | Usage: "Use this command to declare a list of artifacts to be built and/or published", 19 | Flags: flags, 20 | Action: artifacts.Command(c), 21 | } 22 | } 23 | 24 | func (c *CLI) App() *cli.App { 25 | artifactsCommand := c.ArtifactsCommand() 26 | 27 | return &cli.App{ 28 | Name: "grafana-build", 29 | Usage: "A build tool for Grafana", 30 | Commands: []*cli.Command{ 31 | artifactsCommand, 32 | 33 | // Legacy commands, should eventually be completely replaced by what's in "artifacts" 34 | { 35 | Name: "package", 36 | Subcommands: []*cli.Command{ 37 | PackagePublishCommand, 38 | }, 39 | }, 40 | { 41 | Name: "docker", 42 | Subcommands: []*cli.Command{ 43 | DockerPublishCommand, 44 | }, 45 | }, 46 | ProImageCommand, 47 | { 48 | Name: "npm", 49 | Subcommands: []*cli.Command{ 50 | PublishNPMCommand, 51 | }, 52 | }, 53 | GCOMCommand, 54 | }, 55 | } 56 | } 57 | 58 | func (c *CLI) Register(flag string, a artifacts.Initializer) error { 59 | c.artifacts[flag] = a 60 | return nil 61 | } 62 | 63 | func (c *CLI) Initializers() map[string]artifacts.Initializer { 64 | return c.artifacts 65 | } 66 | 67 | var globalCLI = &CLI{ 68 | artifacts: map[string]artifacts.Initializer{}, 69 | } 70 | -------------------------------------------------------------------------------- /targz/build.go: -------------------------------------------------------------------------------- 1 | package targz 2 | 3 | import ( 4 | "path" 5 | 6 | "dagger.io/dagger" 7 | ) 8 | 9 | func NewMappedDir(path string, directory *dagger.Directory) MappedDirectory { 10 | return MappedDirectory{path: path, directory: directory} 11 | } 12 | 13 | type MappedDirectory struct { 14 | path string 15 | directory *dagger.Directory 16 | } 17 | 18 | type MappedFile struct { 19 | path string 20 | file *dagger.File 21 | } 22 | 23 | func NewMappedFile(path string, file *dagger.File) MappedFile { 24 | return MappedFile{path: path, file: file} 25 | } 26 | 27 | type Opts struct { 28 | // Root is the root folder that holds all of the packaged data. 29 | // It is common for targz packages to have a root folder. 30 | // This should equal something like `grafana-9.4.1`. 31 | Root string 32 | 33 | // A map of directory paths relative to the root, like 'bin', 'public', 'npm-artifacts' 34 | // to dagger directories. 35 | Directories []MappedDirectory 36 | Files []MappedFile 37 | } 38 | 39 | func Build(packager *dagger.Container, opts *Opts) *dagger.File { 40 | root := opts.Root 41 | 42 | packager = packager. 43 | WithWorkdir("/src") 44 | 45 | paths := []string{} 46 | for _, v := range opts.Files { 47 | path := path.Join(root, v.path) 48 | packager = packager.WithMountedFile(path, v.file) 49 | paths = append(paths, path) 50 | } 51 | 52 | for _, v := range opts.Directories { 53 | path := path.Join(root, v.path) 54 | packager = packager.WithMountedDirectory(path, v.directory) 55 | paths = append(paths, path) 56 | } 57 | 58 | packager = packager.WithExec(append([]string{"tar", "-czf", "/package.tar.gz"}, paths...)) 59 | 60 | return packager.File("/package.tar.gz") 61 | } 62 | -------------------------------------------------------------------------------- /scripts/drone_build_nightly_grafana.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | local_dst="${DRONE_WORKSPACE}/dist" 4 | 5 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 6 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 7 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 8 | 9 | dagger run --silent go run ./cmd \ 10 | artifacts \ 11 | -a targz:grafana:linux/amd64 \ 12 | -a targz:grafana:linux/arm64 \ 13 | -a targz:grafana:linux/arm/v7 \ 14 | -a targz:grafana:linux/arm/v6 \ 15 | -a deb:grafana:linux/amd64:nightly \ 16 | -a deb:grafana:linux/arm64:nightly \ 17 | -a deb:grafana:linux/arm/v6:nightly \ 18 | -a deb:grafana:linux/arm/v7:nightly \ 19 | -a rpm:grafana:linux/amd64:sign:nightly \ 20 | -a rpm:grafana:linux/arm64:sign:nightly \ 21 | -a targz:grafana:windows/amd64 \ 22 | -a targz:grafana:windows/arm64 \ 23 | -a targz:grafana:darwin/amd64 \ 24 | -a targz:grafana:darwin/arm64 \ 25 | -a zip:grafana:windows/amd64 \ 26 | -a msi:grafana:windows/amd64 \ 27 | -a docker:grafana:linux/amd64 \ 28 | -a docker:grafana:linux/arm64 \ 29 | -a docker:grafana:linux/arm/v7 \ 30 | -a docker:grafana:linux/amd64:ubuntu \ 31 | -a docker:grafana:linux/arm64:ubuntu \ 32 | -a docker:grafana:linux/arm/v7:ubuntu \ 33 | --checksum \ 34 | --verify \ 35 | --build-id=${DRONE_BUILD_NUMBER} \ 36 | --grafana-dir=${GRAFANA_DIR} \ 37 | --github-token=${GITHUB_TOKEN} \ 38 | --destination=${local_dst} \ 39 | --yarn-cache=${YARN_CACHE_FOLDER} \ 40 | --go-version=${GO_VERSION} \ 41 | --ubuntu-base="${UBUNTU_BASE}" \ 42 | --alpine-base="${ALPINE_BASE}" > assets.txt 43 | 44 | cat assets.txt 45 | -------------------------------------------------------------------------------- /containers/publish_dir.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "strings" 9 | 10 | "dagger.io/dagger" 11 | ) 12 | 13 | func publishLocalDir(ctx context.Context, dir *dagger.Directory, dst string) error { 14 | if _, err := dir.Export(ctx, dst); err != nil { 15 | return err 16 | } 17 | 18 | return nil 19 | } 20 | 21 | func publishGCSDir(ctx context.Context, d *dagger.Client, dir *dagger.Directory, opts *GCPOpts, dst string) error { 22 | auth := GCSAuth(d, opts) 23 | uploader, err := GCSUploadDirectory(d, GoogleCloudImage, auth, dir, dst) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if _, err := ExitError(ctx, uploader); err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // PublishDirectory publishes a directory to the given destination. 36 | func PublishDirectory(ctx context.Context, d *dagger.Client, dir *dagger.Directory, opts *GCPOpts, dst string) (string, error) { 37 | log.Println("Publishing directory", dst) 38 | u, err := url.Parse(dst) 39 | if err != nil { 40 | // If the destination URL is not a URL then we can assume that it's just a filepath. 41 | if err := publishLocalDir(ctx, dir, dst); err != nil { 42 | return "", err 43 | } 44 | return "", err 45 | } 46 | 47 | switch u.Scheme { 48 | case "file", "fs": 49 | dst := strings.TrimPrefix(u.String(), u.Scheme+"://") 50 | if err := publishLocalDir(ctx, dir, dst); err != nil { 51 | return "", err 52 | } 53 | case "gs": 54 | if err := publishGCSDir(ctx, d, dir, opts, dst); err != nil { 55 | return "", err 56 | } 57 | default: 58 | return "", fmt.Errorf("%w: '%s'", ErrorUnrecognizedScheme, u.Scheme) 59 | } 60 | 61 | return dst, nil 62 | } 63 | -------------------------------------------------------------------------------- /arguments/go_build_cache.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import ( 4 | "context" 5 | 6 | "dagger.io/dagger" 7 | "github.com/grafana/grafana-build/golang" 8 | "github.com/grafana/grafana-build/pipeline" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var GoBuildCache = pipeline.Argument{ 13 | Name: "go-cache-volume", 14 | Description: "Mounted at GOCACHE when building Go backends", 15 | ArgumentType: pipeline.ArgumentTypeCacheVolume, 16 | Flags: []cli.Flag{}, 17 | ValueFunc: func(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) { 18 | return opts.Client.CacheVolume("go-build-cache"), nil 19 | }, 20 | } 21 | 22 | var GoModCache = pipeline.Argument{ 23 | Name: "go-mod-volume", 24 | Description: "Stores downloaded Go modules when building Go backends", 25 | ArgumentType: pipeline.ArgumentTypeCacheVolume, 26 | Flags: []cli.Flag{}, 27 | ValueFunc: func(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) { 28 | vol := opts.Client.CacheVolume("go-mod-cache") 29 | goVersion, err := opts.State.String(ctx, GoVersion) 30 | if err != nil { 31 | return nil, err 32 | } 33 | src, err := opts.State.Directory(ctx, GrafanaDirectory) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | c := golang.Container(opts.Client, opts.Platform, goVersion). 39 | WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). 40 | WithMountedCache("/go/pkg/mod", vol). 41 | WithDirectory("/src", src, dagger.ContainerWithDirectoryOpts{ 42 | Include: []string{"**/*.mod", "**/*.sum", "**/*.work"}, 43 | }). 44 | WithWorkdir("/src"). 45 | WithExec([]string{"go", "mod", "download"}) 46 | 47 | if _, err := c.Sync(ctx); err != nil { 48 | return nil, err 49 | } 50 | 51 | return vol, nil 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /docker/publish.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "dagger.io/dagger" 8 | ) 9 | 10 | func PublishPackageImage(ctx context.Context, d *dagger.Client, pkg *dagger.File, tag, username, password, registry string) (string, error) { 11 | return d.Container().From("docker"). 12 | WithFile("grafana.img", pkg). 13 | WithSecretVariable("DOCKER_USERNAME", d.SetSecret("docker-username", username)). 14 | WithSecretVariable("DOCKER_PASSWORD", d.SetSecret("docker-password", password)). 15 | WithUnixSocket("/var/run/docker.sock", d.Host().UnixSocket("/var/run/docker.sock")). 16 | WithExec([]string{"/bin/sh", "-c", fmt.Sprintf("docker login %s -u $DOCKER_USERNAME -p $DOCKER_PASSWORD", registry)}). 17 | WithExec([]string{"/bin/sh", "-c", "docker load -i grafana.img | awk -F 'Loaded image: ' '{print $2}' > /tmp/image_tag"}). 18 | WithExec([]string{"/bin/sh", "-c", fmt.Sprintf("docker tag $(cat /tmp/image_tag) %s", tag)}). 19 | WithExec([]string{"docker", "push", tag}). 20 | Stdout(ctx) 21 | } 22 | 23 | func PublishManifest(ctx context.Context, d *dagger.Client, manifest string, tags []string, username, password, registry string) (string, error) { 24 | return d.Container().From("docker"). 25 | WithUnixSocket("/var/run/docker.sock", d.Host().UnixSocket("/var/run/docker.sock")). 26 | WithSecretVariable("DOCKER_USERNAME", d.SetSecret("docker-username", username)). 27 | WithSecretVariable("DOCKER_PASSWORD", d.SetSecret("docker-password", password)). 28 | WithExec([]string{"/bin/sh", "-c", fmt.Sprintf("docker login %s -u $DOCKER_USERNAME -p $DOCKER_PASSWORD", registry)}). 29 | WithExec(append([]string{"docker", "manifest", "create", manifest}, tags...)). 30 | WithExec([]string{"docker", "manifest", "push", manifest}). 31 | Stdout(ctx) 32 | } 33 | -------------------------------------------------------------------------------- /scripts/drone_build_tag_grafana.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dst="${DESTINATION}/${DRONE_BUILD_EVENT}" 3 | local_dst="file://dist/${DRONE_BUILD_EVENT}" 4 | set -e 5 | 6 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 7 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 8 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 9 | 10 | dagger run --silent go run ./cmd \ 11 | artifacts \ 12 | -a npm:grafana \ 13 | -a storybook \ 14 | -a targz:grafana:linux/amd64 \ 15 | -a targz:grafana:linux/arm64 \ 16 | -a targz:grafana:linux/arm/v6 \ 17 | -a targz:grafana:linux/arm/v7 \ 18 | -a deb:grafana:linux/amd64 \ 19 | -a deb:grafana:linux/arm64 \ 20 | -a deb:grafana:linux/arm/v6 \ 21 | -a deb:grafana:linux/arm/v7 \ 22 | -a rpm:grafana:linux/amd64:sign \ 23 | -a rpm:grafana:linux/arm64:sign \ 24 | -a docker:grafana:linux/amd64 \ 25 | -a docker:grafana:linux/arm64 \ 26 | -a docker:grafana:linux/arm/v7 \ 27 | -a docker:grafana:linux/amd64:ubuntu \ 28 | -a docker:grafana:linux/arm64:ubuntu \ 29 | -a docker:grafana:linux/arm/v7:ubuntu \ 30 | -a targz:grafana:windows/amd64 \ 31 | -a targz:grafana:windows/arm64 \ 32 | -a targz:grafana:darwin/amd64 \ 33 | -a targz:grafana:darwin/arm64 \ 34 | -a zip:grafana:windows/amd64 \ 35 | -a msi:grafana:windows/amd64 \ 36 | --yarn-cache=${YARN_CACHE_FOLDER} \ 37 | --checksum \ 38 | --verify \ 39 | --build-id=${DRONE_BUILD_NUMBER} \ 40 | --grafana-dir=${GRAFANA_DIR} \ 41 | --github-token=${GITHUB_TOKEN} \ 42 | --go-version=${GO_VERSION} \ 43 | --ubuntu-base="${UBUNTU_BASE}" \ 44 | --alpine-base="${ALPINE_BASE}" \ 45 | --version=${DRONE_TAG} \ 46 | --destination=${local_dst} > assets.txt 47 | 48 | cat assets.txt | go run ./scripts/move_packages.go ./dist/prerelease 49 | -------------------------------------------------------------------------------- /arguments/packages.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/grafana/grafana-build/containers" 8 | "github.com/grafana/grafana-build/pipeline" 9 | "github.com/grafana/grafana-build/stringutil" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | var flagBuildID = &cli.StringFlag{ 14 | Name: "build-id", 15 | Usage: "Build ID to use in package names", 16 | Value: "local", 17 | } 18 | 19 | var BuildID = pipeline.Argument{ 20 | Name: "build-id", 21 | Description: "The grafana backend binaries ('grafana', 'grafana-cli', 'grafana-server') in a directory", 22 | Flags: []cli.Flag{ 23 | flagBuildID, 24 | }, 25 | ValueFunc: func(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) { 26 | v := opts.CLIContext.String("build-id") 27 | if v == "" { 28 | v = stringutil.RandomString(8) 29 | } 30 | 31 | return v, nil 32 | }, 33 | } 34 | 35 | var flagVersion = &cli.StringFlag{ 36 | Name: "version", 37 | Usage: "Explicit version number. If this is not set then one with will auto-detected based on the source repository", 38 | } 39 | 40 | var Version = pipeline.Argument{ 41 | Name: "version", 42 | Description: "The version string that is shown in the UI, in the CLI, and in package metadata", 43 | Flags: []cli.Flag{ 44 | flagVersion, 45 | }, 46 | Requires: []pipeline.Argument{ 47 | GrafanaDirectory, 48 | }, 49 | ValueFunc: func(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) { 50 | v := opts.CLIContext.String("version") 51 | if v != "" { 52 | return v, nil 53 | } 54 | src, err := opts.State.Directory(ctx, GrafanaDirectory) 55 | if err != nil { 56 | return "", err 57 | } 58 | buildID, err := opts.State.String(ctx, BuildID) 59 | if err != nil { 60 | return "", err 61 | } 62 | version, err := containers.GetJSONValue(ctx, opts.Client, src, "package.json", "version") 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | return strings.ReplaceAll(version, "pre", buildID), nil 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /artifacts/flags.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "strings" 5 | 6 | "log/slog" 7 | 8 | "github.com/grafana/grafana-build/cmd/flags" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | func ArtifactFlags(r Registerer) []cli.Flag { 13 | artifactsFlag := &cli.StringSliceFlag{ 14 | Name: "artifacts", 15 | Aliases: []string{"a"}, 16 | } 17 | 18 | buildFlag := &cli.BoolFlag{ 19 | Name: "build", 20 | Value: true, 21 | } 22 | publishFlag := &cli.BoolFlag{ 23 | Name: "publish", 24 | Usage: "If true, then the artifacts that are built will be published. If `--build=false` and the artifacts are found in the --destination, then those artifacts are not built and are published instead.", 25 | Value: true, 26 | } 27 | 28 | verifyFlag := &cli.BoolFlag{ 29 | Name: "verify", 30 | Usage: "If true, then the artifacts that are built will be verified with e2e tests or similar after being exported, depending on the artifact", 31 | Value: false, 32 | } 33 | 34 | flags := flags.Join( 35 | []cli.Flag{ 36 | artifactsFlag, 37 | buildFlag, 38 | publishFlag, 39 | verifyFlag, 40 | flags.Platform, 41 | }, 42 | flags.PublishFlags, 43 | flags.ConcurrencyFlags, 44 | []cli.Flag{ 45 | flags.Verbose, 46 | }, 47 | ) 48 | 49 | // All of these artifacts are the registered artifacts. These should mostly stay the same no matter what. 50 | initializers := r.Initializers() 51 | 52 | // Add all of the CLI flags that are defined by each artifact's arguments. 53 | m := map[string]cli.Flag{} 54 | 55 | // For artifact arguments that specify flags, we'll coalesce them here and add them to the list of flags. 56 | for _, n := range initializers { 57 | for _, arg := range n.Arguments { 58 | for _, f := range arg.Flags { 59 | fn := strings.Join(f.Names(), ",") 60 | m[fn] = f 61 | slog.Debug("global flag added by argument in artifact", "flag", fn, "arg", arg.Name) 62 | } 63 | } 64 | } 65 | 66 | for _, v := range m { 67 | flags = append(flags, v) 68 | } 69 | 70 | return flags 71 | } 72 | -------------------------------------------------------------------------------- /scripts/all.sh: -------------------------------------------------------------------------------- 1 | go run ./cmd artifacts \ 2 | -a frontend:enterprise \ 3 | -a storybook \ 4 | -a npm:grafana \ 5 | -a targz:grafana:linux/amd64 \ 6 | -a targz:grafana:linux/arm64 \ 7 | -a targz:grafana:linux/riscv64 \ 8 | -a targz:grafana:linux/arm/v6 \ 9 | -a targz:grafana:linux/arm/v7 \ 10 | -a targz:grafana:darwin/amd64 \ 11 | -a targz:grafana:windows/amd64 \ 12 | -a targz:enterprise:linux/amd64 \ 13 | -a targz:enterprise:linux/arm64 \ 14 | -a targz:enterprise:linux/riscv64 \ 15 | -a targz:enterprise:linux/arm/v6 \ 16 | -a targz:enterprise:linux/arm/v7 \ 17 | -a targz:enterprise:darwin/amd64 \ 18 | -a targz:enterprise:windows/amd64 \ 19 | -a targz:boring:linux/amd64/dynamic \ 20 | -a deb:grafana:linux/amd64 \ 21 | -a deb:grafana:linux/arm64 \ 22 | -a deb:grafana:linux/arm/v6 \ 23 | -a deb:grafana:linux/arm/v7 \ 24 | -a deb:enterprise:linux/amd64 \ 25 | -a deb:enterprise:linux/arm64 \ 26 | -a deb:enterprise:linux/arm/v6 \ 27 | -a deb:enterprise:linux/arm/v7 \ 28 | -a rpm:grafana:linux/amd64:sign \ 29 | -a rpm:grafana:linux/arm64:sign \ 30 | -a rpm:enterprise:linux/amd64:sign \ 31 | -a rpm:enterprise:linux/arm64:sign \ 32 | -a docker:grafana:linux/amd64 \ 33 | -a docker:grafana:linux/arm64 \ 34 | -a docker:grafana:linux/amd64:ubuntu \ 35 | -a docker:grafana:linux/arm64:ubuntu \ 36 | -a docker:enterprise:linux/amd64 \ 37 | -a docker:enterprise:linux/arm64 \ 38 | -a docker:enterprise:linux/amd64:ubuntu \ 39 | -a docker:enterprise:linux/arm64:ubuntu \ 40 | -a docker:boring:linux/amd64/dynamic \ 41 | -a zip:grafana:windows/amd64 \ 42 | -a zip:enterprise:windows/amd64 \ 43 | -a zip:grafana:windows/arm64 \ 44 | -a zip:enterprise:windows/arm64 \ 45 | -a exe:grafana:windows/amd64 \ 46 | -a exe:enterprise:windows/amd64 \ 47 | -build-id=103 \ 48 | --grafana-ref=v10.1.0 \ 49 | --checksum \ 50 | --verify \ 51 | --grafana-dir=$HOME/Work/Grafana/grafana \ 52 | --enterprise-dir=$HOME/Work/Grafana/grafana-enterprise \ 53 | --enterprise-ref=v10.1.0 > out.txt 54 | 55 | -------------------------------------------------------------------------------- /pipelines/npm_publish.go: -------------------------------------------------------------------------------- 1 | package pipelines 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "dagger.io/dagger" 9 | "github.com/grafana/grafana-build/containers" 10 | "github.com/grafana/grafana-build/frontend" 11 | "golang.org/x/sync/errgroup" 12 | "golang.org/x/sync/semaphore" 13 | ) 14 | 15 | func PublishNPM(ctx context.Context, d *dagger.Client, args PipelineArgs) error { 16 | var ( 17 | wg = &errgroup.Group{} 18 | sm = semaphore.NewWeighted(args.ConcurrencyOpts.Parallel) 19 | ) 20 | 21 | packages, err := containers.GetPackages(ctx, d, args.PackageInputOpts, args.GCPOpts) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | // Extract the package(s) 27 | for i := range args.PackageInputOpts.Packages { 28 | var ( 29 | // name = ReplaceExt(v, "") 30 | targz = packages[i] 31 | ) 32 | 33 | artifacts := containers.ExtractedArchive(d, targz).Directory("npm-artifacts") 34 | 35 | entries, err := artifacts.Entries(ctx) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | for _, path := range entries { 41 | wg.Go(PublishNPMFunc(ctx, sm, d, artifacts.File(path), path, args.NpmToken, args.NpmRegistry, args.NpmTags)) 42 | } 43 | } 44 | return wg.Wait() 45 | } 46 | 47 | func PublishNPMFunc(ctx context.Context, sm *semaphore.Weighted, d *dagger.Client, pkg *dagger.File, path, token, registry string, tags []string) func() error { 48 | return func() error { 49 | log.Printf("[%s] Attempting to publish package", path) 50 | log.Printf("[%s] Acquiring semaphore", path) 51 | if err := sm.Acquire(ctx, 1); err != nil { 52 | return fmt.Errorf("failed to acquire semaphore: %w", err) 53 | } 54 | defer sm.Release(1) 55 | log.Printf("[%s] Acquired semaphore", path) 56 | 57 | log.Printf("[%s] Publishing package", path) 58 | out, err := frontend.PublishNPM(ctx, d, pkg, token, registry, tags) 59 | if err != nil { 60 | return fmt.Errorf("[%s] error: %w", path, err) 61 | } 62 | log.Printf("[%s] Done publishing package", path) 63 | 64 | fmt.Fprintln(Stdout, out) 65 | return nil 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/drone_build_tag_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dst="${DESTINATION}/${DRONE_BUILD_EVENT}" 3 | local_dst="./dist/${DRONE_BUILD_EVENT}" 4 | set -e 5 | 6 | dagger run go run ./cmd artifacts \ 7 | -a frontend:enterprise \ 8 | -a storybook \ 9 | -a npm:grafana \ 10 | -a targz:grafana:linux/amd64 \ 11 | -a targz:grafana:linux/arm64 \ 12 | -a targz:grafana:linux/riscv64 \ 13 | -a targz:grafana:linux/arm/v6 \ 14 | -a targz:grafana:linux/arm/v7 \ 15 | -a targz:enterprise:linux/amd64 \ 16 | -a targz:enterprise:linux/arm64 \ 17 | -a targz:enterprise:linux/riscv64 \ 18 | -a targz:enterprise:linux/arm/v6 \ 19 | -a targz:enterprise:linux/arm/v7 \ 20 | -a targz:boring:linux/amd64/dynamic \ 21 | -a deb:grafana:linux/amd64 \ 22 | -a deb:grafana:linux/arm64 \ 23 | -a deb:grafana:linux/arm/v6 \ 24 | -a deb:grafana:linux/arm/v7 \ 25 | -a deb:enterprise:linux/amd64 \ 26 | -a deb:enterprise:linux/arm64 \ 27 | -a deb:enterprise:linux/arm/v6 \ 28 | -a deb:enterprise:linux/arm/v7 \ 29 | -a rpm:grafana:linux/amd64:sign \ 30 | -a rpm:grafana:linux/arm64:sign \ 31 | -a rpm:enterprise:linux/amd64 \ 32 | -a rpm:enterprise:linux/arm64 \ 33 | -a docker:grafana:linux/amd64 \ 34 | -a docker:grafana:linux/arm64 \ 35 | -a docker:grafana:linux/amd64:ubuntu \ 36 | -a docker:grafana:linux/arm64:ubuntu \ 37 | -a docker:enterprise:linux/amd64 \ 38 | -a docker:enterprise:linux/arm64 \ 39 | -a docker:enterprise:linux/amd64:ubuntu \ 40 | -a docker:enterprise:linux/arm64:ubuntu \ 41 | -a docker:boring:linux/amd64/dynamic \ 42 | -a zip:grafana:windows/amd64 \ 43 | -a zip:enterprise:windows/amd64 \ 44 | -a zip:grafana:windows/arm64 \ 45 | -a zip:enterprise:windows/arm64 \ 46 | -a msi:grafana:windows/amd64 \ 47 | -a msi:enterprise:windows/amd64 \ 48 | --parallel=2 \ 49 | --ubuntu-base="${UBUNTU_BASE}" \ 50 | --alpine-base="${ALPINE_BASE}" \ 51 | --go-version="${GO_VERSION}" \ 52 | -build-id=103 \ 53 | --checksum > out.txt 54 | 55 | # Move the tar.gz packages to their expected locations 56 | cat assets.txt | go run ./scripts/move_packages.go ./dist/prerelease 57 | -------------------------------------------------------------------------------- /frontend/builder.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "dagger.io/dagger" 5 | ) 6 | 7 | // Builder mounts all of the necessary files to run yarn build commands and includes a yarn install exec 8 | func Builder(d *dagger.Client, platform dagger.Platform, src *dagger.Directory, nodeVersion string, cache *dagger.CacheVolume) *dagger.Container { 9 | container := WithYarnCache( 10 | NodeContainer(d, NodeImage(nodeVersion), platform), 11 | cache, 12 | ). 13 | WithDirectory("/src", 14 | src. 15 | WithoutFile("go.mod"). 16 | WithoutFile("go.sum"). 17 | WithoutFile("go.work"). 18 | WithoutFile("go.work.sum"). 19 | WithoutDirectory("devenv"). 20 | WithoutDirectory(".github"). 21 | WithoutDirectory("docs"). 22 | WithoutDirectory("pkg"). 23 | WithoutDirectory("apps"). 24 | WithoutDirectory(".nx"), 25 | dagger.ContainerWithDirectoryOpts{ 26 | Exclude: []string{ 27 | "*drone*", 28 | "*.go", 29 | "*.md", 30 | }, 31 | }, 32 | ). 33 | WithWorkdir("/src") 34 | 35 | // TODO: Should figure out exactly what we can include without all the extras so we can take advantage of caching better. 36 | // This had to be commented because storybook builds on branches older than 10.1.x were failing. 37 | 38 | // container = containers.WithDirectories(container, map[string]*dagger.Directory{ 39 | // ".yarn": src.Directory(".yarn"), 40 | // "packages": src.Directory("packages"), 41 | // "plugins-bundled": src.Directory("plugins-bundled"), 42 | // "public": src.Directory("public"), 43 | // "scripts": src.Directory("scripts"), 44 | // }) 45 | 46 | // container = containers.WithFiles(container, map[string]*dagger.File{ 47 | // "package.json": src.File("package.json"), 48 | // "lerna.json": src.File("lerna.json"), 49 | // "yarn.lock": src.File("yarn.lock"), 50 | // ".yarnrc.yml": src.File(".yarnrc.yml"), 51 | // }) 52 | 53 | // This yarn install is ran just to rebuild the yarn pnp files; all of the dependencies should be in the cache by now 54 | return container.WithExec([]string{"yarn", "install", "--immutable"}) 55 | } 56 | -------------------------------------------------------------------------------- /scripts/drone_build_tag_enterprise.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | local_dst="dist/${DRONE_BUILD_EVENT}" 3 | set -e 4 | 5 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 6 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 7 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 8 | 9 | # Build all of the grafana.tar.gz packages. 10 | dagger run go run ./cmd \ 11 | artifacts \ 12 | -a targz:enterprise:linux/amd64 \ 13 | -a targz:enterprise:linux/arm64 \ 14 | -a targz:enterprise:linux/arm/v6 \ 15 | -a targz:enterprise:linux/arm/v7 \ 16 | -a deb:enterprise:linux/amd64 \ 17 | -a deb:enterprise:linux/arm64 \ 18 | -a deb:enterprise:linux/arm/v6 \ 19 | -a deb:enterprise:linux/arm/v7 \ 20 | -a rpm:enterprise:linux/amd64:sign \ 21 | -a rpm:enterprise:linux/arm64:sign \ 22 | -a targz:enterprise:windows/amd64 \ 23 | -a targz:enterprise:windows/arm64 \ 24 | -a targz:enterprise:darwin/amd64 \ 25 | -a targz:enterprise:darwin/arm64 \ 26 | -a targz:boring:linux/amd64/dynamic \ 27 | -a zip:enterprise:windows/amd64 \ 28 | -a msi:enterprise:windows/amd64 \ 29 | -a docker:enterprise:linux/amd64 \ 30 | -a docker:enterprise:linux/arm64 \ 31 | -a docker:enterprise:linux/arm/v7 \ 32 | -a docker:enterprise:linux/amd64:ubuntu \ 33 | -a docker:enterprise:linux/arm64:ubuntu \ 34 | -a docker:enterprise:linux/arm/v7:ubuntu \ 35 | -a docker:boring:linux/amd64/dynamic \ 36 | --yarn-cache=${YARN_CACHE_FOLDER} \ 37 | --verify \ 38 | --checksum \ 39 | --parallel=5 \ 40 | --build-id=${DRONE_BUILD_NUMBER} \ 41 | --enterprise-ref=${DRONE_TAG} \ 42 | --grafana-ref=${DRONE_TAG} \ 43 | --grafana-repo=https://github.com/grafana/grafana-security-mirror.git \ 44 | --github-token=${GITHUB_TOKEN} \ 45 | --go-version=${GO_VERSION} \ 46 | --ubuntu-base="${UBUNTU_BASE}" \ 47 | --alpine-base="${ALPINE_BASE}" \ 48 | --version=${DRONE_TAG} \ 49 | --destination=${local_dst} > assets.txt 50 | 51 | # Move the tar.gz packages to their expected locations 52 | cat assets.txt | go run ./scripts/move_packages.go ./dist/prerelease 53 | -------------------------------------------------------------------------------- /scripts/drone_build_nightly_enterprise.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | local_dst="${DRONE_WORKSPACE}/dist" 4 | 5 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' 6 | # This command enables qemu emulators for building Docker images for arm64/armv6/armv7/etc on the host. 7 | docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all 8 | 9 | # -a targz:enterprise:linux/arm/v6 \ 10 | # -a targz:enterprise:linux/arm/v7 \ 11 | # -a deb:enterprise:linux/arm/v6:nightly \ 12 | # -a deb:enterprise:linux/arm/v7:nightly \ 13 | # -a docker:enterprise:linux/arm/v7 \ 14 | # -a docker:enterprise:linux/arm/v7:ubuntu \ 15 | 16 | dagger run --silent go run ./cmd \ 17 | artifacts \ 18 | -a targz:enterprise:linux/amd64 \ 19 | -a targz:enterprise:linux/arm64 \ 20 | -a targz:enterprise:linux/arm/v7 \ 21 | -a targz:enterprise:linux/arm/v6 \ 22 | -a deb:enterprise:linux/amd64:nightly \ 23 | -a deb:enterprise:linux/arm64:nightly \ 24 | -a deb:enterprise:linux/arm/v6:nightly \ 25 | -a deb:enterprise:linux/arm/v7:nightly \ 26 | -a rpm:enterprise:linux/amd64:sign:nightly \ 27 | -a rpm:enterprise:linux/arm64:sign:nightly \ 28 | -a targz:enterprise:windows/amd64 \ 29 | -a targz:enterprise:windows/arm64 \ 30 | -a targz:enterprise:darwin/amd64 \ 31 | -a targz:enterprise:darwin/arm64 \ 32 | -a zip:enterprise:windows/amd64 \ 33 | -a msi:enterprise:windows/amd64 \ 34 | -a docker:enterprise:linux/amd64 \ 35 | -a docker:enterprise:linux/arm64 \ 36 | -a docker:enterprise:linux/arm/v7 \ 37 | -a docker:enterprise:linux/amd64:ubuntu \ 38 | -a docker:enterprise:linux/arm64:ubuntu \ 39 | -a docker:enterprise:linux/arm/v7:ubuntu \ 40 | --checksum \ 41 | --verify \ 42 | --build-id=${DRONE_BUILD_NUMBER} \ 43 | --grafana-ref=main \ 44 | --enterprise-ref=main \ 45 | --grafana-repo=https://github.com/grafana/grafana.git \ 46 | --github-token=${GITHUB_TOKEN} \ 47 | --destination=${local_dst} \ 48 | --yarn-cache=${YARN_CACHE_FOLDER} \ 49 | --go-version=${GO_VERSION} \ 50 | --ubuntu-base="${UBUNTU_BASE}" \ 51 | --alpine-base="${ALPINE_BASE}" > assets.txt 52 | 53 | cat assets.txt 54 | -------------------------------------------------------------------------------- /docs/guides/building.md: -------------------------------------------------------------------------------- 1 | # Build & Packaging Grafana 2 | 3 | The main goal of grafana-build is (as the name already indicates) building Grafana for various platforms. 4 | This actually consists of various parts as you need to have a binary of Grafana and the JavaScript/CSS frontend before you can then package everything up. 5 | 6 | All of that is encompassed by the `artifacts` command, which accepts a list of artifacts and attempts to create them: 7 | 8 | ``` 9 | $ dagger run go run ./cmd artifacts -a targz:grafana:linux/amd64 10 | ``` 11 | 12 | The command above will build the backend binary for Linux on an AMD64-compatible CPU and package that up into a [single archive][tarball] with the frontend artifacts: `grafana-enterprise-10.1.0-pre_lUJuyyVXnECr_linux_amd64.tar.gz` 13 | 14 | If you then extract that package (`tar -xvf *.tar.gz`) and run `./bin/grafana-server`, Grafana will launch and you will be able to access it via . 15 | 16 | ## Local checkout 17 | 18 | If you want to use a local checkout of Grafana (for instance if you want to build it to test some change you made), then set the `--grafana-dir` flag accordingly. 19 | 20 | The following command will create a binary package for `darwin/arm64` of Grafana based on a checkout inside the `$HOME/src/github.com/grafana/grafana` folder: 21 | 22 | ``` 23 | $ dagger run go run ./cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir=$HOME/src/github.com/grafana/grafana 24 | ``` 25 | 26 | ## Platform packages 27 | 28 | Now that you have a Grafana tarball with the main binaries and the frontend assets you can continue creating a package for your target distribution, or skip the tarball step and go straight to your package installer of choice. 29 | grafana-build supports a handful of these specific [artifact types](../artifact-types/index.md) but for this tutorial let's build a [Debian package][deb]: 30 | 31 | ``` 32 | $ dagger run go run ./cmd artifacts -a deb:grafana:linux/amd64 --grafana-dir=$HOME/src/github.com/grafana/grafana 33 | ``` 34 | 35 | This will produce `grafana_10.1.0-pre_lUJuyyVXnECr_linux_amd64.deb` within the `dist` folder. 36 | 37 | [tarball]: ../artifact-types/tarball.md 38 | [deb]: ../artifact-types/deb.md 39 | -------------------------------------------------------------------------------- /gcom/publish.go: -------------------------------------------------------------------------------- 1 | package gcom 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "dagger.io/dagger" 9 | ) 10 | 11 | type GCOMVersionPayload struct { 12 | Version string `json:"version"` 13 | ReleaseDate string `json:"releaseDate"` 14 | Stable bool `json:"stable"` 15 | Beta bool `json:"beta"` 16 | Nightly bool `json:"nightly"` 17 | WhatsNewURL string `json:"whatsNewUrl"` 18 | ReleaseNotesURL string `json:"releaseNotesUrl"` 19 | } 20 | 21 | type GCOMPackagePayload struct { 22 | OS string `json:"os"` 23 | URL string `json:"url"` 24 | Sha256 string `json:"sha256"` 25 | Arch string `json:"arch"` 26 | } 27 | 28 | // PublishGCOMVersion publishes a version to grafana.com. 29 | func PublishGCOMVersion(ctx context.Context, d *dagger.Client, versionPayload *GCOMVersionPayload, opts *GCOMOpts) (string, error) { 30 | versionApiUrl := opts.URL.JoinPath("/versions") 31 | 32 | jsonVersionPayload, err := json.Marshal(versionPayload) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | apiKeySecret := d.SetSecret("gcom-api-key", opts.ApiKey) 38 | 39 | return d.Container().From("alpine/curl"). 40 | WithSecretVariable("GCOM_API_KEY", apiKeySecret). 41 | WithExec([]string{"/bin/sh", "-c", fmt.Sprintf(`curl -H "Content-Type: application/json" -H "Authorization: Bearer $GCOM_API_KEY" -d '%s' %s`, string(jsonVersionPayload), versionApiUrl.String())}). 42 | Stdout(ctx) 43 | } 44 | 45 | // PublishGCOMPackage publishes a package to grafana.com. 46 | func PublishGCOMPackage(ctx context.Context, d *dagger.Client, packagePayload *GCOMPackagePayload, opts *GCOMOpts, version string) (string, error) { 47 | packagesApiUrl := opts.URL.JoinPath("/versions/", version, "/packages") 48 | 49 | jsonPackagePayload, err := json.Marshal(packagePayload) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | apiKeySecret := d.SetSecret("gcom-api-key", opts.ApiKey) 55 | 56 | return d.Container().From("alpine/curl"). 57 | WithSecretVariable("GCOM_API_KEY", apiKeySecret). 58 | WithExec([]string{"/bin/sh", "-c", fmt.Sprintf(`curl -H "Content-Type: application/json" -H "Authorization: Bearer $GCOM_API_KEY" -d '%s' %s`, string(jsonPackagePayload), packagesApiUrl.String())}). 59 | Stdout(ctx) 60 | } 61 | -------------------------------------------------------------------------------- /arguments/hg_docker.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/grafana/grafana-build/cliutil" 8 | "github.com/grafana/grafana-build/pipeline" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var HGDirectoryFlags = []cli.Flag{ 13 | &cli.StringFlag{ 14 | Name: "hosted-grafana-dir", 15 | Usage: "Local clone of HG to use, instead of git cloning", 16 | Required: false, 17 | }, 18 | &cli.StringFlag{ 19 | Name: "hosted-grafana-repo", 20 | Usage: "https `.git` repository to use for hosted-grafana", 21 | Required: false, 22 | Value: "https://github.com/grafana/hosted-grafana.git", 23 | }, 24 | &cli.StringFlag{ 25 | Name: "hosted-grafana-ref", 26 | Usage: "git ref to checkout", 27 | Required: false, 28 | Value: "main", 29 | }, 30 | } 31 | 32 | // HGDirectory will provide the valueFunc that initializes and returns a *dagger.Directory that has a repository that has the Grafana Pro/Enterprise docker image. 33 | // Where possible, when cloning and no authentication options are provided, the valuefunc will try to use the configured github CLI for cloning. 34 | var HGDirectory = pipeline.Argument{ 35 | Name: "hg-dir", 36 | Description: "The source tree of that has the Dockerfile for Grafana Pro/Enterprise", 37 | Flags: HGDirectoryFlags, 38 | ValueFunc: hgDirectory, 39 | } 40 | 41 | type HGDirectoryOpts struct { 42 | GitHubToken string 43 | HGDir string 44 | HGRepo string 45 | HGRef string 46 | } 47 | 48 | func HGDirectoryOptsFromFlags(c cliutil.CLIContext) *HGDirectoryOpts { 49 | return &HGDirectoryOpts{ 50 | GitHubToken: c.String("github-token"), 51 | HGDir: c.String("hosted-grafana-dir"), 52 | HGRepo: c.String("hosted-grafana-repo"), 53 | HGRef: c.String("hosted-grafana-ref"), 54 | } 55 | } 56 | 57 | func hgDirectory(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) { 58 | o := HGDirectoryOptsFromFlags(opts.CLIContext) 59 | ght, err := githubToken(ctx, o.GitHubToken) 60 | if err != nil { 61 | return nil, fmt.Errorf("could not get GitHub token: %w", err) 62 | } 63 | 64 | src, err := cloneOrMount(ctx, opts.Client, o.HGDir, o.HGRepo, o.HGRef, ght) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return src, nil 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/codeowners-validator.yml: -------------------------------------------------------------------------------- 1 | name: "Codeowners Validator" 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | codeowners-validator: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Checks-out your repository, which is validated in the next step 12 | - uses: actions/checkout@v4 13 | - name: GitHub CODEOWNERS Validator 14 | uses: mszostok/codeowners-validator@7f3f5e28c6d7b8dfae5731e54ce2272ca384592f 15 | # input parameters 16 | with: 17 | # ==== GitHub Auth ==== 18 | 19 | # "The list of checks that will be executed. By default, all checks are executed. Possible values: files,owners,duppatterns,syntax" 20 | checks: "files,duppatterns,syntax" 21 | 22 | # "The comma-separated list of experimental checks that should be executed. By default, all experimental checks are turned off. Possible values: notowned,avoid-shadowing" 23 | experimental_checks: "notowned,avoid-shadowing" 24 | 25 | # The repository path in which CODEOWNERS file should be validated." 26 | repository_path: "." 27 | 28 | # Defines the level on which the application should treat check issues as failures. Defaults to warning, which treats both errors and warnings as failures, and exits with error code 3. Possible values are error and warning. Default: warning" 29 | check_failure_level: "error" 30 | 31 | # The comma-separated list of patterns that should be ignored by not-owned-checker. For example, you can specify * and as a result, the * pattern from the CODEOWNERS file will be ignored and files owned by this pattern will be reported as unowned unless a later specific pattern will match that path. It's useful because often we have default owners entry at the begging of the CODOEWNERS file, e.g. * @global-owner1 @global-owner2" 32 | not_owned_checker_skip_patterns: "" 33 | 34 | # Specifies whether CODEOWNERS may have unowned files. For example, `/infra/oncall-rotator/oncall-config.yml` doesn't have owner and this is not reported. 35 | owner_checker_allow_unowned_patterns: "false" 36 | 37 | # Specifies whether only teams are allowed as owners of files. 38 | owner_checker_owners_must_be_teams: "false" 39 | -------------------------------------------------------------------------------- /containers/package_input.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "path/filepath" 8 | "strings" 9 | 10 | "dagger.io/dagger" 11 | "github.com/grafana/grafana-build/cliutil" 12 | ) 13 | 14 | type PackageInputOpts struct { 15 | // Name is used when overriding the artifact that is being produced. This is used in very specific scenarios where 16 | // the source package's name does not match the package's metadata name. 17 | Name string 18 | Packages []string 19 | } 20 | 21 | func PackageInputOptsFromFlags(c cliutil.CLIContext) *PackageInputOpts { 22 | return &PackageInputOpts{ 23 | Name: c.String("name"), 24 | Packages: c.StringSlice("package"), 25 | } 26 | } 27 | 28 | // GetPackage uses the PackageInputOpts to get a Grafana package, either from the local filesystem (if the package is of type 'file://...') 29 | // or Google Cloud Storage if the package is a 'gs://' URL. 30 | func GetPackages(ctx context.Context, d *dagger.Client, packageOpts *PackageInputOpts, gcpOpts *GCPOpts) ([]*dagger.File, error) { 31 | files := make([]*dagger.File, len(packageOpts.Packages)) 32 | for i, pkg := range packageOpts.Packages { 33 | u, err := url.Parse(pkg) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | var file *dagger.File 39 | switch u.Scheme { 40 | case "file", "fs": 41 | p := strings.TrimPrefix(u.String(), u.Scheme+"://") 42 | f, err := getLocalPackage(ctx, d, p) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | file = f 48 | case "gs": 49 | f, err := getGCSPackage(ctx, d, gcpOpts, u.String()) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | file = f 55 | default: 56 | return nil, fmt.Errorf("%w: %s", ErrorUnrecognizedScheme, u.Scheme) 57 | } 58 | 59 | files[i] = file 60 | } 61 | 62 | return files, nil 63 | } 64 | 65 | func getLocalPackage(ctx context.Context, d *dagger.Client, file string) (*dagger.File, error) { 66 | // pending https://github.com/dagger/dagger/issues/4745 67 | return d.Host().Directory(filepath.Dir(file)).File(filepath.Base(file)), nil 68 | } 69 | 70 | func getGCSPackage(ctx context.Context, d *dagger.Client, opts *GCPOpts, gcsURL string) (*dagger.File, error) { 71 | auth := GCSAuth(d, opts) 72 | return GCSDownloadFile(d, GoogleCloudImage, auth, gcsURL) 73 | } 74 | -------------------------------------------------------------------------------- /scripts/drone_publish_nightly_grafana.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | # ver=$(cat ${GRAFANA_DIR}/package.json | jq -r .version | sed -E "s/$/-/" | sed -E "s/-.*/-${DRONE_BUILD_NUMBER}/") 4 | local_dir="${DRONE_WORKSPACE}/dist" 5 | 6 | # Publish the docker images present in the bucket 7 | dagger run --silent go run ./cmd docker publish \ 8 | $(find $local_dir | grep docker.tar.gz | grep -v sha256 | awk '{print "--package=file://"$0}') \ 9 | --username=${DOCKER_USERNAME} \ 10 | --password=${DOCKER_PASSWORD} \ 11 | --repo="grafana-dev" 12 | 13 | # Publish packages to the downloads bucket 14 | dagger run --silent go run ./cmd package publish \ 15 | $(find $local_dir | grep -e .rpm -e .tar.gz -e .exe -e .zip -e .deb | awk '{print "--package=file://"$0}') \ 16 | --gcp-service-account-key-base64=${GCP_KEY_BASE64} \ 17 | --destination="${DOWNLOADS_DESTINATION}/oss/release" 18 | 19 | # Publish only the linux/amd64 edition storybook into the storybook bucket 20 | # dagger run --silent go run ./cmd storybook \ 21 | # $(find $local_dir | grep tar.gz | grep linux | grep amd64 | grep -v sha256 | grep -v docker | awk '{print "--package=file://"$0}') \ 22 | # --gcp-service-account-key-base64=${GCP_KEY_BASE64} \ 23 | # --destination="${STORYBOOK_DESTINATION}/${ver}" 24 | 25 | # # Publish only the linux/amd64 edition static assets into the static assets bucket 26 | # dagger run --silent go run ./cmd cdn \ 27 | # $(find $local_dir | grep tar.gz | grep linux | grep amd64 | grep -v sha256 | grep -v docker | awk '{print "--package=file://"$0}') \ 28 | # --gcp-service-account-key-base64=${GCP_KEY_BASE64} \ 29 | # --destination="${CDN_DESTINATION}/${ver}/public" 30 | 31 | # Publish only the linux/amd64 edition npm packages to npm 32 | dagger run --silent go run ./cmd npm publish \ 33 | $(find $local_dir | grep tar.gz | grep linux | grep amd64 | grep -v sha256 | grep -v docker | awk '{print "--package=file://"$0}') \ 34 | --token=${NPM_TOKEN} \ 35 | --tag="nightly" 36 | 37 | # Publish packages to grafana.com 38 | dagger run --silent go run ./cmd gcom publish \ 39 | $(find $local_dir | grep -e .rpm -e .tar.gz -e .exe -e .zip -e .deb | grep -v sha256 | grep -v docker | awk '{print "--package=file://"$0}') \ 40 | --api-key=${GCOM_API_KEY} \ 41 | --api-url="https://grafana.com/api/grafana" \ 42 | --download-url="https://dl.grafana.com/oss/release" \ 43 | --nightly -------------------------------------------------------------------------------- /docker/tags.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "text/template" 8 | 9 | "github.com/grafana/grafana-build/backend" 10 | "github.com/grafana/grafana-build/packages" 11 | ) 12 | 13 | type BaseImage int 14 | 15 | const ( 16 | BaseImageUbuntu BaseImage = iota 17 | BaseImageAlpine 18 | ) 19 | 20 | const ( 21 | DefaultTagFormat = "{{ .version }}-{{ .arch }}" 22 | DefaultUbuntuTagFormat = "{{ .version }}-ubuntu-{{ .arch }}" 23 | DefaultBoringTagFormat = "{{ .version }}-{{ .arch }}-boringcrypto" 24 | DefaultHGTagFormat = "{{ .version }}-{{ .arch }}" 25 | ) 26 | 27 | // Tags returns the name of the grafana docker image based on the tar package name. 28 | // To maintain backwards compatibility, we must keep this the same as it was before. 29 | func Tags(org, registry string, repos []string, format string, tarOpts packages.NameOpts) ([]string, error) { 30 | tags := make([]string, len(repos)) 31 | 32 | for i, repo := range repos { 33 | tag, err := ImageTag(tarOpts.Distro, format, registry, org, repo, tarOpts.Version, tarOpts.BuildID) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | tags[i] = tag 39 | } 40 | 41 | return tags, nil 42 | } 43 | 44 | func ImageTag(distro backend.Distribution, format, registry, org, repo, version, buildID string) (string, error) { 45 | version, err := ImageVersion(format, TemplateValues(distro, version, buildID)) 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | return fmt.Sprintf("%s/%s/%s:%s", registry, org, repo, version), nil 51 | } 52 | 53 | func ImageVersion(format string, values map[string]string) (string, error) { 54 | tmpl, err := template.New("version").Parse(format) 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | buf := bytes.NewBuffer(nil) 60 | if err := tmpl.Execute(buf, values); err != nil { 61 | return "", err 62 | } 63 | 64 | return buf.String(), nil 65 | } 66 | 67 | func TemplateValues(distro backend.Distribution, version, buildID string) map[string]string { 68 | arch := backend.FullArch(distro) 69 | arch = strings.ReplaceAll(arch, "/", "") 70 | arch = strings.ReplaceAll(arch, "dynamic", "") 71 | ersion := strings.TrimPrefix(version, "v") 72 | 73 | semverc := strings.Split(ersion, "-") 74 | return map[string]string{ 75 | "arch": arch, 76 | "version": ersion, 77 | "version_base": semverc[0], 78 | "buildID": buildID, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /backend/build.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path" 7 | "strings" 8 | 9 | "dagger.io/dagger" 10 | ) 11 | 12 | type LDFlag struct { 13 | Name string 14 | Values []string 15 | } 16 | 17 | func GoLDFlags(flags []LDFlag) string { 18 | ldflags := strings.Builder{} 19 | for _, v := range flags { 20 | if v.Values == nil { 21 | ldflags.WriteString(v.Name + " ") 22 | continue 23 | } 24 | 25 | for _, value := range v.Values { 26 | // For example, "-X 'main.version=v1.0.0'" 27 | ldflags.WriteString(fmt.Sprintf(`%s \"%s\" `, v.Name, value)) 28 | } 29 | } 30 | 31 | return ldflags.String() 32 | } 33 | 34 | // GoBuildCommand returns the arguments for go build to be used in 'WithExec'. 35 | func GoBuildCommand(output string, ldflags []LDFlag, tags []string, main string) []string { 36 | args := []string{"go", "build", "-v", "-x", 37 | fmt.Sprintf("-ldflags=\"%s\"", GoLDFlags(ldflags)), 38 | fmt.Sprintf("-o=%s", output), 39 | "-trimpath", 40 | fmt.Sprintf("-tags=%s", strings.Join(tags, ",")), 41 | // Go is weird and paths referring to packages within a module to be prefixed with "./". 42 | // Otherwise, the path is assumed to be relative to $GOROOT 43 | "./" + main, 44 | } 45 | 46 | return args 47 | } 48 | 49 | func Build( 50 | d *dagger.Client, 51 | builder *dagger.Container, 52 | src *dagger.Directory, 53 | distro Distribution, 54 | out string, 55 | opts *BuildOpts, 56 | ) *dagger.Directory { 57 | vcsinfo := GetVCSInfo(src, opts.Version, opts.Enterprise) 58 | builder = WithVCSInfo(builder, vcsinfo, opts.Enterprise) 59 | 60 | ldflags := LDFlagsDynamic(vcsinfo) 61 | 62 | if opts.Static { 63 | ldflags = LDFlagsStatic(vcsinfo) 64 | } 65 | 66 | cmd := []string{ 67 | "grafana", 68 | "grafana-server", 69 | "grafana-cli", 70 | "grafana-example-apiserver", 71 | } 72 | 73 | os, _ := OSAndArch(distro) 74 | 75 | for _, v := range cmd { 76 | // Some CLI packages such as grafana-example-apiserver don't exist in earlier Grafana Versions <10.3 77 | // Below check skips building them as needed 78 | pkgPath := path.Join("pkg", "cmd", v) 79 | out := path.Join(out, v) 80 | if os == "windows" { 81 | out += ".exe" 82 | } 83 | 84 | cmd := GoBuildCommand(out, ldflags, opts.Tags, pkgPath) 85 | 86 | script := fmt.Sprintf(`if [ -d %s ]; then %s; fi`, pkgPath, strings.Join(cmd, " ")) 87 | log.Printf("Building with command '%s'", script) 88 | 89 | builder = builder. 90 | WithExec([]string{"/bin/sh", "-c", script}) 91 | } 92 | 93 | return builder.Directory(out) 94 | } 95 | -------------------------------------------------------------------------------- /pipelines/package_names.go: -------------------------------------------------------------------------------- 1 | package pipelines 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/grafana/grafana-build/backend" 9 | "github.com/grafana/grafana-build/packages" 10 | ) 11 | 12 | type TarFileOpts struct { 13 | // Name is the name of the product in the package. 99% of the time, this will be "grafana" or "grafana-enterprise". 14 | Name string 15 | Version string 16 | BuildID string 17 | // Edition is the flavor text after "grafana-", like "enterprise". 18 | Edition string 19 | Distro backend.Distribution 20 | Suffix string 21 | } 22 | 23 | func (opts *TarFileOpts) NameOpts() packages.NameOpts { 24 | return packages.NameOpts{ 25 | // Name is the name of the product in the package. 99% of the time, this will be "grafana" or "grafana-enterprise". 26 | Name: packages.Name(opts.Name), 27 | Version: opts.Version, 28 | BuildID: opts.BuildID, 29 | Distro: opts.Distro, 30 | } 31 | } 32 | 33 | func WithoutExt(name string) string { 34 | ext := filepath.Ext(name) 35 | n := strings.TrimSuffix(name, ext) 36 | 37 | // Explicitly handle `.gz` which might will also probably have a `.tar` extension as well. 38 | if ext == ".gz" { 39 | n = strings.TrimSuffix(n, ".ubuntu.docker.tar") 40 | n = strings.TrimSuffix(n, ".docker.tar") 41 | n = strings.TrimSuffix(n, ".tar") 42 | } 43 | 44 | return n 45 | } 46 | 47 | func TarOptsFromFileName(filename string) TarFileOpts { 48 | filename = filepath.Base(filename) 49 | n := WithoutExt(filename) 50 | components := strings.Split(n, "_") 51 | if len(components) != 5 { 52 | return TarFileOpts{} 53 | } 54 | 55 | var ( 56 | name = components[0] 57 | version = components[1] 58 | buildID = components[2] 59 | os = components[3] 60 | arch = components[4] 61 | ) 62 | if archv := strings.Split(arch, "-"); len(archv) == 2 { 63 | // The reverse operation of removing the 'v' for 'arm' because the golang environment variable 64 | // GOARM doesn't want it, but the docker --platform flag either doesn't care or does want it. 65 | if archv[0] == "arm" { 66 | archv[1] = "v" + archv[1] 67 | } 68 | 69 | // arm-7 should become arm/v7 70 | arch = strings.Join([]string{archv[0], archv[1]}, "/") 71 | } 72 | edition := "" 73 | suffix := "" 74 | if n := strings.Split(name, "-"); len(n) != 1 { 75 | edition = strings.Join(n[1:], "-") 76 | suffix = fmt.Sprintf("-%s", n[1]) 77 | } 78 | 79 | return TarFileOpts{ 80 | Name: name, 81 | Edition: edition, 82 | Version: version, 83 | BuildID: buildID, 84 | Distro: backend.Distribution(strings.Join([]string{os, arch}, "/")), 85 | Suffix: suffix, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docker/build.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "dagger.io/dagger" 7 | "github.com/grafana/grafana-build/containers" 8 | ) 9 | 10 | type BuildOpts struct { 11 | // Dockerfile is the path to the dockerfile with the '-f' command. 12 | // If it's not provided, then the docker command will default to 'Dockerfile' in `pwd`. 13 | Dockerfile string 14 | 15 | // Tags are provided as the '-t' argument, and can include the registry domain as well as the repository. 16 | // Docker build supports building the same image with multiple tags. 17 | // You might want to also include a 'latest' version of the tag. 18 | Tags []string 19 | // BuildArgs are provided to the docker command as '--build-arg' 20 | BuildArgs []string 21 | // Set the target build stage to build as '--target' 22 | Target string 23 | 24 | // Platform, if set to the non-default value, will use buildkit's emulation to build the docker image. This can be useful if building a docker image for a platform that doesn't match the host platform. 25 | Platform dagger.Platform 26 | } 27 | 28 | func Builder(d *dagger.Client, socket *dagger.Socket, targz *dagger.File) *dagger.Container { 29 | extracted := containers.ExtractedArchive(d, targz) 30 | 31 | // Instead of supplying the Platform argument here, we need to tell the host docker socket that it needs to build with the given platform. 32 | return d.Container().From("docker"). 33 | WithUnixSocket("/var/run/docker.sock", socket). 34 | WithWorkdir("/src"). 35 | WithMountedFile("/src/Dockerfile", extracted.File("Dockerfile")). 36 | WithMountedFile("/src/packaging/docker/run.sh", extracted.File("packaging/docker/run.sh")). 37 | WithMountedFile("/src/grafana.tar.gz", targz) 38 | } 39 | 40 | func Build(d *dagger.Client, builder *dagger.Container, opts *BuildOpts) *dagger.Container { 41 | args := []string{"docker", "buildx", "build"} 42 | if p := opts.Platform; p != "" { 43 | args = append(args, fmt.Sprintf("--platform=%s", string(p))) 44 | } 45 | dockerfile := opts.Dockerfile 46 | if dockerfile == "" { 47 | dockerfile = "Dockerfile" 48 | } 49 | 50 | args = append(args, ".", "-f", dockerfile) 51 | 52 | for _, v := range opts.BuildArgs { 53 | args = append(args, fmt.Sprintf("--build-arg=%s", v)) 54 | } 55 | 56 | for _, v := range opts.Tags { 57 | args = append(args, "-t", v) 58 | } 59 | 60 | if opts.Target != "" { 61 | args = append(args, "--target", opts.Target) 62 | } 63 | 64 | return builder.WithExec(args) 65 | } 66 | 67 | func Save(builder *dagger.Container, opts *BuildOpts) *dagger.File { 68 | return builder.WithExec([]string{"docker", "save", opts.Tags[0], "-o", "image.tar.gz"}).File("image.tar.gz") 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/pr-integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: PR Integration Tests 2 | 3 | on: 4 | workflow_dispatch: {} 5 | 6 | jobs: 7 | grafana-integration-tests: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | version: [main, v11.2.x, v11.1.x, v11.0.x, v10.4.x, v10.3.x] 12 | type: [grafana] 13 | # TODO: figure out enterprise auth 14 | # type: [grafana, enterprise] 15 | permissions: 16 | id-token: write 17 | contents: read 18 | steps: 19 | - name: Checkout grafana-build 20 | uses: actions/checkout@v4 21 | - name: Checkout grafana 22 | uses: actions/checkout@v4 23 | with: 24 | repository: grafana/grafana 25 | ref: ${{ matrix.version }} 26 | path: grafana 27 | - name: Checkout grafana-enterprise 28 | if: matrix.type == 'enterprise' 29 | uses: actions/checkout@v4 30 | with: 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | repository: grafana/grafana-enterprise 33 | ref: ${{ matrix.version }} 34 | path: grafana-enterprise 35 | - name: Clean runner 36 | run: | 37 | df -h 38 | docker builder prune -f 39 | docker system prune -a -f 40 | sudo rm -rf /opt/google/chrome 41 | sudo rm -rf /opt/microsoft/msedge 42 | sudo rm -rf /opt/microsoft/powershell 43 | sudo rm -rf /opt/pipx 44 | sudo rm -rf /usr/lib/mono 45 | sudo rm -rf /usr/local/julia* 46 | sudo rm -rf /usr/local/lib/android 47 | sudo rm -rf /usr/local/lib/node_modules 48 | sudo rm -rf /usr/local/share/chromium 49 | sudo rm -rf /usr/local/share/powershell 50 | sudo rm -rf /usr/share/dotnet 51 | sudo rm -rf /usr/share/swift 52 | df -h 53 | - name: Get Grafana golang version 54 | run: echo "GRAFANA_GO_VERSION=$(grep "go 1." grafana/go.work | cut -d\ -f2)" >> "$GITHUB_ENV" 55 | - name: Grafana tests 56 | uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e 57 | if: matrix.type == 'grafana' 58 | with: 59 | verb: run 60 | dagger-flags: '--quiet' 61 | args: go run ./cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir=grafana --go-version=${GRAFANA_GO_VERSION} 62 | - name: Enterprise tests 63 | uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e 64 | if: matrix.type == 'enterprise' 65 | with: 66 | verb: run 67 | dagger-flags: '--quiet' 68 | args: go run ./cmd artifacts -a targz:grafana-enterprise:linux/amd64 --grafana-dir=grafana --enterprise-dir=grafana-enterprise --go-version=${GRAFANA_GO_VERSION} 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/grafana-build 2 | 3 | go 1.23.8 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | dagger.io/dagger v0.18.6 9 | github.com/Masterminds/semver v1.5.0 10 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 11 | github.com/stretchr/testify v1.10.0 12 | github.com/urfave/cli/v2 v2.27.6 13 | go.opentelemetry.io/otel v1.35.0 14 | go.opentelemetry.io/otel/trace v1.35.0 15 | golang.org/x/sync v0.14.0 16 | ) 17 | 18 | require ( 19 | github.com/99designs/gqlgen v0.17.73 // indirect 20 | github.com/Khan/genqlient v0.8.0 // indirect 21 | github.com/adrg/xdg v0.5.3 // indirect 22 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 23 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/go-logr/logr v1.4.2 // indirect 26 | github.com/go-logr/stdr v1.2.2 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect 29 | github.com/mitchellh/go-homedir v1.1.0 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 32 | github.com/sosodev/duration v1.3.1 // indirect 33 | github.com/vektah/gqlparser/v2 v2.5.27 // indirect 34 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 35 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 36 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 // indirect 37 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 // indirect 38 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect 39 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect 40 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect 41 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect 42 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect 43 | go.opentelemetry.io/otel/log v0.11.0 // indirect 44 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 45 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 46 | go.opentelemetry.io/otel/sdk/log v0.11.0 // indirect 47 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 48 | go.opentelemetry.io/proto/otlp v1.6.0 // indirect 49 | golang.org/x/net v0.40.0 // indirect 50 | golang.org/x/sys v0.33.0 // indirect 51 | golang.org/x/text v0.25.0 // indirect 52 | google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect 53 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect 54 | google.golang.org/grpc v1.72.0 // indirect 55 | google.golang.org/protobuf v1.36.6 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /packages/names_test.go: -------------------------------------------------------------------------------- 1 | package packages_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/grafana-build/backend" 7 | "github.com/grafana/grafana-build/packages" 8 | ) 9 | 10 | func TestFileName(t *testing.T) { 11 | t.Run("It should use the correct name if Enterprise is false", func(t *testing.T) { 12 | distro := backend.Distribution("plan9/amd64") 13 | opts := packages.NameOpts{ 14 | Name: "grafana", 15 | Version: "v1.0.1-test", 16 | BuildID: "333", 17 | Distro: distro, 18 | Extension: "tar.gz", 19 | } 20 | 21 | expected := "grafana_v1.0.1-test_333_plan9_amd64.tar.gz" 22 | if name, _ := packages.FileName(opts.Name, opts.Version, opts.BuildID, opts.Distro, opts.Extension); name != expected { 23 | t.Errorf("name '%s' does not match expected name '%s'", name, expected) 24 | } 25 | }) 26 | t.Run("It should use the correct name if Enterprise is true", func(t *testing.T) { 27 | distro := backend.Distribution("plan9/amd64") 28 | opts := packages.NameOpts{ 29 | Name: "grafana-enterprise", 30 | Version: "v1.0.1-test", 31 | BuildID: "333", 32 | Distro: distro, 33 | Extension: "tar.gz", 34 | } 35 | 36 | expected := "grafana-enterprise_v1.0.1-test_333_plan9_amd64.tar.gz" 37 | if name, _ := packages.FileName(opts.Name, opts.Version, opts.BuildID, opts.Distro, opts.Extension); name != expected { 38 | t.Errorf("name '%s' does not match expected name '%s'", name, expected) 39 | } 40 | }) 41 | t.Run("It should use include the arch version if one is supplied in the distro", func(t *testing.T) { 42 | distro := backend.Distribution("plan9/arm/v6") 43 | opts := packages.NameOpts{ 44 | Name: "grafana-enterprise", 45 | Version: "v1.0.1-test", 46 | BuildID: "333", 47 | Distro: distro, 48 | Extension: "tar.gz", 49 | } 50 | 51 | expected := "grafana-enterprise_v1.0.1-test_333_plan9_arm-6.tar.gz" 52 | if name, _ := packages.FileName(opts.Name, opts.Version, opts.BuildID, opts.Distro, opts.Extension); name != expected { 53 | t.Errorf("name '%s' does not match expected name '%s'", name, expected) 54 | } 55 | }) 56 | t.Run("It should support grafana names with multiple hyphens", func(t *testing.T) { 57 | distro := backend.Distribution("plan9/arm/v6") 58 | opts := packages.NameOpts{ 59 | Name: "grafana-enterprise-rpi", 60 | Version: "v1.0.1-test", 61 | BuildID: "333", 62 | Distro: distro, 63 | Extension: "tar.gz", 64 | } 65 | 66 | expected := "grafana-enterprise-rpi_v1.0.1-test_333_plan9_arm-6.tar.gz" 67 | if name, _ := packages.FileName(opts.Name, opts.Version, opts.BuildID, opts.Distro, opts.Extension); name != expected { 68 | t.Errorf("name '%s' does not match expected name '%s'", name, expected) 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pipeline/state_log.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "dagger.io/dagger" 8 | ) 9 | 10 | type StateLogger struct { 11 | Log *slog.Logger 12 | Handler StateHandler 13 | } 14 | 15 | func (s *StateLogger) String(ctx context.Context, arg Argument) (string, error) { 16 | s.Log.Debug("Getting string from state", "arg", arg.Name) 17 | val, err := s.Handler.String(ctx, arg) 18 | if err != nil { 19 | s.Log.Error("Error getting string from state", "arg", arg.Name, "error", err) 20 | } 21 | s.Log.Debug("Got string from state", "arg", arg.Name) 22 | 23 | return val, err 24 | } 25 | func (s *StateLogger) Int64(ctx context.Context, arg Argument) (int64, error) { 26 | s.Log.Debug("Getting int64 from state", "arg", arg.Name) 27 | val, err := s.Handler.Int64(ctx, arg) 28 | if err != nil { 29 | s.Log.Error("Error getting int64 from state", "arg", arg.Name, "error", err) 30 | } 31 | s.Log.Debug("Got int64 from state", "arg", arg.Name) 32 | 33 | return val, err 34 | } 35 | func (s *StateLogger) Bool(ctx context.Context, arg Argument) (bool, error) { 36 | s.Log.Debug("Getting bool from state", "arg", arg.Name) 37 | val, err := s.Handler.Bool(ctx, arg) 38 | if err != nil { 39 | s.Log.Error("Error getting bool from state", "arg", arg.Name, "error", err) 40 | } 41 | s.Log.Debug("Got bool from state", "arg", arg.Name) 42 | 43 | return val, err 44 | } 45 | func (s *StateLogger) File(ctx context.Context, arg Argument) (*dagger.File, error) { 46 | s.Log.Debug("Getting file from state", "arg", arg.Name) 47 | val, err := s.Handler.File(ctx, arg) 48 | if err != nil { 49 | s.Log.Error("Error getting file from state", "arg", arg.Name, "error", err) 50 | } 51 | s.Log.Debug("Got file from state", "arg", arg.Name) 52 | 53 | return val, err 54 | } 55 | func (s *StateLogger) Directory(ctx context.Context, arg Argument) (*dagger.Directory, error) { 56 | s.Log.Debug("Getting directory from state", "arg", arg.Name) 57 | val, err := s.Handler.Directory(ctx, arg) 58 | if err != nil { 59 | s.Log.Error("Error getting directory from state", "arg", arg.Name, "error", err) 60 | } 61 | s.Log.Debug("Got directory from state", "arg", arg.Name) 62 | 63 | return val, err 64 | } 65 | func (s *StateLogger) CacheVolume(ctx context.Context, arg Argument) (*dagger.CacheVolume, error) { 66 | s.Log.Debug("Getting cache volume from state", "arg", arg.Name) 67 | val, err := s.Handler.CacheVolume(ctx, arg) 68 | if err != nil { 69 | s.Log.Error("Error getting cache volume from state", "arg", arg.Name, "error", err) 70 | } 71 | s.Log.Debug("Got cache volume from state", "arg", arg.Name) 72 | 73 | return val, err 74 | } 75 | 76 | func StateWithLogger(log *slog.Logger, s StateHandler) StateHandler { 77 | return &StateLogger{ 78 | Log: log, 79 | Handler: s, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /frontend/npm.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | 8 | "dagger.io/dagger" 9 | "github.com/grafana/grafana-build/containers" 10 | ) 11 | 12 | // NPMPackages versions and packs the npm packages into tarballs into `npm-packages` directory. 13 | // It then returns the npm-packages directory as a dagger.Directory. 14 | func NPMPackages(builder *dagger.Container, d *dagger.Client, log *slog.Logger, src *dagger.Directory, ersion string) (*dagger.Directory, error) { 15 | // Check if the version of Grafana uses lerna or nx to manage package versioning. 16 | var ( 17 | out = fmt.Sprintf("/src/npm-packages/%%s-%v.tgz", "v"+ersion) 18 | 19 | lernaBuild = fmt.Sprintf("yarn run packages:build && yarn lerna version %s --exact --no-git-tag-version --no-push --force-publish -y", ersion) 20 | lernaPack = fmt.Sprintf("yarn lerna exec --no-private -- yarn pack --out %s", out) 21 | 22 | nxBuild = fmt.Sprintf("yarn run packages:build && yarn nx release version %s --no-git-commit --no-git-tag --no-stage-changes --group grafanaPackages", ersion) 23 | nxPack = fmt.Sprintf("yarn nx exec --projects=$(cat nx.json | jq -r '.relase.groups.grafanaPackages.projects | join(\",\")') -- yarn pack --out %s", out) 24 | ) 25 | 26 | return builder.WithExec([]string{"mkdir", "npm-packages"}). 27 | WithEnvVariable("SHELL", "/bin/bash"). 28 | WithExec([]string{"yarn", "install", "--immutable"}). 29 | WithExec([]string{"/bin/bash", "-c", fmt.Sprintf("if [ -f lerna.json ]; then %s; else %s; fi", lernaBuild, nxBuild)}). 30 | WithExec([]string{"/bin/bash", "-c", fmt.Sprintf("if [ -f lerna.json ]; then %s; else %s; fi", lernaPack, nxPack)}). 31 | Directory("./npm-packages"), nil 32 | } 33 | 34 | // PublishNPM publishes a npm package to the given destination. 35 | func PublishNPM(ctx context.Context, d *dagger.Client, pkg *dagger.File, token, registry string, tags []string) (string, error) { 36 | src := containers.ExtractedArchive(d, pkg) 37 | 38 | version, err := containers.GetJSONValue(ctx, d, src, "package.json", "version") 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | name, err := containers.GetJSONValue(ctx, d, src, "package.json", "name") 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | tokenSecret := d.SetSecret("npm-token", token) 49 | 50 | c := d.Container().From(NodeImage("lts")). 51 | WithFile("/pkg.tgz", pkg). 52 | WithSecretVariable("NPM_TOKEN", tokenSecret). 53 | WithExec([]string{"/bin/sh", "-c", fmt.Sprintf("npm set //%s/:_authToken $NPM_TOKEN", registry)}). 54 | WithExec([]string{"npm", "publish", "/pkg.tgz", fmt.Sprintf("--registry https://%s", registry), "--tag", tags[0]}) 55 | 56 | for _, tag := range tags[1:] { 57 | c = c.WithExec([]string{"npm", "dist-tag", "add", fmt.Sprintf("%s@%s", name, version), tag}) 58 | } 59 | 60 | return c.Stdout(ctx) 61 | } 62 | -------------------------------------------------------------------------------- /pipeline/flag.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type FlagOption string 10 | 11 | // A Flag is a single component of an artifact string. 12 | // For example, in the artifact string `linux/amd64:targz:enterprise`, the flags are 13 | // `linux/amd64`, `targz`, and `enterprise`. Artifacts define what flags are allowed to be set on them, and handle applying those flags 14 | // in their constructors. 15 | type Flag struct { 16 | Name string 17 | Options map[FlagOption]any 18 | } 19 | 20 | // OptionsHandler is used for storing and setting options populated from artifact flags in a map. 21 | type OptionsHandler struct { 22 | Artifact string 23 | Options map[FlagOption]any 24 | } 25 | 26 | func NewOptionsHandler(artifact string) *OptionsHandler { 27 | return &OptionsHandler{ 28 | Artifact: artifact, 29 | Options: map[FlagOption]any{}, 30 | } 31 | } 32 | 33 | var ( 34 | ErrorDuplicateFlagOption = errors.New("another flag has already set this option") 35 | ErrorFlagOptionNotFound = errors.New("no flag provided the requested option") 36 | ) 37 | 38 | func (o *OptionsHandler) Apply(flag Flag) error { 39 | for k, v := range flag.Options { 40 | if _, ok := o.Options[k]; ok { 41 | return fmt.Errorf("flag: %s, option: %s, error: %w", flag.Name, k, ErrorDuplicateFlagOption) 42 | } 43 | o.Options[k] = v 44 | } 45 | return nil 46 | } 47 | 48 | func (o *OptionsHandler) Get(option FlagOption) (any, error) { 49 | val, ok := o.Options[option] 50 | if !ok { 51 | return "", fmt.Errorf("[%s] %s: %w", o.Artifact, option, ErrorFlagOptionNotFound) 52 | } 53 | 54 | return val, nil 55 | } 56 | 57 | func (o *OptionsHandler) String(option FlagOption) (string, error) { 58 | v, err := o.Get(option) 59 | if err != nil { 60 | return "", err 61 | } 62 | 63 | return v.(string), nil 64 | } 65 | 66 | func (o *OptionsHandler) StringSlice(option FlagOption) ([]string, error) { 67 | v, err := o.Get(option) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return v.([]string), nil 73 | } 74 | 75 | func (o *OptionsHandler) Bool(option FlagOption) (bool, error) { 76 | v, err := o.Get(option) 77 | if err != nil { 78 | if errors.Is(err, ErrorFlagOptionNotFound) { 79 | return false, nil 80 | } 81 | 82 | return false, err 83 | } 84 | 85 | return v.(bool), nil 86 | } 87 | 88 | func ParseFlags(artifact string, flags []Flag) (*OptionsHandler, error) { 89 | h := NewOptionsHandler(artifact) 90 | f := strings.Split(artifact, ":") 91 | 92 | for _, v := range f { 93 | for _, flag := range flags { 94 | if flag.Name != v { 95 | continue 96 | } 97 | 98 | if err := h.Apply(flag); err != nil { 99 | return nil, err 100 | } 101 | } 102 | } 103 | 104 | return h, nil 105 | } 106 | -------------------------------------------------------------------------------- /artifacts/parse_args.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "strings" 9 | 10 | "github.com/grafana/grafana-build/pipeline" 11 | ) 12 | 13 | var ( 14 | ErrorArtifactCollision = errors.New("artifact argument specifies two different artifacts") 15 | ErrorDuplicateArgument = errors.New("artifact argument specifies duplicate or incompatible arguments") 16 | ErrorNoArtifact = errors.New("could not find compatible artifact for argument string") 17 | 18 | ErrorFlagNotFound = errors.New("no option available for the given flag") 19 | ) 20 | 21 | func findInitializer(val string, initializers map[string]Initializer) (Initializer, error) { 22 | c := strings.Split(val, ":") 23 | var initializer *Initializer 24 | 25 | // Find the artifact that is requested by `val`. 26 | // The artifact can be defined anywhere in the artifact string. Example: `linux/amd64:grafana:targz` or `linux/amd64:grafana:targz` are the same, where targz is the artifact. 27 | for _, v := range c { 28 | n, ok := initializers[v] 29 | if !ok { 30 | continue 31 | } 32 | if initializer != nil { 33 | return Initializer{}, fmt.Errorf("%s: %w", val, ErrorArtifactCollision) 34 | } 35 | 36 | initializer = &n 37 | } 38 | 39 | if initializer == nil { 40 | return Initializer{}, fmt.Errorf("%s: %w", val, ErrorNoArtifact) 41 | } 42 | 43 | return *initializer, nil 44 | } 45 | 46 | // The ArtifactsFromStrings function should provide all of the necessary arguments to produce each artifact 47 | // dleimited by colons. It's a repeated flag, so all permutations are stored in 1 instance of the ArtifactsFlag struct. 48 | // Examples: 49 | // * targz:linux/amd64 -- Will produce a "Grafana" tar.gz for "linux/amd64". 50 | // * targz:enterprise:linux/amd64 -- Will produce a "Grafana" tar.gz for "linux/amd64". 51 | func ArtifactsFromStrings(ctx context.Context, log *slog.Logger, a []string, registered map[string]Initializer, state pipeline.StateHandler) ([]*pipeline.Artifact, error) { 52 | artifacts := make([]*pipeline.Artifact, len(a)) 53 | for i, v := range a { 54 | n, err := Parse(ctx, log, v, registered, state) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | artifacts[i] = n 60 | } 61 | 62 | return artifacts, nil 63 | } 64 | 65 | // Parse parses the artifact string `artifact` and finds the matching initializer. 66 | func Parse(ctx context.Context, log *slog.Logger, artifact string, initializers map[string]Initializer, state pipeline.StateHandler) (*pipeline.Artifact, error) { 67 | artifact = strings.TrimSpace(artifact) 68 | initializer, err := findInitializer(artifact, initializers) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | initializerFunc := initializer.InitializerFunc 74 | // TODO soon, the initializer might need more info about flags 75 | return initializerFunc(ctx, log, artifact, state) 76 | } 77 | -------------------------------------------------------------------------------- /versions/opts_test.go: -------------------------------------------------------------------------------- 1 | package versions_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/grafana-build/versions" 7 | ) 8 | 9 | func TestOptsFor(t *testing.T) { 10 | t.Run("v9.3.0 should not have combined executables", func(t *testing.T) { 11 | opts := versions.OptionsFor("v9.3.0") 12 | 13 | if opts.CombinedExecutable.IsSet != true { 14 | t.Errorf("CombinedExecutable should be set for v9.3.0") 15 | } 16 | if opts.CombinedExecutable.Value != false { 17 | t.Errorf("CombinedExecutable should be false for v9.3.0") 18 | } 19 | }) 20 | t.Run("v9.3.0 should not have packaging/autocomplete", func(t *testing.T) { 21 | opts := versions.OptionsFor("9.3.0") 22 | 23 | if opts.Autocomplete.IsSet != true { 24 | t.Errorf("Autocomplete should be set for v9.3.0") 25 | } 26 | if opts.Autocomplete.Value != false { 27 | t.Errorf("Autocomplete should be false for v9.3.0") 28 | } 29 | }) 30 | t.Run("v9.3.0-beta.1 should not have packaging/autocomplete", func(t *testing.T) { 31 | opts := versions.OptionsFor("v9.3.0-beta.1") 32 | 33 | if opts.Autocomplete.IsSet != true { 34 | t.Errorf("Autocomplete should be set for v9.3.0-beta.1") 35 | } 36 | if opts.Autocomplete.Value != false { 37 | t.Errorf("Autocomplete should be false for v9.3.0-beta.1") 38 | } 39 | }) 40 | t.Run("v10.0.1 should have packaging/autocomplete", func(t *testing.T) { 41 | opts := versions.OptionsFor("v10.0.1") 42 | 43 | if opts.Autocomplete.IsSet != true { 44 | t.Errorf("Autocomplete should be set for v10.0.1") 45 | } 46 | if opts.Autocomplete.Value != true { 47 | t.Errorf("Autocomplete should be true for v10.0.1") 48 | } 49 | }) 50 | } 51 | 52 | func TestMerge(t *testing.T) { 53 | opts1 := versions.Options{ 54 | Constraint: versions.NewNullable("^1.2.3"), 55 | DebPreRM: versions.NewNullable(true), 56 | } 57 | 58 | opts2 := versions.Options{ 59 | Constraint: versions.NewNullable("^3.2.1"), 60 | CombinedExecutable: versions.NewNullable(false), 61 | } 62 | 63 | opts3 := versions.Options{ 64 | Constraint: versions.NewNullable("^5.0.0"), 65 | } 66 | 67 | merged := versions.Merge(opts1, opts2) 68 | merged = versions.Merge(merged, opts3) 69 | t.Run("It should keep the first constraint", func(t *testing.T) { 70 | if merged.Constraint.Value != "^1.2.3" { 71 | t.Fatalf(`merged.Constraint.Value != "^1.2.3", it is '%s'`, merged.Constraint.Value) 72 | } 73 | }) 74 | 75 | t.Run("It should use the last set 'CombinedExecutable'", func(t *testing.T) { 76 | if merged.CombinedExecutable.Value != false { 77 | t.Fatalf(`merged.Constraint.Value != false it is %t`, merged.CombinedExecutable.Value) 78 | } 79 | }) 80 | 81 | t.Run("It should use the last set 'DebPreRM'", func(t *testing.T) { 82 | if merged.DebPreRM.Value != true { 83 | t.Fatalf(`merged.DebPreRM.Value != true, it is %t`, merged.DebPreRM.Value) 84 | } 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /fpm/verify.go: -------------------------------------------------------------------------------- 1 | package fpm 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "dagger.io/dagger" 8 | "github.com/grafana/grafana-build/backend" 9 | "github.com/grafana/grafana-build/containers" 10 | "github.com/grafana/grafana-build/e2e" 11 | "github.com/grafana/grafana-build/frontend" 12 | "github.com/grafana/grafana-build/gpg" 13 | ) 14 | 15 | func VerifyDeb(ctx context.Context, d *dagger.Client, file *dagger.File, src *dagger.Directory, yarn *dagger.CacheVolume, distro backend.Distribution, enterprise bool) error { 16 | nodeVersion, err := frontend.NodeVersion(d, src).Stdout(ctx) 17 | if err != nil { 18 | return fmt.Errorf("failed to get node version from source code: %w", err) 19 | } 20 | 21 | var ( 22 | platform = backend.Platform(distro) 23 | ) 24 | 25 | // This grafana service runs in the background for the e2e tests 26 | service := d.Container(dagger.ContainerOpts{ 27 | Platform: platform, 28 | }).From("debian:latest"). 29 | WithFile("/src/package.deb", file). 30 | WithExec([]string{"apt-get", "update"}). 31 | WithExec([]string{"apt-get", "install", "-y", "/src/package.deb"}). 32 | WithWorkdir("/usr/share/grafana") 33 | 34 | if err := e2e.ValidateLicense(ctx, service, "/usr/share/grafana/LICENSE", enterprise); err != nil { 35 | return err 36 | } 37 | 38 | svc := service. 39 | WithExec([]string{"grafana-server"}). 40 | WithExposedPort(3000).AsService() 41 | 42 | if _, err := containers.ExitError(ctx, e2e.ValidatePackage(d, svc, src, yarn, nodeVersion)); err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func VerifyRpm(ctx context.Context, d *dagger.Client, file *dagger.File, src *dagger.Directory, yarn *dagger.CacheVolume, distro backend.Distribution, enterprise, sign bool, pubkey, privkey, passphrase string) error { 50 | nodeVersion, err := frontend.NodeVersion(d, src).Stdout(ctx) 51 | if err != nil { 52 | return fmt.Errorf("failed to get node version from source code: %w", err) 53 | } 54 | 55 | var ( 56 | platform = backend.Platform(distro) 57 | ) 58 | 59 | // This grafana service runs in the background for the e2e tests 60 | service := d.Container(dagger.ContainerOpts{ 61 | Platform: platform, 62 | }).From("redhat/ubi8:latest"). 63 | WithFile("/src/package.rpm", file). 64 | WithExec([]string{"yum", "install", "-y", "/src/package.rpm"}). 65 | WithWorkdir("/usr/share/grafana") 66 | 67 | if err := e2e.ValidateLicense(ctx, service, "/usr/share/grafana/LICENSE", enterprise); err != nil { 68 | return err 69 | } 70 | 71 | service = service. 72 | WithExec([]string{"grafana-server"}). 73 | WithExposedPort(3000) 74 | 75 | if _, err := containers.ExitError(ctx, e2e.ValidatePackage(d, service.AsService(), src, yarn, nodeVersion)); err != nil { 76 | return err 77 | } 78 | if !sign { 79 | return nil 80 | } 81 | 82 | return gpg.VerifySignature(ctx, d, file, pubkey, privkey, passphrase) 83 | } 84 | -------------------------------------------------------------------------------- /gpg/sign.go: -------------------------------------------------------------------------------- 1 | package gpg 2 | 3 | import ( 4 | "dagger.io/dagger" 5 | ) 6 | 7 | const RPMMacros = ` 8 | %_signature gpg 9 | %_gpg_path /root/.gnupg 10 | %_gpg_name Grafana 11 | %_gpgbin /usr/bin/gpg2 12 | %__gpg_sign_cmd %{__gpg} gpg \ 13 | --batch --yes --no-armor --pinentry-mode loopback \ 14 | --passphrase-file /root/.rpmdb/passkeys/grafana.key \ 15 | --no-secmem-warning -u "%{_gpg_name}" -sbo %{__signature_filename} \ 16 | %{?_gpg_digest_algo:--digest-algo %{_gpg_digest_algo}} %{__plaintext_filename} 17 | ` 18 | 19 | type GPGOpts struct { 20 | GPGPrivateKey string 21 | GPGPublicKey string 22 | GPGPassphrase string 23 | } 24 | 25 | func Signer(d *dagger.Client, pubkey, privkey, passphrase string) *dagger.Container { 26 | var ( 27 | gpgPublicKeySecret = d.SetSecret("gpg-public-key", pubkey) 28 | gpgPrivateKeySecret = d.SetSecret("gpg-private-key", privkey) 29 | gpgPassphraseSecret = d.SetSecret("gpg-passphrase", passphrase) 30 | ) 31 | 32 | return d.Container().From("debian:stable"). 33 | WithExec([]string{"apt-get", "update"}). 34 | WithExec([]string{"apt-get", "install", "-yq", "rpm", "gnupg2", "file"}). 35 | WithMountedSecret("/root/.rpmdb/privkeys/grafana.key", gpgPrivateKeySecret). 36 | WithMountedSecret("/root/.rpmdb/pubkeys/grafana.key", gpgPublicKeySecret). 37 | WithMountedSecret("/root/.rpmdb/passkeys/grafana.key", gpgPassphraseSecret). 38 | WithExec([]string{"/bin/sh", "-c", ` 39 | echo "DEBUG: Mounted RPM Pub Key file detected to be: $(file "/root/.rpmdb/pubkeys/grafana.key")"; 40 | echo "DEBUG: Mounted RPM Pub Key file has $(wc -c "/root/.rpmdb/pubkeys/grafana.key") bytes"; 41 | echo "DEBUG: Mounted RPM Pub Key file has $(wc -l "/root/.rpmdb/pubkeys/grafana.key") lines"; 42 | if grep -q "PUBLIC KEY" "/root/.rpmdb/pubkeys/grafana.key"; then 43 | cp "/root/.rpmdb/pubkeys/grafana.key" "/tmp/grafana.key"; 44 | else 45 | gpg --enarmor "/root/.rpmdb/pubkeys/grafana.key" > "/tmp/grafana.key"; 46 | fi; 47 | if [ "$(tail -n 1 "/tmp/grafana.key" | wc -l)" = 0 ]; then 48 | echo >> "/tmp/grafana.key"; 49 | fi; 50 | echo "DEBUG: Final RPM Pub Key file has $(wc -c "/tmp/grafana.key") bytes"; 51 | echo "DEBUG: Final RPM Pub Key file has $(wc -l "/tmp/grafana.key") lines"; 52 | `}). 53 | WithExec([]string{"rpm", "--import", "/tmp/grafana.key"}). 54 | WithNewFile("/root/.rpmmacros", RPMMacros, dagger.ContainerWithNewFileOpts{ 55 | Permissions: 0400, 56 | }). 57 | WithExec([]string{"gpg", "--batch", "--yes", "--no-tty", "--allow-secret-key-import", "--import", "/root/.rpmdb/privkeys/grafana.key"}) 58 | } 59 | 60 | func Sign(d *dagger.Client, file *dagger.File, opts GPGOpts) *dagger.File { 61 | return Signer(d, opts.GPGPublicKey, opts.GPGPrivateKey, opts.GPGPassphrase). 62 | WithMountedFile("/src/package.rpm", file). 63 | WithExec([]string{"rpm", "--addsign", "/src/package.rpm"}). 64 | File("/src/package.rpm") 65 | } 66 | -------------------------------------------------------------------------------- /pipelines/pro_image.go: -------------------------------------------------------------------------------- 1 | package pipelines 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "dagger.io/dagger" 9 | "github.com/grafana/grafana-build/containers" 10 | "github.com/grafana/grafana-build/git" 11 | ) 12 | 13 | func ProImage(ctx context.Context, dc *dagger.Client, args PipelineArgs) error { 14 | if len(args.PackageInputOpts.Packages) > 1 { 15 | return fmt.Errorf("only one package is allowed: packages=%+v", args.PackageInputOpts.Packages) 16 | } 17 | packages, err := containers.GetPackages(ctx, dc, args.PackageInputOpts, args.GCPOpts) 18 | if err != nil { 19 | return fmt.Errorf("getting packages: packages=%+v %w", args.PackageInputOpts.Packages, err) 20 | } 21 | 22 | debianPackageFile := packages[0] 23 | 24 | log.Printf("Cloning hosted Grafana...") 25 | hostedGrafanaRepo, err := git.CloneWithGitHubToken(dc, args.ProImageOpts.GitHubToken, "https://github.com/grafana/hosted-grafana.git", "main") 26 | if err != nil { 27 | return fmt.Errorf("cloning hosted-grafana repo: %w", err) 28 | } 29 | 30 | socketPath := "/var/run/docker.sock" 31 | socket := dc.Host().UnixSocket(socketPath) 32 | 33 | hostedGrafanaImage := fmt.Sprintf("%s/%s:%s", args.ProImageOpts.ContainerRegistry, args.ProImageOpts.Repo, args.ProImageOpts.ImageTag) 34 | 35 | log.Printf("Building hosted Grafana image: %s", hostedGrafanaImage) 36 | container := dc.Container().From("google/cloud-sdk:433.0.0-alpine"). 37 | WithExec([]string{ 38 | "/bin/sh", "-c", 39 | "gcloud auth configure-docker --quiet", 40 | }). 41 | WithUnixSocket(socketPath, socket). 42 | WithDirectory("/src", hostedGrafanaRepo). 43 | WithFile("/src/grafana.deb", debianPackageFile). 44 | WithWorkdir("/src"). 45 | WithExec([]string{ 46 | "/bin/sh", "-c", 47 | "docker build --platform=linux/amd64 . -f ./cmd/hgrun/Dockerfile -t hgrun:latest", 48 | }). 49 | WithExec([]string{ 50 | "/bin/sh", "-c", 51 | fmt.Sprintf("docker build --platform=linux/amd64 --build-arg=RELEASE_TYPE=%s --build-arg=GRAFANA_VERSION=%s --build-arg=HGRUN_IMAGE=hgrun:latest . -f ./docker/hosted-grafana-all/Dockerfile -t %s", 52 | args.ProImageOpts.ReleaseType, 53 | args.ProImageOpts.GrafanaVersion, 54 | hostedGrafanaImage, 55 | ), 56 | }) 57 | 58 | if args.ProImageOpts.Push { 59 | if args.ProImageOpts.ContainerRegistry == "" { 60 | return fmt.Errorf("--registry= is required") 61 | } 62 | 63 | authenticator := containers.GCSAuth(dc, &containers.GCPOpts{ 64 | ServiceAccountKey: args.GCPOpts.ServiceAccountKey, 65 | ServiceAccountKeyBase64: args.GCPOpts.ServiceAccountKeyBase64, 66 | }) 67 | 68 | authenticatedContainer, err := authenticator.Authenticate(dc, container) 69 | if err != nil { 70 | return fmt.Errorf("authenticating container with gcs auth: %w", err) 71 | } 72 | 73 | log.Printf("Pushing hosted Grafana image to registry...") 74 | container = authenticatedContainer.WithExec([]string{ 75 | "/bin/sh", "-c", 76 | fmt.Sprintf("docker push %s", hostedGrafanaImage), 77 | }) 78 | } 79 | 80 | if _, err := containers.ExitError(ctx, container); err != nil { 81 | return fmt.Errorf("container did not exit successfully: %w", err) 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /flags/packages.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/packages" 5 | "github.com/grafana/grafana-build/pipeline" 6 | ) 7 | 8 | var DefaultTags = []string{ 9 | "osusergo", 10 | "timetzdata", 11 | } 12 | 13 | const ( 14 | PackageName pipeline.FlagOption = "package-name" 15 | Distribution pipeline.FlagOption = "distribution" 16 | Static pipeline.FlagOption = "static" 17 | Enterprise pipeline.FlagOption = "enterprise" 18 | WireTag pipeline.FlagOption = "wire-tag" 19 | GoTags pipeline.FlagOption = "go-tag" 20 | GoExperiments pipeline.FlagOption = "go-experiments" 21 | Sign pipeline.FlagOption = "sign" 22 | 23 | // Pretty much only used to set the deb or RPM internal package name (and file name) to `{}-nightly` and/or `{}-rpi` 24 | Nightly pipeline.FlagOption = "nightly" 25 | RPI pipeline.FlagOption = "rpi" 26 | ) 27 | 28 | // PackageNameFlags - flags that packages (targz, deb, rpm, docker) must have. 29 | // Essentially they must have all of the same things that the targz package has. 30 | var PackageNameFlags = []pipeline.Flag{ 31 | { 32 | Name: "grafana", 33 | Options: map[pipeline.FlagOption]any{ 34 | DockerRepositories: []string{"grafana-image-tags", "grafana-oss-image-tags"}, 35 | PackageName: string(packages.PackageGrafana), 36 | Enterprise: false, 37 | WireTag: "oss", 38 | GoExperiments: []string{}, 39 | GoTags: DefaultTags, 40 | }, 41 | }, 42 | { 43 | Name: "enterprise", 44 | Options: map[pipeline.FlagOption]any{ 45 | DockerRepositories: []string{"grafana-enterprise-image-tags"}, 46 | PackageName: string(packages.PackageEnterprise), 47 | Enterprise: true, 48 | WireTag: "enterprise", 49 | GoExperiments: []string{}, 50 | GoTags: append(DefaultTags, "enterprise"), 51 | }, 52 | }, 53 | { 54 | Name: "pro", 55 | Options: map[pipeline.FlagOption]any{ 56 | DockerRepositories: []string{"grafana-pro"}, 57 | PackageName: string(packages.PackagePro), 58 | Enterprise: true, 59 | WireTag: "pro", 60 | GoExperiments: []string{}, 61 | GoTags: append(DefaultTags, "pro"), 62 | }, 63 | }, 64 | { 65 | Name: "boring", 66 | Options: map[pipeline.FlagOption]any{ 67 | DockerRepositories: []string{"grafana-enterprise-image-tags"}, 68 | PackageName: string(packages.PackageEnterpriseBoring), 69 | Enterprise: true, 70 | WireTag: "enterprise", 71 | GoExperiments: []string{"boringcrypto"}, 72 | GoTags: append(DefaultTags, "enterprise"), 73 | }, 74 | }, 75 | } 76 | 77 | var SignFlag = pipeline.Flag{ 78 | Name: "sign", 79 | Options: map[pipeline.FlagOption]any{ 80 | Sign: true, 81 | }, 82 | } 83 | 84 | var NightlyFlag = pipeline.Flag{ 85 | Name: "nightly", 86 | Options: map[pipeline.FlagOption]any{ 87 | Nightly: true, 88 | }, 89 | } 90 | 91 | func StdPackageFlags() []pipeline.Flag { 92 | distros := DistroFlags() 93 | names := PackageNameFlags 94 | 95 | return JoinFlags( 96 | distros, 97 | names, 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /artifacts/version.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "dagger.io/dagger" 8 | "github.com/grafana/grafana-build/arguments" 9 | "github.com/grafana/grafana-build/pipeline" 10 | ) 11 | 12 | var ( 13 | VersionArguments = []pipeline.Argument{ 14 | arguments.GrafanaDirectory, 15 | arguments.Version, 16 | } 17 | 18 | VersionFlags = TargzFlags 19 | ) 20 | 21 | var VersionInitializer = Initializer{ 22 | InitializerFunc: NewVersionFromString, 23 | Arguments: VersionArguments, 24 | } 25 | 26 | type Version struct { 27 | // Version is embedded in the binary at build-time 28 | Version string 29 | } 30 | 31 | func (b *Version) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) { 32 | return opts.Client.Container().WithNewFile("/VERSION", b.Version), nil 33 | } 34 | 35 | func (b *Version) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) { 36 | return []*pipeline.Artifact{}, nil 37 | } 38 | 39 | func (b *Version) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) { 40 | return builder.File("/VERSION"), nil 41 | } 42 | 43 | func (b *Version) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) { 44 | return nil, nil 45 | } 46 | 47 | func (b *Version) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) { 48 | return nil, nil 49 | } 50 | 51 | func (b *Version) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error { 52 | return nil 53 | } 54 | 55 | func (b *Version) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error { 56 | panic("not implemented") // TODO: Implement 57 | } 58 | 59 | // Filename should return a deterministic file or folder name that this build will produce. 60 | // This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output 61 | // also affect the filename to ensure that there are no collisions. 62 | // For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a 63 | // `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want. 64 | func (b *Version) Filename(ctx context.Context) (string, error) { 65 | return "VERSION", nil 66 | } 67 | 68 | func (b *Version) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error { 69 | return nil 70 | } 71 | 72 | func (b *Version) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error { 73 | return nil 74 | } 75 | 76 | func NewVersionFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) { 77 | version, err := state.String(ctx, arguments.Version) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return NewVersion(ctx, log, artifact, version) 83 | } 84 | 85 | func NewVersion(ctx context.Context, log *slog.Logger, artifact, version string) (*pipeline.Artifact, error) { 86 | return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{ 87 | ArtifactString: artifact, 88 | Type: pipeline.ArtifactTypeFile, 89 | Flags: VersionFlags, 90 | Handler: &Version{ 91 | Version: version, 92 | }, 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /scripts/move_packages_rpm_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var rpmMapping = map[string]m{ 4 | "OSS: Linux AMD64": { 5 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_amd64.rpm", 6 | output: []string{ 7 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3-1.x86_64.rpm", 8 | }, 9 | }, 10 | "OSS: Linux AMD64 SHA256": { 11 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_amd64.rpm.sha256", 12 | output: []string{ 13 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3-1.x86_64.rpm.sha256", 14 | }, 15 | }, 16 | "OSS: Linux ARM7": { 17 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_arm-7.rpm", 18 | output: []string{ 19 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3-1.armhfp.rpm", 20 | }, 21 | }, 22 | "OSS: Linux ARM7 SHA256": { 23 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_arm-7.rpm.sha256", 24 | output: []string{ 25 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3-1.armhfp.rpm.sha256", 26 | }, 27 | }, 28 | "OSS: Linux aarch64": { 29 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_aarch64.rpm", 30 | output: []string{ 31 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3-1.aarch64.rpm", 32 | }, 33 | }, 34 | "OSS: Linux aarch64 SHA256": { 35 | input: "gs://bucket/tag/grafana_v1.2.3_102_linux_arm64.rpm.sha256", 36 | output: []string{ 37 | "artifacts/downloads/v1.2.3/oss/release/grafana-1.2.3-1.aarch64.rpm.sha256", 38 | }, 39 | }, 40 | "ENT: Linux AMD64": { 41 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_amd64.rpm", 42 | output: []string{ 43 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3-1.x86_64.rpm", 44 | }, 45 | }, 46 | "ENT: Linux AMD64 SHA256": { 47 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_amd64.rpm.sha256", 48 | output: []string{ 49 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3-1.x86_64.rpm.sha256", 50 | }, 51 | }, 52 | "ENT: Linux ARM64": { 53 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_arm64.rpm", 54 | output: []string{ 55 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3-1.aarch64.rpm", 56 | }, 57 | }, 58 | "ENT: Linux ARM64 SHA256": { 59 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_arm64.rpm.sha256", 60 | output: []string{ 61 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3-1.aarch64.rpm.sha256", 62 | }, 63 | }, 64 | "ENT: Linux ARM7": { 65 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_arm-7.rpm", 66 | output: []string{ 67 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3-1.armhfp.rpm", 68 | }, 69 | }, 70 | "ENT: Linux ARM7 SHA256": { 71 | input: "gs://bucket/tag/grafana-enterprise_v1.2.3_102_linux_arm-7.rpm.sha256", 72 | output: []string{ 73 | "artifacts/downloads/v1.2.3/enterprise/release/grafana-enterprise-1.2.3-1.armhfp.rpm.sha256", 74 | }, 75 | }, 76 | "PRO: Linux AMD64": { 77 | input: "gs://bucket/tag/grafana-pro_v1.2.3-pre.4_102_linux_amd64.rpm", 78 | output: []string{ 79 | "artifacts/downloads-enterprise2/v1.2.3-pre.4/enterprise2/release/grafana-enterprise2-1.2.3~pre.4-1.x86_64.rpm", 80 | }, 81 | }, 82 | "PRO: Linux AMD64 SHA256": { 83 | input: "gs://bucket/tag/grafana-pro_v1.2.3-pre.4_102_linux_amd64.rpm.sha256", 84 | output: []string{ 85 | "artifacts/downloads-enterprise2/v1.2.3-pre.4/enterprise2/release/grafana-enterprise2-1.2.3~pre.4-1.x86_64.rpm.sha256", 86 | }, 87 | }, 88 | } 89 | -------------------------------------------------------------------------------- /pipeline/artifact_store.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "path/filepath" 9 | "sync" 10 | 11 | "dagger.io/dagger" 12 | "github.com/grafana/grafana-build/containers" 13 | ) 14 | 15 | // The Storer stores the result of artifacts. 16 | type ArtifactStore interface { 17 | StoreFile(ctx context.Context, a *Artifact, file *dagger.File) error 18 | File(ctx context.Context, a *Artifact) (*dagger.File, error) 19 | 20 | StoreDirectory(ctx context.Context, a *Artifact, dir *dagger.Directory) error 21 | Directory(ctx context.Context, a *Artifact) (*dagger.Directory, error) 22 | 23 | Export(ctx context.Context, d *dagger.Client, a *Artifact, destination string, checksum bool) ([]string, error) 24 | Exists(ctx context.Context, a *Artifact) (bool, error) 25 | } 26 | 27 | type MapArtifactStore struct { 28 | data *sync.Map 29 | } 30 | 31 | func (m *MapArtifactStore) StoreFile(ctx context.Context, a *Artifact, file *dagger.File) error { 32 | f, err := a.Handler.Filename(ctx) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | m.data.Store(f, file) 38 | return nil 39 | } 40 | 41 | func (m *MapArtifactStore) File(ctx context.Context, a *Artifact) (*dagger.File, error) { 42 | f, err := a.Handler.Filename(ctx) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | v, ok := m.data.Load(f) 48 | if !ok { 49 | return nil, errors.New("not found") 50 | } 51 | 52 | return v.(*dagger.File), nil 53 | } 54 | 55 | func (m *MapArtifactStore) StoreDirectory(ctx context.Context, a *Artifact, dir *dagger.Directory) error { 56 | f, err := a.Handler.Filename(ctx) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | m.data.Store(f, dir) 62 | return nil 63 | } 64 | 65 | func (m *MapArtifactStore) Directory(ctx context.Context, a *Artifact) (*dagger.Directory, error) { 66 | f, err := a.Handler.Filename(ctx) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | v, ok := m.data.Load(f) 72 | if !ok { 73 | return nil, errors.New("not found") 74 | } 75 | 76 | return v.(*dagger.Directory), nil 77 | } 78 | 79 | func (m *MapArtifactStore) Export(ctx context.Context, d *dagger.Client, a *Artifact, dst string, checksum bool) ([]string, error) { 80 | path, err := a.Handler.Filename(ctx) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | path = filepath.Join(dst, path) 86 | switch a.Type { 87 | case ArtifactTypeFile: 88 | f, err := m.File(ctx, a) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | if _, err := f.Export(ctx, path); err != nil { 94 | return nil, err 95 | } 96 | 97 | if !checksum { 98 | return []string{path}, nil 99 | } 100 | if _, err := containers.Sha256(d, f).Export(ctx, path+".sha256"); err != nil { 101 | return nil, err 102 | } 103 | 104 | return []string{path, path + ".sha256"}, nil 105 | case ArtifactTypeDirectory: 106 | f, err := m.Directory(ctx, a) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | if _, err := f.Export(ctx, path); err != nil { 112 | return nil, err 113 | } 114 | 115 | return []string{path}, nil 116 | } 117 | 118 | return nil, fmt.Errorf("unrecognized artifact type: %d", a.Type) 119 | } 120 | 121 | func (m *MapArtifactStore) Exists(ctx context.Context, a *Artifact) (bool, error) { 122 | path, err := a.Handler.Filename(ctx) 123 | if err != nil { 124 | return false, err 125 | } 126 | 127 | _, ok := m.data.Load(path) 128 | return ok, nil 129 | } 130 | 131 | func NewArtifactStore(log *slog.Logger) ArtifactStore { 132 | return StoreWithLogging(&MapArtifactStore{ 133 | data: &sync.Map{}, 134 | }, log) 135 | } 136 | -------------------------------------------------------------------------------- /versions/opts.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "github.com/Masterminds/semver" 5 | ) 6 | 7 | type Nullable[T any] struct { 8 | Value T 9 | IsSet bool 10 | } 11 | 12 | func NewNullable[T any](val T) Nullable[T] { 13 | return Nullable[T]{ 14 | Value: val, 15 | IsSet: true, 16 | } 17 | } 18 | 19 | // Options holds the general options for each version that may be different. 20 | type Options struct { 21 | Constraint Nullable[string] 22 | // CombinedExecutable was introduced in Grafana 9.4; it combined the `grafana-server` and `grafana-cli` commands into one `grafana` executable. 23 | CombinedExecutable Nullable[bool] 24 | // DebPreRM defines the 'prerm' script in the debian installer, introduced by this PR: https://github.com/grafana/grafana/pull/59580 in v9.5.0. Versions before v9.5.0 do not have the 'prerm' script in the grafana package. 25 | DebPreRM Nullable[bool] 26 | 27 | // Automcplete (in packaging/autocomplete) was added in Grafana 9.4.0, so we should not try to include this folder in the package before then. 28 | Autocomplete Nullable[bool] 29 | } 30 | 31 | func MergeNullables[T any](values ...Nullable[T]) Nullable[T] { 32 | val := values[0] 33 | for _, v := range values { 34 | if v.IsSet { 35 | val = v 36 | } 37 | } 38 | 39 | return val 40 | } 41 | 42 | func Merge(from, to Options) Options { 43 | return Options{ 44 | Constraint: from.Constraint, 45 | CombinedExecutable: MergeNullables(from.CombinedExecutable, to.CombinedExecutable), 46 | DebPreRM: MergeNullables(from.DebPreRM, to.DebPreRM), 47 | Autocomplete: MergeNullables(from.Autocomplete, to.Autocomplete), 48 | } 49 | } 50 | 51 | // LatestOptions are the options that apply to the latest version of Grafana 52 | var LatestOptions = Options{ 53 | Autocomplete: NewNullable(true), 54 | CombinedExecutable: NewNullable(true), 55 | DebPreRM: NewNullable(true), 56 | } 57 | 58 | // OptionsList is a list of semver filters and corresponding options. 59 | // If multiple constraints match the given semver, then they are merged in the order they appear, where later entries override earlier ones. 60 | // These options should only exist if they are contrary to the LatestOptions, as the applicable options will be merged with it. In the event of any conflicts, the options in this list will override those in the LatestOptions. 61 | var OptionsList = []Options{ 62 | { 63 | Constraint: NewNullable("< 9.5.0-0"), 64 | DebPreRM: NewNullable(false), 65 | }, 66 | { 67 | Constraint: NewNullable("< 9.3.7-0"), 68 | CombinedExecutable: NewNullable(false), 69 | }, 70 | { 71 | Constraint: NewNullable("< 9.4.0-0"), // The -0 includes prereleases. Without it, prereleases are ignored from comparison. I don't really know why??? but it is what it is. 72 | Autocomplete: NewNullable(false), 73 | }, 74 | { 75 | Constraint: NewNullable(">= 9.2.11-0, < 9.3.0-0"), // The combined executable change was backported to 9.2.x at v9.2.11 76 | CombinedExecutable: NewNullable(true), 77 | }, 78 | } 79 | 80 | // OptionsFor returns the options found for a given version. If no versions that matched were found, then the result of "LatestOptions" is returned. 81 | func OptionsFor(version string) Options { 82 | opts := LatestOptions 83 | smversion, err := semver.NewVersion(version) 84 | if err != nil { 85 | return opts 86 | } 87 | 88 | for _, v := range OptionsList { 89 | c, err := semver.NewConstraint(v.Constraint.Value) 90 | if err != nil { 91 | continue 92 | } 93 | if !c.Check(smversion) { 94 | continue 95 | } 96 | 97 | // This version matches the semver, override all options set in 'opts' with those set in 'v' 98 | opts = Merge(opts, v) 99 | } 100 | 101 | return opts 102 | } 103 | -------------------------------------------------------------------------------- /artifacts/plugins_bundled.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "path" 7 | 8 | "dagger.io/dagger" 9 | "github.com/grafana/grafana-build/arguments" 10 | "github.com/grafana/grafana-build/flags" 11 | "github.com/grafana/grafana-build/frontend" 12 | "github.com/grafana/grafana-build/packages" 13 | "github.com/grafana/grafana-build/pipeline" 14 | ) 15 | 16 | var ( 17 | BundledPluginsFlags = flags.PackageNameFlags 18 | BundledPluginsArguments = []pipeline.Argument{ 19 | arguments.YarnCacheDirectory, 20 | } 21 | ) 22 | 23 | type BundledPlugins struct { 24 | Name packages.Name 25 | Src *dagger.Directory 26 | YarnCache *dagger.CacheVolume 27 | Version string 28 | } 29 | 30 | // The frontend does not have any artifact dependencies. 31 | func (f *BundledPlugins) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) { 32 | return nil, nil 33 | } 34 | 35 | // Builder will return a node.js alpine container that matches the .nvmrc in the Grafana source repository 36 | func (f *BundledPlugins) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) { 37 | return FrontendBuilder(ctx, f.Src, f.YarnCache, opts) 38 | } 39 | 40 | func (f *BundledPlugins) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) { 41 | panic("not implemented") // BundledPlugins doesn't return a file 42 | } 43 | 44 | func (f *BundledPlugins) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) { 45 | return frontend.BuildPlugins(builder), nil 46 | } 47 | 48 | func (f *BundledPlugins) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) { 49 | return nil, nil 50 | } 51 | 52 | func (f *BundledPlugins) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error { 53 | panic("not implemented") // TODO: Implement 54 | } 55 | 56 | func (f *BundledPlugins) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error { 57 | return nil 58 | } 59 | 60 | func (f *BundledPlugins) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error { 61 | return nil 62 | } 63 | 64 | func (f *BundledPlugins) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error { 65 | panic("not implemented") // TODO: Implement 66 | } 67 | 68 | // Filename should return a deterministic file or folder name that this build will produce. 69 | // This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output 70 | // also affect the filename to ensure that there are no collisions. 71 | // For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a 72 | // `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want. 73 | func (f *BundledPlugins) Filename(ctx context.Context) (string, error) { 74 | // Important note: this path is only used in two ways: 75 | // 1. When requesting an artifact be built and exported, this is the path where it will be exported to 76 | // 2. In a map to distinguish when the same artifact is being built more than once 77 | return path.Join("bin", "bundled-plugins"), nil 78 | } 79 | 80 | func NewBundledPlugins(ctx context.Context, log *slog.Logger, artifact string, src *dagger.Directory, version string, cacheVolume *dagger.CacheVolume) (*pipeline.Artifact, error) { 81 | return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{ 82 | ArtifactString: artifact, 83 | Type: pipeline.ArtifactTypeDirectory, 84 | Flags: BundledPluginsFlags, 85 | Handler: &BundledPlugins{ 86 | Src: src, 87 | YarnCache: cacheVolume, 88 | Version: version, 89 | }, 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /pipelines/docker_publish_test.go: -------------------------------------------------------------------------------- 1 | package pipelines_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/grafana-build/pipelines" 7 | ) 8 | 9 | func TestImageManifest(t *testing.T) { 10 | manifests := map[string]string{ 11 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-amd64": "docker.io/grafana/grafana:1.2.3-test.1.2.3", 12 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-amd64": "docker.io/grafana/grafana-oss:1.2.3-test.1.2.3", 13 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-arm64": "docker.io/grafana/grafana:1.2.3-test.1.2.3", 14 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-arm64": "docker.io/grafana/grafana-oss:1.2.3-test.1.2.3", 15 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-ubuntu-amd64": "docker.io/grafana/grafana:1.2.3-test.1.2.3-ubuntu", 16 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-ubuntu-amd64": "docker.io/grafana/grafana-oss:1.2.3-test.1.2.3-ubuntu", 17 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-ubuntu-arm64": "docker.io/grafana/grafana:1.2.3-test.1.2.3-ubuntu", 18 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-ubuntu-arm64": "docker.io/grafana/grafana-oss:1.2.3-test.1.2.3-ubuntu", 19 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-amd64": "docker.io/grafana/grafana-enterprise:1.2.3-test.1.2.3", 20 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-arm64": "docker.io/grafana/grafana-enterprise:1.2.3-test.1.2.3", 21 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-ubuntu-amd64": "docker.io/grafana/grafana-enterprise:1.2.3-test.1.2.3-ubuntu", 22 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-ubuntu-arm64": "docker.io/grafana/grafana-enterprise:1.2.3-test.1.2.3-ubuntu", 23 | } 24 | 25 | for k, v := range manifests { 26 | if n := pipelines.ImageManifest(k); n != v { 27 | t.Errorf("Expected '%s' manifest to equal '%s' but got '%s'", k, v, n) 28 | } 29 | } 30 | } 31 | 32 | func TestLatestManifest(t *testing.T) { 33 | manifests := map[string]string{ 34 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-amd64": "docker.io/grafana/grafana:latest", 35 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-amd64": "docker.io/grafana/grafana-oss:latest", 36 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-arm64": "docker.io/grafana/grafana:latest", 37 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-arm64": "docker.io/grafana/grafana-oss:latest", 38 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-ubuntu-amd64": "docker.io/grafana/grafana:latest-ubuntu", 39 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-ubuntu-amd64": "docker.io/grafana/grafana-oss:latest-ubuntu", 40 | "docker.io/grafana/grafana-image-tags:1.2.3-test.1.2.3-ubuntu-arm64": "docker.io/grafana/grafana:latest-ubuntu", 41 | "docker.io/grafana/grafana-oss-image-tags:1.2.3-test.1.2.3-ubuntu-arm64": "docker.io/grafana/grafana-oss:latest-ubuntu", 42 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-amd64": "docker.io/grafana/grafana-enterprise:latest", 43 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-arm64": "docker.io/grafana/grafana-enterprise:latest", 44 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-ubuntu-amd64": "docker.io/grafana/grafana-enterprise:latest-ubuntu", 45 | "docker.io/grafana/grafana-enterprise-image-tags:1.2.3-test.1.2.3-ubuntu-arm64": "docker.io/grafana/grafana-enterprise:latest-ubuntu", 46 | } 47 | 48 | for k, v := range manifests { 49 | if n := pipelines.LatestManifest(k); n != v { 50 | t.Errorf("Expected '%s' manifest to equal '%s' but got '%s'", k, v, n) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.golangci.toml: -------------------------------------------------------------------------------- 1 | [run] 2 | timeout = "10m" 3 | 4 | [linters-settings.goconst] 5 | min-len = 5 6 | min-occurrences = 5 7 | 8 | checks = [ 9 | "all", 10 | ] 11 | 12 | [linters-settings.exhaustive] 13 | default-signifies-exhaustive = true 14 | 15 | [linters-settings.revive] 16 | confidence = 3 17 | 18 | [linters-settings.errcheck] 19 | exclude-functions = [ 20 | "(*os.File).Close", 21 | "(*compress/gzip.Writer).Close", 22 | "(*compress/gzip.Reader).Close", 23 | "(*archive/tar.Writer).Close", 24 | "(*dagger.io/dagger.Client).Close", 25 | ] 26 | 27 | [linters] 28 | disable-all = true 29 | enable = [ 30 | "bodyclose", 31 | "dogsled", 32 | "errcheck", 33 | "gochecknoinits", 34 | "goconst", 35 | "gocritic", 36 | "goimports", 37 | "goprintffuncname", 38 | "gosec", 39 | "gosimple", 40 | "govet", 41 | "ineffassign", 42 | "misspell", 43 | "nakedret", 44 | "staticcheck", 45 | "stylecheck", 46 | "typecheck", 47 | "unconvert", 48 | "unused", 49 | "whitespace", 50 | "gocyclo", 51 | "exhaustive", 52 | "typecheck", 53 | "asciicheck", 54 | "errorlint", 55 | "revive", 56 | ] 57 | 58 | # Disabled linters (might want them later) 59 | # "unparam" 60 | 61 | [issues] 62 | exclude-use-default = false 63 | 64 | # Enable when appropriate 65 | # Poorly chosen identifier 66 | [[issues.exclude-rules]] 67 | linters = ["stylecheck"] 68 | text = "ST1003" 69 | 70 | # Enable when appropriate 71 | # Dot imports that aren't in external test packages are discouraged. 72 | [[issues.exclude-rules]] 73 | linters = ["stylecheck"] 74 | text = "ST1001" 75 | 76 | # Enable when appropriate 77 | # strings.Title has been deprecated since Go 1.18 and an alternative has been available since Go 1.0: The rule Title uses for word boundaries does not handle Unicode punctuation properly. 78 | # Use golang.org/x/text/cases instead. 79 | [[issues.exclude-rules]] 80 | linters = ["staticcheck"] 81 | text = "SA1019: strings.Title" 82 | 83 | # ExitCode is deprecated with 0.7.x 84 | [[issues.exclude-rules]] 85 | linters = ["staticcheck"] 86 | text = "SA1019: container" 87 | [[issues.exclude-rules]] 88 | linters = ["staticcheck"] 89 | path = "ci/" 90 | text = "SA1019" 91 | 92 | [[issues.exclude-rules]] 93 | linters = ["staticcheck"] 94 | text = "use fake service and real access control evaluator instead" 95 | 96 | [[issues.exclude-rules]] 97 | linters = ["gosec"] 98 | text = "G108" 99 | 100 | [[issues.exclude-rules]] 101 | linters = ["gosec"] 102 | text = "G110" 103 | 104 | [[issues.exclude-rules]] 105 | linters = ["gosec"] 106 | text = "G201" 107 | 108 | [[issues.exclude-rules]] 109 | linters = ["gosec"] 110 | text = "G202" 111 | 112 | [[issues.exclude-rules]] 113 | linters = ["gosec"] 114 | text = "G306" 115 | 116 | [[issues.exclude-rules]] 117 | linters = ["gosec"] 118 | text = "401" 119 | 120 | [[issues.exclude-rules]] 121 | linters = ["gosec"] 122 | text = "402" 123 | 124 | [[issues.exclude-rules]] 125 | linters = ["gosec"] 126 | text = "501" 127 | 128 | [[issues.exclude-rules]] 129 | linters = ["gosec"] 130 | text = "404" 131 | 132 | [[issues.exclude-rules]] 133 | linters = ["gosec"] 134 | text = "G304" # There's just no way ensuring that files are not created or read using variables will be possible in this project. 135 | 136 | [[issues.exclude-rules]] 137 | linters = ["gosec"] 138 | text = "G307" 139 | 140 | [[issues.exclude-rules]] 141 | linters = ["misspell"] 142 | text = "Unknwon` is a misspelling of `Unknown" 143 | 144 | [[issues.exclude-rules]] 145 | linters = ["errorlint"] 146 | text = "non-wrapping format verb for fmt.Errorf" 147 | 148 | # TODO: Enable 149 | [[issues.exclude-rules]] 150 | linters = ["stylecheck"] 151 | text = "ST1000" 152 | 153 | # TODO: Enable 154 | [[issues.exclude-rules]] 155 | linters = ["stylecheck"] 156 | text = "ST1020" 157 | 158 | # TODO: Enable 159 | [[issues.exclude-rules]] 160 | linters = ["stylecheck"] 161 | text = "ST1021" 162 | -------------------------------------------------------------------------------- /artifacts/package_zip.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "dagger.io/dagger" 8 | "github.com/grafana/grafana-build/backend" 9 | "github.com/grafana/grafana-build/packages" 10 | "github.com/grafana/grafana-build/pipeline" 11 | "github.com/grafana/grafana-build/zip" 12 | ) 13 | 14 | var ( 15 | ZipArguments = TargzArguments 16 | ZipFlags = TargzFlags 17 | ) 18 | 19 | var ZipInitializer = Initializer{ 20 | InitializerFunc: NewZipFromString, 21 | Arguments: TargzArguments, 22 | } 23 | 24 | // PacakgeZip uses a built tar.gz package to create a .zip package for zipian based Linux distributions. 25 | type Zip struct { 26 | Name packages.Name 27 | Version string 28 | BuildID string 29 | Distribution backend.Distribution 30 | Enterprise bool 31 | 32 | Tarball *pipeline.Artifact 33 | } 34 | 35 | func (d *Zip) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) { 36 | return []*pipeline.Artifact{ 37 | d.Tarball, 38 | }, nil 39 | } 40 | 41 | func (d *Zip) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) { 42 | return zip.Builder(opts.Client), nil 43 | } 44 | 45 | func (d *Zip) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) { 46 | targz, err := opts.Store.File(ctx, d.Tarball) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return zip.Build(builder, targz), nil 52 | } 53 | 54 | func (d *Zip) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) { 55 | panic("not implemented") // TODO: Implement 56 | } 57 | 58 | func (d *Zip) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) { 59 | return nil, nil 60 | } 61 | 62 | func (d *Zip) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error { 63 | return nil 64 | } 65 | 66 | func (d *Zip) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error { 67 | panic("not implemented") // TODO: Implement 68 | } 69 | 70 | func (d *Zip) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error { 71 | return nil 72 | } 73 | 74 | func (d *Zip) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error { 75 | panic("not implemented") // TODO: Implement 76 | } 77 | 78 | // Filename should return a deterministic file or folder name that this build will produce. 79 | // This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output 80 | // also affect the filename to ensure that there are no collisions. 81 | // For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a 82 | // `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want. 83 | func (d *Zip) Filename(ctx context.Context) (string, error) { 84 | return packages.FileName(d.Name, d.Version, d.BuildID, d.Distribution, "zip") 85 | } 86 | 87 | func NewZipFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) { 88 | tarball, err := NewTarballFromString(ctx, log, artifact, state) 89 | if err != nil { 90 | return nil, err 91 | } 92 | options, err := pipeline.ParseFlags(artifact, ZipFlags) 93 | if err != nil { 94 | return nil, err 95 | } 96 | p, err := GetPackageDetails(ctx, options, state) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{ 101 | ArtifactString: artifact, 102 | Handler: &Zip{ 103 | Name: p.Name, 104 | Version: p.Version, 105 | BuildID: p.BuildID, 106 | Distribution: p.Distribution, 107 | Enterprise: p.Enterprise, 108 | Tarball: tarball, 109 | }, 110 | Type: pipeline.ArtifactTypeFile, 111 | Flags: TargzFlags, 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /pipeline/artifact_store_logger.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "dagger.io/dagger" 8 | ) 9 | 10 | type ArtifactStoreLogger struct { 11 | Store ArtifactStore 12 | Log *slog.Logger 13 | } 14 | 15 | func (m *ArtifactStoreLogger) StoreFile(ctx context.Context, a *Artifact, file *dagger.File) error { 16 | fn, err := a.Handler.Filename(ctx) 17 | if err != nil { 18 | return err 19 | } 20 | log := m.Log.With("artifact", a.ArtifactString, "path", fn) 21 | 22 | log.DebugContext(ctx, "storing artifact file...") 23 | if err := m.Store.StoreFile(ctx, a, file); err != nil { 24 | log.DebugContext(ctx, "error storing artifact file", "error", err) 25 | return err 26 | } 27 | log.DebugContext(ctx, "done storing artifact file") 28 | return nil 29 | } 30 | 31 | func (m *ArtifactStoreLogger) File(ctx context.Context, a *Artifact) (*dagger.File, error) { 32 | fn, err := a.Handler.Filename(ctx) 33 | if err != nil { 34 | return nil, err 35 | } 36 | log := m.Log.With("artifact", a.ArtifactString, "path", fn) 37 | 38 | log.DebugContext(ctx, "fetching artifact file...") 39 | file, err := m.Store.File(ctx, a) 40 | if err != nil { 41 | log.DebugContext(ctx, "error fetching artifact file", "error", err) 42 | return nil, err 43 | } 44 | 45 | log.DebugContext(ctx, "done fetching artifact file") 46 | return file, nil 47 | } 48 | 49 | func (m *ArtifactStoreLogger) StoreDirectory(ctx context.Context, a *Artifact, dir *dagger.Directory) error { 50 | fn, err := a.Handler.Filename(ctx) 51 | if err != nil { 52 | return err 53 | } 54 | log := m.Log.With("artifact", a.ArtifactString, "path", fn) 55 | 56 | log.DebugContext(ctx, "storing artifact directory...") 57 | if err := m.Store.StoreDirectory(ctx, a, dir); err != nil { 58 | log.DebugContext(ctx, "error storing artifact directory", "error", err) 59 | return err 60 | } 61 | log.DebugContext(ctx, "done storing artifact directory") 62 | return nil 63 | } 64 | 65 | func (m *ArtifactStoreLogger) Directory(ctx context.Context, a *Artifact) (*dagger.Directory, error) { 66 | fn, err := a.Handler.Filename(ctx) 67 | if err != nil { 68 | return nil, err 69 | } 70 | log := m.Log.With("artifact", a.ArtifactString, "path", fn) 71 | 72 | log.DebugContext(ctx, "fetching artifact directory...") 73 | dir, err := m.Store.Directory(ctx, a) 74 | if err != nil { 75 | log.DebugContext(ctx, "error fetching artifact directory", "error", err) 76 | return nil, err 77 | } 78 | 79 | log.DebugContext(ctx, "done fetching artifact directory") 80 | return dir, nil 81 | } 82 | 83 | func (m *ArtifactStoreLogger) Export(ctx context.Context, d *dagger.Client, a *Artifact, dst string, checksum bool) ([]string, error) { 84 | fn, err := a.Handler.Filename(ctx) 85 | if err != nil { 86 | return nil, err 87 | } 88 | log := m.Log.With("artifact", a.ArtifactString, "path", fn, "destination", dst, "checksum", checksum) 89 | 90 | log.DebugContext(ctx, "exporting artifact...") 91 | path, err := m.Store.Export(ctx, d, a, dst, checksum) 92 | if err != nil { 93 | log.DebugContext(ctx, "error exporting artifact", "error", err) 94 | return nil, err 95 | } 96 | 97 | log.DebugContext(ctx, "done exporting artifact") 98 | return path, nil 99 | } 100 | 101 | func (m *ArtifactStoreLogger) Exists(ctx context.Context, a *Artifact) (bool, error) { 102 | fn, err := a.Handler.Filename(ctx) 103 | if err != nil { 104 | return false, err 105 | } 106 | log := m.Log.With("artifact", a.ArtifactString, "path", fn) 107 | 108 | log.DebugContext(ctx, "checking existence of artifact...") 109 | v, err := m.Store.Exists(ctx, a) 110 | if err != nil { 111 | log.DebugContext(ctx, "error checking existence of artifact", "error", err) 112 | return false, err 113 | } 114 | 115 | log.DebugContext(ctx, "done checking existence of artifact") 116 | return v, nil 117 | } 118 | 119 | func StoreWithLogging(s ArtifactStore, log *slog.Logger) *ArtifactStoreLogger { 120 | return &ArtifactStoreLogger{ 121 | Store: s, 122 | Log: log.With("service", "ArtifactStore"), 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /arguments/docker.go: -------------------------------------------------------------------------------- 1 | package arguments 2 | 3 | import ( 4 | "github.com/grafana/grafana-build/docker" 5 | "github.com/grafana/grafana-build/pipeline" 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | var ( 10 | DockerRegistryFlag = &cli.StringFlag{ 11 | Name: "registry", 12 | Usage: "Prefix the image name with the registry provided", 13 | Value: "docker.io", 14 | } 15 | DockerOrgFlag = &cli.StringFlag{ 16 | Name: "org", 17 | Usage: "Overrides the organization of the images", 18 | Value: "grafana", 19 | } 20 | AlpineImageFlag = &cli.StringFlag{ 21 | Name: "alpine-base", 22 | Usage: "The alpine image to use as the base image when building the Alpine version of the Grafana docker image", 23 | Value: "alpine:latest", 24 | } 25 | UbuntuImageFlag = &cli.StringFlag{ 26 | Name: "ubuntu-base", 27 | Usage: "The Ubuntu image to use as the base image when building the Ubuntu version of the Grafana docker image", 28 | Value: "ubuntu:latest", 29 | } 30 | TagFormatFlag = &cli.StringFlag{ 31 | Name: "tag-format", 32 | Usage: "Provide a go template for formatting the docker tag(s) for images with an Alpine base", 33 | Value: docker.DefaultTagFormat, 34 | } 35 | UbuntuTagFormatFlag = &cli.StringFlag{ 36 | Name: "ubuntu-tag-format", 37 | Usage: "Provide a go template for formatting the docker tag(s) for images with a ubuntu base", 38 | Value: docker.DefaultUbuntuTagFormat, 39 | } 40 | BoringTagFormatFlag = &cli.StringFlag{ 41 | Name: "boring-tag-format", 42 | Usage: "Provide a go template for formatting the docker tag(s) for the boringcrypto build of Grafana Enterprise", 43 | Value: docker.DefaultBoringTagFormat, 44 | } 45 | 46 | ProDockerRegistryFlag = &cli.StringFlag{ 47 | Name: "pro-registry", 48 | Usage: "Prefix the image name with the registry provided", 49 | Value: "docker.io", 50 | } 51 | ProDockerOrgFlag = &cli.StringFlag{ 52 | Name: "pro-org", 53 | Usage: "Overrides the organization of the images", 54 | Value: "grafana", 55 | } 56 | ProDockerRepoFlag = &cli.StringFlag{ 57 | Name: "pro-repo", 58 | Usage: "Overrides the docker repository of the built images", 59 | Value: "grafana-pro", 60 | } 61 | 62 | EntDockerRegistryFlag = &cli.StringFlag{ 63 | Name: "docker-enterprise-registry", 64 | Usage: "Prefix the image name with the registry provided", 65 | Value: "docker.io", 66 | } 67 | EntDockerOrgFlag = &cli.StringFlag{ 68 | Name: "docker-enterprise-org", 69 | Usage: "Overrides the organization of the images", 70 | Value: "grafana", 71 | } 72 | EntDockerRepoFlag = &cli.StringFlag{ 73 | Name: "docker-enterprise-repo", 74 | Usage: "Overrides the docker repository of the built images", 75 | Value: "grafana-enterprise", 76 | } 77 | 78 | HGTagFormatFlag = &cli.StringFlag{ 79 | Name: "hg-tag-format", 80 | Usage: "Provide a go template for formatting the docker tag(s) for Hosted Grafana images", 81 | Value: docker.DefaultHGTagFormat, 82 | } 83 | 84 | DockerRegistry = pipeline.NewStringFlagArgument(DockerRegistryFlag) 85 | DockerOrg = pipeline.NewStringFlagArgument(DockerOrgFlag) 86 | AlpineImage = pipeline.NewStringFlagArgument(AlpineImageFlag) 87 | UbuntuImage = pipeline.NewStringFlagArgument(UbuntuImageFlag) 88 | TagFormat = pipeline.NewStringFlagArgument(TagFormatFlag) 89 | UbuntuTagFormat = pipeline.NewStringFlagArgument(UbuntuTagFormatFlag) 90 | BoringTagFormat = pipeline.NewStringFlagArgument(BoringTagFormatFlag) 91 | 92 | // The docker registry for Grafana Pro is often different than the one for Grafana & Enterprise 93 | ProDockerRegistry = pipeline.NewStringFlagArgument(ProDockerRegistryFlag) 94 | ProDockerOrg = pipeline.NewStringFlagArgument(ProDockerOrgFlag) 95 | ProDockerRepo = pipeline.NewStringFlagArgument(ProDockerRepoFlag) 96 | 97 | EntDockerRegistry = pipeline.NewStringFlagArgument(EntDockerRegistryFlag) 98 | EntDockerOrg = pipeline.NewStringFlagArgument(EntDockerOrgFlag) 99 | EntDockerRepo = pipeline.NewStringFlagArgument(EntDockerRepoFlag) 100 | 101 | HGTagFormat = pipeline.NewStringFlagArgument(HGTagFormatFlag) 102 | ) 103 | -------------------------------------------------------------------------------- /backend/env.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/grafana/grafana-build/containers" 7 | ) 8 | 9 | type ( 10 | BuildMode string 11 | GoARM string 12 | GoAMD64 string 13 | Go386 string 14 | LibC int 15 | ) 16 | 17 | const ( 18 | BuildModeDefault BuildMode = "default" 19 | BuildModeExe BuildMode = "exe" 20 | ) 21 | 22 | const ( 23 | GOARM5 GoARM = "5" 24 | GOARM6 GoARM = "6" 25 | GOARM7 GoARM = "7" 26 | ) 27 | 28 | const ( 29 | Go386SSE2 Go386 = "sse2" 30 | Go386SoftFloat Go386 = "softfloat" 31 | ) 32 | 33 | const ( 34 | Musl LibC = iota 35 | GLibC 36 | ) 37 | 38 | type GoBuildOpts struct { 39 | // OS is value supplied to the GOOS environment variable 40 | OS string 41 | 42 | // Arch is value supplied to the GOARCH environment variable 43 | Arch string 44 | 45 | // ExperimentalFlags are Go build-time feature flags in the "GOEXPERIMENT" environment variable that enable experimental features. 46 | ExperimentalFlags []string 47 | 48 | // CGOEnabled defines whether or not the CGO_ENABLED flag is set. 49 | CGOEnabled bool 50 | 51 | // GOARM: For GOARCH=arm, the ARM architecture for which to compile. 52 | // Valid values are 5, 6, 7. 53 | GoARM GoARM 54 | 55 | // GO386: For GOARCH=386, how to implement floating point instructions. 56 | // Valid values are sse2 (default), softfloat. 57 | Go386 Go386 58 | 59 | // CC is the command to use to compile C code when CGO is enabled. (Sets the "CC" environment variable) 60 | CC string 61 | 62 | // CXX is the command to use to compile C++ code when CGO is enabled. (Sets the "CXX" environment variable) 63 | CXX string 64 | } 65 | 66 | // GoBuildEnv returns the environment variables that must be set for a 'go build' command given the provided 'GoBuildOpts'. 67 | func GoBuildEnv(opts *GoBuildOpts) []containers.Env { 68 | var ( 69 | os = opts.OS 70 | arch = opts.Arch 71 | ) 72 | 73 | env := []containers.Env{containers.EnvVar("GOOS", os), containers.EnvVar("GOARCH", arch)} 74 | 75 | if arch == "arm" { 76 | env = append(env, containers.EnvVar("GOARM", string(opts.GoARM))) 77 | } 78 | 79 | if opts.CGOEnabled { 80 | env = append(env, containers.EnvVar("GOARM", string(opts.GoARM))) 81 | env = append(env, containers.EnvVar("CGO_ENABLED", "1")) 82 | 83 | // https://github.com/mattn/go-sqlite3/issues/1164#issuecomment-1635253695 84 | env = append(env, containers.EnvVar("CGO_CFLAGS", "-D_LARGEFILE64_SOURCE")) 85 | } else { 86 | env = append(env, containers.EnvVar("CGO_ENABLED", "0")) 87 | } 88 | 89 | if opts.ExperimentalFlags != nil { 90 | env = append(env, containers.EnvVar("GOEXPERIMENT", strings.Join(opts.ExperimentalFlags, ","))) 91 | } 92 | 93 | if opts.CC != "" { 94 | env = append(env, containers.EnvVar("CC", opts.CC)) 95 | } 96 | 97 | if opts.CXX != "" { 98 | env = append(env, containers.EnvVar("CXX", opts.CXX)) 99 | } 100 | 101 | return env 102 | } 103 | 104 | // ViceroyEnv returns the environment variables that must be set for a 'go build' command given the provided 'GoBuildOpts'. 105 | func ViceroyEnv(opts *GoBuildOpts) []containers.Env { 106 | var ( 107 | os = opts.OS 108 | arch = opts.Arch 109 | ) 110 | 111 | env := []containers.Env{ 112 | containers.EnvVar("VICEROYOS", os), 113 | containers.EnvVar("GOOS", os), 114 | containers.EnvVar("VICEROYARCH", arch), 115 | containers.EnvVar("GOARCH", arch), 116 | } 117 | 118 | if arch == "arm" { 119 | env = append(env, containers.EnvVar("VICEROYARM", string(opts.GoARM))) 120 | } 121 | 122 | if opts.CGOEnabled { 123 | env = append(env, containers.EnvVar("CGO_ENABLED", "1")) 124 | 125 | // https://github.com/mattn/go-sqlite3/issues/1164#issuecomment-1635253695 126 | env = append(env, containers.EnvVar("CGO_CFLAGS", "-D_LARGEFILE64_SOURCE")) 127 | } else { 128 | env = append(env, containers.EnvVar("CGO_ENABLED", "0")) 129 | } 130 | 131 | if opts.ExperimentalFlags != nil { 132 | env = append(env, containers.EnvVar("GOEXPERIMENT", strings.Join(opts.ExperimentalFlags, ","))) 133 | } 134 | 135 | if opts.CC != "" { 136 | env = append(env, containers.EnvVar("CC", "viceroycc")) 137 | } 138 | 139 | return env 140 | } 141 | -------------------------------------------------------------------------------- /msi/build.go: -------------------------------------------------------------------------------- 1 | package msi 2 | 3 | import ( 4 | "fmt" 5 | 6 | "dagger.io/dagger" 7 | "github.com/grafana/grafana-build/containers" 8 | ) 9 | 10 | func Build(d *dagger.Client, builder *dagger.Container, targz *dagger.File, version string, enterprise bool) (*dagger.File, error) { 11 | wxsFiles, err := WXSFiles(version, enterprise) 12 | if err != nil { 13 | return nil, fmt.Errorf("error generating wxs files: %w", err) 14 | } 15 | 16 | f := containers.ExtractedArchive(d, targz) 17 | builder = builder.WithDirectory("/src/grafana", f, dagger.ContainerWithDirectoryOpts{ 18 | // Hack from grafana/build-pipeline: Remove files with names too long... 19 | Exclude: []string{ 20 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.test.ts", 21 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.ts", 22 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts", 23 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts", 24 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts", 25 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts", 26 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts", 27 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts", 28 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/insights_analytics/insights_analytics_datasource.ts", 29 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts", 30 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts", 31 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.test.tsx", 32 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AzureCredentialsForm.test.tsx", 33 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.test.tsx", 34 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AnalyticsConfig.test.tsx.snap", 35 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AzureCredentialsForm.test.tsx.snap", 36 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap", 37 | "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/ConfigEditor.test.tsx.snap", 38 | "storybook", 39 | }, 40 | }).WithWorkdir("/src") 41 | 42 | for _, v := range wxsFiles { 43 | builder = builder.WithNewFile(v.Name, v.Contents) 44 | } 45 | 46 | // 1. `heat`: create 'grafana.wxs' 47 | // 2. 'candle': Compile .wxs files into .wixobj 48 | // 3. `light`: assembles the MSI 49 | builder = builder. 50 | WithExec([]string{"/bin/sh", "-c", "cp -r /src/resources/resources/* /src && rm -rf /src/resources"}). 51 | WithExec([]string{"/bin/sh", "-c", `WINEPATH=$(winepath /src/wix3) wine heat dir /src -platform x64 -sw5150 -srd -cg GrafanaX64 -gg -sfrag -dr GrafanaX64Dir -template fragment -out $(winepath -w grafana.wxs)`}). 52 | WithExec([]string{"winepath"}). 53 | WithExec([]string{"mkdir", "/root/.wine/drive_c/temp"}) 54 | 55 | for _, name := range []string{ 56 | "grafana-service.wxs", 57 | "grafana-firewall.wxs", 58 | "grafana.wxs", 59 | "grafana-product.wxs", 60 | } { 61 | builder = builder.WithExec([]string{"/bin/sh", "-c", fmt.Sprintf(`WINEPATH=$(winepath /src/wix3) wine candle -ext WixFirewallExtension -ext WixUtilExtension -v -arch x64 $(winepath -w %s)`, name)}) 62 | } 63 | builder = builder. 64 | WithExec([]string{"/bin/bash", "-c", "WINEPATH=$(winepath /src/wix3) wine light -cultures:en-US -ext WixUIExtension.dll -ext WixFirewallExtension -ext WixUtilExtension -v -sval -spdb grafana-service.wixobj grafana-firewall.wixobj grafana.wixobj grafana-product.wixobj -out $(winepath -w /src/grafana.msi)"}) 65 | 66 | return builder.File("/src/grafana.msi"), nil 67 | } 68 | --------------------------------------------------------------------------------