diff --git a/README.md b/README.md index 00ce28782..3101f8294 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,8 @@ are not enforcing it though. We do remove the `v` prefix and then enforce that the next character is a number. So, `v0.1.0` and `0.1.0` are virtually the same and are both accepted, while `version0.1.0` is not. +If you don't want to create a tag yet but instead simply create a package based on the latest commit, then you can also use the `--snapshot` flag. + Now you can run GoReleaser at the root of your repository: ```console @@ -164,7 +166,7 @@ func main() { } ``` -`version` will always be the name of the current Git tag. +`version` will be the current Git tag or the name of the snapshot if you're using the `--snapshot` flag. ## Release customization @@ -287,6 +289,20 @@ release: You can also specify a release notes file in markdown format using the `--release-notes` flag. +### Snapshot customization + +```yml +# goreleaser.yml +snapshot: + # Allows you to change the name of the generated snapshot + # releases. The following variables are available: + # - Commit + # - Tag + # - Timestamp + # Default: SNAPSHOT-{{.Commit}} + name_template: SNAPSHOT-{{.Commit}} +``` + ### Homebrew tap customization The brew section specifies how the formula should be created. diff --git a/config/config.go b/config/config.go index d5fb09b79..886851d5f 100644 --- a/config/config.go +++ b/config/config.go @@ -83,13 +83,19 @@ type FPM struct { License string `yaml:",omitempty"` } +// Snapshot config +type Snapshot struct { + NameTemplate string `yaml:"name_template,omitempty"` +} + // Project includes all project configuration type Project struct { - Release Release `yaml:",omitempty"` - Brew Homebrew `yaml:",omitempty"` - Build Build `yaml:",omitempty"` - Archive Archive `yaml:",omitempty"` - FPM FPM `yaml:",omitempty"` + Release Release `yaml:",omitempty"` + Brew Homebrew `yaml:",omitempty"` + Build Build `yaml:",omitempty"` + Archive Archive `yaml:",omitempty"` + FPM FPM `yaml:",omitempty"` + Snapshot Snapshot `yaml:",omitempty"` // test only property indicating the path to the dist folder Dist string `yaml:"-"` diff --git a/context/context.go b/context/context.go index 46bbda57e..dd3412d23 100644 --- a/context/context.go +++ b/context/context.go @@ -33,6 +33,7 @@ type Context struct { Version string Validate bool Publish bool + Snapshot bool } var lock sync.Mutex diff --git a/goreleaserlib/goreleaser.go b/goreleaserlib/goreleaser.go index ea725936e..153c9f844 100644 --- a/goreleaserlib/goreleaser.go +++ b/goreleaserlib/goreleaser.go @@ -70,6 +70,11 @@ func Release(flags Flags) error { log.Println("Loaded custom release notes from", notes) ctx.ReleaseNotes = string(bts) } + ctx.Snapshot = flags.Bool("snapshot") + if ctx.Snapshot { + log.Println("Publishing disabled in snapshot mode") + ctx.Publish = false + } for _, pipe := range pipes { log.Println(pipe.Description()) log.SetPrefix(" -> ") diff --git a/main.go b/main.go index 069c87e43..0af8bb37f 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,10 @@ func main() { Name: "skip-publish", Usage: "Skip all publishing pipes of the release", }, + cli.BoolFlag{ + Name: "snapshot", + Usage: "Generate an unversioned snapshot release", + }, } app.Action = func(c *cli.Context) error { log.Printf("Running goreleaser %v\n", version) diff --git a/pipeline/defaults/defaults.go b/pipeline/defaults/defaults.go index 2ad854c98..e0b65dc3d 100644 --- a/pipeline/defaults/defaults.go +++ b/pipeline/defaults/defaults.go @@ -15,6 +15,9 @@ var defaultFiles = []string{"licence", "license", "readme", "changelog"} // NameTemplate default name_template for the archive. const NameTemplate = "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" +// SnapshotNameTemplate represents the default format for snapshot release names. +const SnapshotNameTemplate = "SNAPSHOT-{{.Commit}}" + // Pipe for brew deployment type Pipe struct{} @@ -25,6 +28,9 @@ func (Pipe) Description() string { // Run the pipe func (Pipe) Run(ctx *context.Context) error { + if ctx.Config.Snapshot.NameTemplate == "" { + ctx.Config.Snapshot.NameTemplate = SnapshotNameTemplate + } if ctx.Config.Release.GitHub.Name == "" { repo, err := remoteRepo() ctx.Config.Release.GitHub = repo diff --git a/pipeline/env/env.go b/pipeline/env/env.go index 67f6e5c71..4beb2780f 100644 --- a/pipeline/env/env.go +++ b/pipeline/env/env.go @@ -24,6 +24,10 @@ func (Pipe) Description() string { // Run the pipe func (Pipe) Run(ctx *context.Context) (err error) { ctx.Token = os.Getenv("GITHUB_TOKEN") + if !ctx.Publish { + log.Println("GITHUB_TOKEN not validated because publishing has been disabled") + return nil + } if !ctx.Validate { log.Println("Skipped validations because --skip-validate is set") return nil diff --git a/pipeline/env/env_test.go b/pipeline/env/env_test.go index 810bb5bd8..f58dc9397 100644 --- a/pipeline/env/env_test.go +++ b/pipeline/env/env_test.go @@ -19,6 +19,7 @@ func TestValidEnv(t *testing.T) { var ctx = &context.Context{ Config: config.Project{}, Validate: true, + Publish: true, } assert.NoError(Pipe{}.Run(ctx)) } @@ -29,10 +30,33 @@ func TestInvalidEnv(t *testing.T) { var ctx = &context.Context{ Config: config.Project{}, Validate: true, + Publish: true, } assert.Error(Pipe{}.Run(ctx)) } +func TestInvalidEnvDisabled(t *testing.T) { + assert := assert.New(t) + assert.NoError(os.Unsetenv("GITHUB_TOKEN")) + var ctx = &context.Context{ + Config: config.Project{}, + Validate: false, + Publish: true, + } + assert.NoError(Pipe{}.Run(ctx)) +} + +func TestEnvWithoutPublish(t *testing.T) { + assert := assert.New(t) + assert.NoError(os.Unsetenv("GITHUB_TOKEN")) + var ctx = &context.Context{ + Config: config.Project{}, + Validate: true, + Publish: false, + } + assert.NoError(Pipe{}.Run(ctx)) +} + func TestSkipValidate(t *testing.T) { assert := assert.New(t) var ctx = &context.Context{ diff --git a/pipeline/git/git.go b/pipeline/git/git.go index 663ee85eb..237011577 100644 --- a/pipeline/git/git.go +++ b/pipeline/git/git.go @@ -3,10 +3,14 @@ package git import ( + "bytes" "fmt" "log" "regexp" "strings" + "time" + + "text/template" "github.com/goreleaser/goreleaser/context" ) @@ -38,6 +42,10 @@ func (e ErrWrongRef) Error() string { return fmt.Sprintf("git tag %v was not made against commit %v", e.tag, e.commit) } +// ErrNoTag happens if the underlying git repository doesn't contain any tags +// but no snapshot-release was requested. +var ErrNoTag = fmt.Errorf("git doesn't contain any tags. Either add a tag or use --snapshot") + // Pipe for brew deployment type Pipe struct{} @@ -52,38 +60,77 @@ func (Pipe) Run(ctx *context.Context) (err error) { if err != nil { return } + if tag == "" && !ctx.Snapshot { + return ErrNoTag + } ctx.Git = context.GitInfo{ CurrentTag: tag, Commit: commit, } if ctx.ReleaseNotes == "" { - log, err := getChangelog(tag) + var log string + if tag != "" { + log, err = getChangelog(tag) + } else { + log, err = getChangelog(commit) + } if err != nil { return err } ctx.ReleaseNotes = fmt.Sprintf("## Changelog\n\n%v", log) } - // removes usual `v` prefix - ctx.Version = strings.TrimPrefix(tag, "v") + if tag == "" || ctx.Snapshot { + snapshotName, err := getSnapshotName(ctx, tag, commit) + if err != nil { + log.Printf("Failed to generate snapshot name: %s", err.Error()) + return err + } + ctx.Version = snapshotName + } else { + // removes usual `v` prefix + ctx.Version = strings.TrimPrefix(tag, "v") + } if !ctx.Validate { log.Println("Skipped validations because --skip-validate is set") return nil } - return validate(commit, tag, ctx.Version) + return validate(commit, tag, ctx.Version, ctx.Snapshot) } -func validate(commit, tag, version string) error { - matches, err := regexp.MatchString("^[0-9.]+", version) - if err != nil || !matches { - return ErrInvalidVersionFormat{version} +type snapshotNameData struct { + Commit string + Tag string + Timestamp int64 +} + +func getSnapshotName(ctx *context.Context, tag, commit string) (string, error) { + tmpl, err := template.New("snapshot").Parse(ctx.Config.Snapshot.NameTemplate) + var out bytes.Buffer + if err != nil { + return "", err + } + if err := tmpl.Execute(&out, snapshotNameData{Commit: commit, Tag: tag, Timestamp: time.Now().Unix()}); err != nil { + return "", err + } + return out.String(), nil +} + +func validate(commit, tag, version string, isSnapshot bool) error { + if !isSnapshot { + matches, err := regexp.MatchString("^[0-9.]+", version) + if err != nil || !matches { + return ErrInvalidVersionFormat{version} + } } out, err := git("status", "-s") if strings.TrimSpace(out) != "" || err != nil { return ErrDirty{out} } - _, err = cleanGit("describe", "--exact-match", "--tags", "--match", tag) - if err != nil { - return ErrWrongRef{commit, tag} + if !isSnapshot { + _, err = cleanGit("describe", "--exact-match", "--tags", "--match", tag) + if err != nil { + return ErrWrongRef{commit, tag} + } } return nil } @@ -108,7 +155,7 @@ func gitLog(refs ...string) (string, error) { func getInfo() (tag, commit string, err error) { tag, err = cleanGit("describe", "--tags", "--abbrev=0") if err != nil { - return + log.Printf("Failed to retrieve current tag: %s", err.Error()) } commit, err = cleanGit("show", "--format='%H'", "HEAD") return diff --git a/pipeline/git/git_test.go b/pipeline/git/git_test.go index 35e8da3ab..da3d98ad2 100644 --- a/pipeline/git/git_test.go +++ b/pipeline/git/git_test.go @@ -50,6 +50,58 @@ func TestNewRepository(t *testing.T) { assert.Error(Pipe{}.Run(ctx)) } +func TestNoTagsSnapshot(t *testing.T) { + assert := assert.New(t) + _, back := createAndChdir(t) + defer back() + gitInit(t) + gitCommit(t, "first") + var ctx = &context.Context{ + Config: config.Project{ + Snapshot: config.Snapshot{NameTemplate: "SNAPSHOT-{{.Commit}}"}, + }, + Snapshot: true, + Publish: false, + } + assert.NoError(Pipe{}.Run(ctx)) + assert.Contains(ctx.Version, "SNAPSHOT-") +} + +func TestNoTagsSnapshotInvalidTemplate(t *testing.T) { + assert := assert.New(t) + _, back := createAndChdir(t) + defer back() + gitInit(t) + gitCommit(t, "first") + var ctx = &context.Context{ + Config: config.Project{ + Snapshot: config.Snapshot{NameTemplate: "{{"}, + }, + Snapshot: true, + Publish: false, + } + assert.Error(Pipe{}.Run(ctx)) +} + +// TestNoTagsNoSnapshot covers the situation where a repository +// only contains simple commits and no tags. In this case you have +// to set the --snapshot flag otherwise an error is returned. +func TestNoTagsNoSnapshot(t *testing.T) { + assert := assert.New(t) + _, back := createAndChdir(t) + defer back() + gitInit(t) + gitCommit(t, "first") + var ctx = &context.Context{ + Config: config.Project{ + Snapshot: config.Snapshot{NameTemplate: "SNAPSHOT-{{.Commit}}"}, + }, + Snapshot: false, + Publish: false, + } + assert.Error(Pipe{}.Run(ctx)) +} + func TestInvalidTagFormat(t *testing.T) { var assert = assert.New(t) _, back := createAndChdir(t)