Skip to content

Commit

Permalink
Merge pull request #76 from ajoberstar/composite
Browse files Browse the repository at this point in the history
0.6.0 changes
  • Loading branch information
ajoberstar committed Apr 15, 2018
2 parents c332caf + dfdb2b5 commit fbd0b2f
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 53 deletions.
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

Most build tools and release systems require you to hardcode a version number into
a file in your source repository. This results in commit messages like "Bumping
version number.". Even if you don't have to do this manually, your release plugin
version number". Even if you don't have to do this manually, your release plugin
probably modifies your build file and commits the new version.

Git already contains tags with a version number pointing to a
Expand Down Expand Up @@ -41,7 +41,7 @@ For example, this API's scheme includes 3 stages:

- **final** (e.g. 1.0.0) the fully-tested version ready for end-user consumption
- **rc** (e.g. 1.1.0-rc.1) release candidates, versions believed to be ready for release after final testing
- **milestone** (e.g. 1.1.0-milestone.4) versions containing a significant piece of functionality on the road
- **beta** (e.g. 1.1.0-beta.4) versions containing a significant piece of functionality on the road
to the next version

## What is it?
Expand All @@ -51,7 +51,7 @@ Reckon is two things:
- an API to infer your next version from a Git repository
- applications of that API in various tools (initially, just Gradle)

### Reckon Version Scheme
### Stage Version Scheme

Reckon uses an opinionated subset of [SemVer](http://semver.org), meant to provide more structure around how the
pre-release versions are managed.
Expand Down Expand Up @@ -115,9 +115,11 @@ reckon {

Execute Gradle providing the properties, as needed:

* `reckon.scope` - one of `major`, `minor`, or `patch` (defaults to `minor`) to specify which component of the previous release should be incremented
* `reckon.stage` - (if you used `stageFromProp`) one of the values passed to `stageFromProp` (defaults to the first alphabetically) to specify what phase of development you are in
* `reckon.snapshot` - (if you used `snapshotFromProp`) one of `true` or `false` (defaults to `true`) to determine whether a snapshot should be made
- `reckon.scope` - one of `major`, `minor`, or `patch` (defaults to `minor`) to specify which component of the previous release should be incremented
- `reckon.stage`
- (if you used `stageFromProp`) one of the values passed to `stageFromProp` (defaults to the first alphabetically) to specify what phase of development you are in
- (if you used `snapshotFromProp`) either `snapshot` or `final` (defaults to `snapshot`) to specify what phase of development you are in
- `reckon.snapshot` - (**deprecated**, if you used `snapshotFromProp`) one of `true` or `false` (defaults to `true`) to determine whether a snapshot should be made

When Gradle executes, the version will be inferred as soon as something tries to access it. This will be output to the console (as below).

Expand All @@ -138,6 +140,12 @@ Reckon's Gradle plugin also provides two tasks:
./gradlew reckonTagPush
```

It's suggested you add dependencies to these tasks to ensure your project is in the right state before tagging it. For example:

```
reckonTagCreate.dependsOn check
```

## Contributing

Contributions are very welcome and are accepted through pull requests.
Expand Down
167 changes: 162 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

These are the rules that reckon presumes are true, both informing how it reads a repo's history and how it calculates the next version:

1. **Existing version tags MUST be SemVer compliant or they will be ignored**. Any existing tags you want to be considered by algorithm **must** be SemVer compliant strings (optionally, prefixed with `v`). i.e. `v2.3.0` and `1.0.4-beta.1+abcde` **are** compliant, `1.0` **is not** (only 2 segments).
1. **Existing version tags MUST be SemVer compliant or they will be ignored**. Any existing tags you want to be considered by the algorithm **must** be SemVer compliant strings (optionally, prefixed with `v`). i.e. `v2.3.0` and `1.0.4-beta.1+abcde` **are** compliant, `1.0` **is not** (only 2 segments).
1. **NO duplicates version in the history.** A single version MUST not be produced from two different commits.
1. **Version numbers MUST increase.** If version X's commit is an ancestor of version Y's commit, X < Y.
1. **NO skipping final versions.** Final versions MUST increase using only the rules from [SemVer 6, 7, 8](http://semver.org/spec/v2.0.0.html). e.g. If X=1.2.3, Y must be one of 1.2.4, 1.3.0, 2.0.0.
1. **Two branches MUST NOT create tagged pre-releases for the same targeted final version.**
* If the branches have a shared merge base the version being inferred must skip the targeted final version and increment a second time.
* If no shared merge base the inference should fail.

1. **Final versions MUST NOT be re-released as a pre-release.** Once you release a final version (e.g. 1.2.3), that same commit cannot be re-released as a pre-release (e.g. 1.3.0-beta.1). However, the commit can be re-released as a final (e.g. 1.3.0).
1. **Final and significant versions MUST be released from a clean repo.** If there are any uncommitted changes, the version will not be treated as a final or significant.

### Inputs

Expand All @@ -24,8 +25,164 @@ In order to infer the next version, reckon needs two pieces of input:

These inputs can be provided directly by the user or using a custom implementation that might detect them from elsewhere.

### Inference
## Examples

This is a continuous example of how the inference algorithm works in practice with the Gradle plugin.

```groovy
plugins {
id 'org.ajoberstar.reckon' version '<version>'
// other plugins
}
// ...
reckon {
normal = scopeFromProp()
preRelease = stageFromProp('beta', 'rc', 'final')
}
// ...
```

**You have some changes in the repository, but no commits yet.**

```
$ ./gradlew build
Reckoned version: 0.1.0-beta.0.0+uncommitted
```

This used the default of `minor` scope and `beta` stage (`beta` is the first stage alphabetically). Since you have some changes in your repo that aren't committed, indicate that in the build

**Now make a commit, but run the same Gradle command.**

```
$ ./gradlew build
Reckoned version: 0.1.0-beta.0.1+e06c68a863eb6ceaf889ee5802f478c10c1464bb
```

The version now shows 1 commit since a normal has been released, and the full commit hash in the build metadata.

**Now make some more changes, but don't commit them**

```
$ ./gradlew build
Reckoned version: 0.1.0-beta.0.1+e06c68a863eb6ceaf889ee5802f478c10c1464bb.uncommitted
```

The version hasn't changed except to indicate that you have uncommitted changes.

**Now commit this and let's release a minor version beta**

You can specify the scope or leave it off, since `minor` is the default.

```
$ ./gradlew build reckonTagPush -Preckon.scope=minor -Preckon.stage=beta
$ ./gradlew build reckonTagPush -Preckon.stage=beta
Reckoned version: 0.1.0-beta.1
```
Note that you no longer have a count of commits or a commit hash, since this is a significant version that will result in a tag.

**Now just run the build again**

```
$ ./gradlew build
Reckoned version: 0.1.0-beta.1
```

The current `HEAD` is tagged and you haven't changed anything, or indicated you wanted a different version by providing scope or stage. Reckon assumes you just want to rebuild the existing version.

**Make a bunch more commits and build again**

```
$ ./gradlew build
Reckoned version: 0.1.0-beta.1.8+e06c68a863eb6ceaf889ee5802f478c10c1464bb
```

We're back to an insignificant version, since you didn't indicate a stage. Again we get the commit count and hash.

**Release another beta**

```
$ ./gradlew build reckonTagPush -Preckon.stage=beta
Reckoned version: 0.1.0-beta.2
```

While you already could have left the scope of with the default of `minor`, you can also leave it off because you just want to continue development towards the _target_ normal version you've been working on.

**Release this commit as an rc**

You've decided there's enough features in this release, and you're ready to treat it as a release-candidate.

```
$ ./gradlew build reckonTagPush -Preckon.stage=rc
Reckoned version: 0.1.0-rc.1
```

Note that the count after the stage resets to 1.

**Make a bug fix but don't commit it yet**

```
$ ./gradlew build
Reckoned version: 0.1.0-rc.1.8+e06c68a863eb6ceaf889ee5802f478c10c1464bb.uncommitted
```

Note that the commit count does not reset (since it's based on commits since the last normal).

**Commit the change and release another rc**

```
$ ./gradlew build reckonTagPush -Preckon.stage=rc
Reckoned version: 0.1.0-rc.2
```

**Release this as a final**

You've decided there aren't any bugs in this release and you're ready to make it official.

```
$ ./gradlew build reckonTagPush -Preckon.stage=final
Reckoned version: 0.1.0
```

**Make this the 1.0.0**

You've decided this is feature complete and you're ready to make your 1.0.0 release.

```
$ ./gradlew build reckonTagPush -Preckon.scope=major -Preckon.stage=final
Reckoned version: 1.0.0
```

**Make some commits and build**

```
$ ./gradlew build
Reckoned version: 1.1.0-beta.0.4+7836cf7469dd00fe1035ea14ef1faaa7452cc5e0
```

Note that `minor` was again used as a default, same with `beta`, and that your commit count reset since a normal was released.

**Release this as a patch rc**

```
$ ./gradlew build reckonTagPush -Preckon.scope=patch -Preckon.stage=rc
Reckoned version: 1.0.1-rc.1
```

**Release as a final patch**

```
$ ./gradlew build reckonTagPush -Preckon.stage=final
Reckoned version: 1.0.1
```

While the default is usually `minor`, if you're already developing towards a `patch` or `major` those will be used as defaults instead.

**Make some changes but don't commit them and run again**

Reckon will use the history of your repository to determine what version your changes are based on and the inputs above will indicate how the version should be incremented from that previous one.
```
$ ./gradlew build reckonTagPush -Preckon.stage=final
Reckoned version: 1.0.1-beta.0.0+c306b0d062ac78cc28170a607c6f8ddc5e99cf70.uncommitted
```

_More detail will be added in the future._
Normally if your `HEAD` is already tagged, that version is used as a rebuild. However, if your repo is dirty, it knows it's not a rebuild.
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,28 @@
import org.ajoberstar.reckon.core.VcsInventory;

public final class SnapshotPreReleaseStrategy implements PreReleaseStrategy {
private final BiFunction<VcsInventory, Version, Optional<Boolean>> snapshotCalc;
public static final String FINAL_STAGE = "final";
public static final String SNAPSHOT_STAGE = "snapshot";

public SnapshotPreReleaseStrategy(BiFunction<VcsInventory, Version, Optional<Boolean>> snapshotCalc) {
this.snapshotCalc = snapshotCalc;
private final BiFunction<VcsInventory, Version, Optional<String>> stageCalc;

public SnapshotPreReleaseStrategy(BiFunction<VcsInventory, Version, Optional<String>> stageCalc) {
this.stageCalc = stageCalc;
}

@Override
public Version reckonTargetVersion(VcsInventory inventory, Version targetNormal) {
Optional<Boolean> maybeSnapshot = snapshotCalc.apply(inventory, targetNormal);
Optional<String> maybeStage = stageCalc.apply(inventory, targetNormal);

if (inventory.isClean() && inventory.getCurrentVersion().isPresent() && !maybeSnapshot.isPresent()) {
if (inventory.isClean() && inventory.getCurrentVersion().isPresent() && !maybeStage.isPresent()) {
// rebuild
return inventory.getCurrentVersion().get();
} else {
if (maybeSnapshot.orElse(true)) {
String stage = maybeStage.orElse(SNAPSHOT_STAGE);
if (stage.equals(SNAPSHOT_STAGE)) {
return targetNormal.setPreReleaseVersion("SNAPSHOT");
} else if (!stage.equals(FINAL_STAGE)) {
throw new IllegalArgumentException(String.format("Stage \"%s\" is not one of: [snapshot, final]", stage));
} else if (!inventory.isClean()) {
throw new IllegalStateException("Cannot release a final version without a clean repo.");
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class ReckonerTest extends Specification {
)
expect:
reckonStage(inventory2, 'minor', 'final') == '1.3.0'
reckonSnapshot(inventory2, 'major', false) == '2.0.0'
reckonSnapshot(inventory2, 'major', 'final') == '2.0.0'
}

def 'if current version is present and normal, repo is clean, not allowed to release an incremented pre-release stage'() {
Expand Down Expand Up @@ -202,7 +202,7 @@ class ReckonerTest extends Specification {
[Version.valueOf('1.2.2'), Version.valueOf('1.2.3')] as Set
)
when:
reckonSnapshot(inventory2, null, true)
reckonSnapshot(inventory2, null, 'snapshot')
then:
thrown(IllegalStateException)
}
Expand All @@ -229,9 +229,9 @@ class ReckonerTest extends Specification {
return Reckoner.reckon({ -> inventory }, normal, preRelease)
}

private String reckonSnapshot(inventory, scope, snapshot) {
private String reckonSnapshot(inventory, scope, stage) {
ScopeNormalStrategy normal = new ScopeNormalStrategy({ i -> Optional.ofNullable(scope) })
SnapshotPreReleaseStrategy preRelease = new SnapshotPreReleaseStrategy({ i, v -> Optional.ofNullable(snapshot) })
SnapshotPreReleaseStrategy preRelease = new SnapshotPreReleaseStrategy({ i, v -> Optional.ofNullable(stage) })
return Reckoner.reckon({ -> inventory }, normal, preRelease)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,25 @@ import org.ajoberstar.reckon.core.VcsInventory
import spock.lang.Specification

class SnapshotPreReleaseStrategyTest extends Specification {
def 'if snapshot is false, return the target normal'() {
def 'if stage supplier returns an invalid stage, throw'() {
given:
def inventory = new VcsInventory(
'abcdef',
true,
null,
Version.valueOf('1.2.3-milestone.1'),
Version.valueOf('1.2.2'),
1,
[] as Set,
[] as Set
)
when:
strategy('not').reckonTargetVersion(inventory, Version.valueOf('2.0.0'))
then:
thrown(IllegalArgumentException)
}

def 'if stage is final, return the target normal'() {
given:
def inventory = new VcsInventory(
'abcdef',
Expand All @@ -33,10 +51,10 @@ class SnapshotPreReleaseStrategyTest extends Specification {
[] as Set
)
expect:
strategy(false).reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0'
strategy('final').reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0'
}

def 'if snapshot is true, set pre-release to snapshot'() {
def 'if stage is snapshot or null, set pre-release to snapshot'() {
given:
def inventory = new VcsInventory(
'abcdef',
Expand All @@ -49,11 +67,11 @@ class SnapshotPreReleaseStrategyTest extends Specification {
[] as Set
)
expect:
strategy(true).reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-SNAPSHOT'
strategy('snapshot').reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-SNAPSHOT'
strategy(null).reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-SNAPSHOT'
}

def 'if repo has uncommitted changes, fail if snapshot is false'() {
def 'if repo has uncommitted changes, fail if stage is final'() {
given:
def inventory = new VcsInventory(
'abcdef',
Expand All @@ -66,12 +84,12 @@ class SnapshotPreReleaseStrategyTest extends Specification {
[] as Set
)
when:
strategy(false).reckonTargetVersion(inventory, Version.valueOf('2.0.0'))
strategy('final').reckonTargetVersion(inventory, Version.valueOf('2.0.0'))
then:
thrown(IllegalStateException)
}

private SnapshotPreReleaseStrategy strategy(snapshot) {
return new SnapshotPreReleaseStrategy({ i, v -> Optional.ofNullable(snapshot) })
private SnapshotPreReleaseStrategy strategy(stage) {
return new SnapshotPreReleaseStrategy({ i, v -> Optional.ofNullable(stage) })
}
}
1 change: 1 addition & 0 deletions reckon-gradle/.stutter/java10.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
4.5
4.5.1
4.6
4.7-rc-2
2 changes: 1 addition & 1 deletion reckon-gradle/.stutter/java8.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
4.5
4.5.1
4.6
4.7-rc-1
4.7-rc-2
2 changes: 1 addition & 1 deletion reckon-gradle/.stutter/java9.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
4.5
4.5.1
4.6
4.7-rc-1
4.7-rc-2
Loading

0 comments on commit fbd0b2f

Please sign in to comment.