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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Release

on:
push:
tags:
- 'v*'

jobs:
publish:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Dagger
run: |
curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sudo -E sh
- name: Extract version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Publish release
run: |
dagger call publish \
--version "${{ steps.version.outputs.version }}" \
--one-password-service-account-production env:OP_SERVICE_ACCOUNT_PRODUCTION \
--github-token env:GITHUB_TOKEN \
--clean \
--progress plain
env:
OP_SERVICE_ACCOUNT_PRODUCTION: ${{ secrets.OP_SERVICE_ACCOUNT_PRODUCTION }}
GITHUB_TOKEN: ${{ secrets.GH_PAT }}

docs:
runs-on: ubuntu-24.04
needs: publish
steps:
- uses: actions/checkout@v4
- name: Install Dagger
run: |
curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sudo -E sh
- name: Generate docs PR
run: |
dagger call generate-docs \
--github-token env:GITHUB_TOKEN \
--skip-validation \
--progress plain
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
16 changes: 1 addition & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,4 @@ build:

.PHONY: release
release:
dagger call release \
--one-password-service-account-production env:OP_SERVICE_ACCOUNT_PRODUCTION \
--version $(version) \
--github-token env:GITHUB_TOKEN \
--progress plain
@echo ""
@echo "✓ Release completed successfully"
@echo "Generating documentation PR..."
@$(MAKE) docs

.PHONY: docs
docs:
dagger --progress plain call generate-docs \
--github-token env:GITHUB_TOKEN \
--progress plain
./scripts/release-tag.sh $(version)
12 changes: 9 additions & 3 deletions dagger/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ func (r *Replicated) GenerateDocs(
source *dagger.Directory,

githubToken *dagger.Secret,

// Skip git tree validation (useful in CI where the checkout is a tag, not main)
// +default=false
skipValidation bool,
) error {
err := checkGitTree(ctx, source, githubToken)
if err != nil {
return errors.Wrap(err, "failed to check git tree")
if !skipValidation {
err := checkGitTree(ctx, source, githubToken)
if err != nil {
return errors.Wrap(err, "failed to check git tree")
}
}

latestVersion, err := getLatestVersion(ctx, githubToken)
Expand Down
182 changes: 113 additions & 69 deletions dagger/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import (

var goreleaserVersion = "v2.10.2"

func (r *Replicated) Release(
// Publish builds and publishes the release artifacts (Docker images, GoReleaser)
// for a version that has already been tagged and pushed. This is intended to be
// called from CI after the tag is created.
func (r *Replicated) Publish(
ctx context.Context,

// +defaultPath="./"
source *dagger.Directory,

// The version tag to publish (e.g. "v1.2.3")
version string,

// +default=false
Expand All @@ -32,87 +36,31 @@ func (r *Replicated) Release(

githubToken *dagger.Secret,
) error {
err := checkGitTree(ctx, source, githubToken)
if err != nil {
return errors.Wrap(err, "failed to check git tree")
}

previousVersionTag, err := getLatestVersion(ctx, githubToken)
if err != nil {
return errors.Wrap(err, "failed to get latest version")
}

previousReleaseBranchName, err := getReleaseBranchName(ctx, previousVersionTag)
if err != nil {
return errors.Wrap(err, "failed to get release branch name")
}

major, minor, patch, err := getNextVersion(ctx, previousVersionTag, version)
if err != nil {
return errors.Wrap(err, "failed to get next version")
}

fmt.Printf("Releasing as version %d.%d.%d\n", major, minor, patch)

// replace the version in the Makefile
buildFileContent, err := source.File("./pkg/version/build.go").Contents(ctx)
if err != nil {
return errors.Wrap(err, "failed to get build file contents")
}
buildFileContent = strings.ReplaceAll(buildFileContent, "const version = \"unknown\"", fmt.Sprintf("const version = \"%d.%d.%d\"", major, minor, patch))
updatedSource := source.WithNewFile("./pkg/version/build.go", buildFileContent)

releaseBranchName := fmt.Sprintf("release-%d.%d.%d", major, minor, patch)
githubTokenPlaintext, err := githubToken.Plaintext(ctx)
if err != nil {
return errors.Wrap(err, "failed to get github token plaintext")
}

// mount that and commit the updated build.go to git (don't push)
// so that goreleaser won't have a dirty git tree error
gitCommitContainer := dag.Container().
From("alpine/git:latest").
WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource).
WithWorkdir("/go/src/github.com/replicatedhq/replicated").
WithExec([]string{"git", "config", "user.email", "release@replicated.com"}).
WithExec([]string{"git", "config", "user.name", "Replicated Release Pipeline"}).
WithExec([]string{"git", "remote", "add", "dagger", fmt.Sprintf("https://%s@github.com/replicatedhq/replicated.git", githubTokenPlaintext)}).
WithExec([]string{"git", "checkout", "-b", releaseBranchName}).
WithExec([]string{"git", "add", "pkg/version/build.go"}).
WithExec([]string{"git", "commit", "-m", fmt.Sprintf("Set version to %d.%d.%d", major, minor, patch)}).
WithExec([]string{"git", "push", "dagger", releaseBranchName})
_, err = gitCommitContainer.Stdout(ctx)
parsedVersion, err := semver.NewVersion(version)
if err != nil {
return errors.Wrap(err, "failed to get git commit stdout")
return errors.Wrapf(err, "failed to parse version %q", version)
}
updatedSource = gitCommitContainer.Directory("/go/src/github.com/replicatedhq/replicated")

major := parsedVersion.Major()
minor := parsedVersion.Minor()
patch := parsedVersion.Patch()
nextVersionTag := fmt.Sprintf("v%d.%d.%d", major, minor, patch)

tagContainer := dag.Container().
From("alpine/git:latest").
WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource).
WithWorkdir("/go/src/github.com/replicatedhq/replicated").
With(CacheBustingExec([]string{"git", "tag", nextVersionTag})).
With(CacheBustingExec([]string{"git", "push", "dagger", nextVersionTag})).
With(CacheBustingExec([]string{"git", "fetch", "dagger", previousReleaseBranchName})).
With(CacheBustingExec([]string{"git", "fetch", "dagger", "--tags"}))
_, err = tagContainer.Stdout(ctx)
fmt.Printf("Publishing version %d.%d.%d\n", major, minor, patch)

previousVersionTag, err := getLatestVersion(ctx, githubToken)
if err != nil {
return errors.Wrap(err, "failed to get tag stdout")
return errors.Wrap(err, "failed to get latest version")
}

// copy the source that has the tag included in it
updatedSource = tagContainer.Directory("/go/src/github.com/replicatedhq/replicated")

goModCache := dag.CacheVolume("replicated-go-mod-122")
goBuildCache := dag.CacheVolume("replicated-go-build-121")

replicatedBinary := dag.Container(dagger.ContainerOpts{
Platform: "linux/amd64",
}).
From("golang:1.24").
WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource).
WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source).
WithoutFile("/go/src/github.com/replicatedhq/replicated/bin/replicated").
WithWorkdir("/go/src/github.com/replicatedhq/replicated").
WithMountedCache("/go/pkg/mod", goModCache).
Expand Down Expand Up @@ -182,7 +130,7 @@ func (r *Replicated) Release(
Version: goreleaserVersion,
Ctr: goreleaserContainer,
}).
WithSource(updatedSource).
WithSource(source).
Snapshot(ctx, dagger.GoreleaserSnapshotOpts{
Clean: clean,
})
Expand All @@ -195,7 +143,7 @@ func (r *Replicated) Release(
Version: goreleaserVersion,
Ctr: goreleaserContainer,
}).
WithSource(updatedSource).
WithSource(source).
Release(ctx, dagger.GoreleaserReleaseOpts{
Clean: clean,
})
Expand All @@ -207,6 +155,102 @@ func (r *Replicated) Release(
return nil
}

// Release handles git operations (branch, tag, push) and then delegates to
// Publish for building and publishing artifacts. Prefer using `make release` +
// CI for new releases, which splits these two phases between local and CI.
func (r *Replicated) Release(
ctx context.Context,

// +defaultPath="./"
source *dagger.Directory,

version string,

// +default=false
snapshot bool,

// +default=false
clean bool,

onePasswordServiceAccountProduction *dagger.Secret,

githubToken *dagger.Secret,
) error {
err := checkGitTree(ctx, source, githubToken)
if err != nil {
return errors.Wrap(err, "failed to check git tree")
}

previousVersionTag, err := getLatestVersion(ctx, githubToken)
if err != nil {
return errors.Wrap(err, "failed to get latest version")
}

previousReleaseBranchName, err := getReleaseBranchName(ctx, previousVersionTag)
if err != nil {
return errors.Wrap(err, "failed to get release branch name")
}

major, minor, patch, err := getNextVersion(ctx, previousVersionTag, version)
if err != nil {
return errors.Wrap(err, "failed to get next version")
}

fmt.Printf("Releasing as version %d.%d.%d\n", major, minor, patch)

// replace the version in build.go
buildFileContent, err := source.File("./pkg/version/build.go").Contents(ctx)
if err != nil {
return errors.Wrap(err, "failed to get build file contents")
}
buildFileContent = strings.ReplaceAll(buildFileContent, "const version = \"unknown\"", fmt.Sprintf("const version = \"%d.%d.%d\"", major, minor, patch))
updatedSource := source.WithNewFile("./pkg/version/build.go", buildFileContent)

releaseBranchName := fmt.Sprintf("release-%d.%d.%d", major, minor, patch)
githubTokenPlaintext, err := githubToken.Plaintext(ctx)
if err != nil {
return errors.Wrap(err, "failed to get github token plaintext")
}

// commit the updated build.go to a release branch so goreleaser has a clean tree
gitCommitContainer := dag.Container().
From("alpine/git:latest").
WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource).
WithWorkdir("/go/src/github.com/replicatedhq/replicated").
WithExec([]string{"git", "config", "user.email", "release@replicated.com"}).
WithExec([]string{"git", "config", "user.name", "Replicated Release Pipeline"}).
WithExec([]string{"git", "remote", "add", "dagger", fmt.Sprintf("https://%s@github.com/replicatedhq/replicated.git", githubTokenPlaintext)}).
WithExec([]string{"git", "checkout", "-b", releaseBranchName}).
WithExec([]string{"git", "add", "pkg/version/build.go"}).
WithExec([]string{"git", "commit", "-m", fmt.Sprintf("Set version to %d.%d.%d", major, minor, patch)}).
WithExec([]string{"git", "push", "dagger", releaseBranchName})
_, err = gitCommitContainer.Stdout(ctx)
if err != nil {
return errors.Wrap(err, "failed to get git commit stdout")
}
updatedSource = gitCommitContainer.Directory("/go/src/github.com/replicatedhq/replicated")

nextVersionTag := fmt.Sprintf("v%d.%d.%d", major, minor, patch)

tagContainer := dag.Container().
From("alpine/git:latest").
WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", updatedSource).
WithWorkdir("/go/src/github.com/replicatedhq/replicated").
With(CacheBustingExec([]string{"git", "tag", nextVersionTag})).
With(CacheBustingExec([]string{"git", "push", "dagger", nextVersionTag})).
With(CacheBustingExec([]string{"git", "fetch", "dagger", previousReleaseBranchName})).
With(CacheBustingExec([]string{"git", "fetch", "dagger", "--tags"}))
_, err = tagContainer.Stdout(ctx)
if err != nil {
return errors.Wrap(err, "failed to get tag stdout")
}

updatedSource = tagContainer.Directory("/go/src/github.com/replicatedhq/replicated")

// Delegate to Publish for building and publishing artifacts
return r.Publish(ctx, updatedSource, nextVersionTag, snapshot, clean, onePasswordServiceAccountProduction, githubToken)
}

func getNextVersion(ctx context.Context, latestVersion string, version string) (int64, int64, int64, error) {
parsedLatestVersion, err := semver.NewVersion(latestVersion)
if err != nil {
Expand Down
Loading
Loading