14 Commits
0.2.0 ... dev

Author SHA1 Message Date
016f0d6c3f feat: Some parameter renaming for consistency
All checks were successful
CI / Ubuntu (push) Successful in 2m36s
2024-12-31 08:31:41 -05:00
147f6df1b3 feat: Test updates
All checks were successful
CI / Ubuntu (push) Successful in 2m37s
2024-12-31 08:04:04 -05:00
5c22250a63 feat: Adds documentation for precedence.
All checks were successful
CI / Ubuntu (push) Successful in 2m29s
2024-12-29 00:01:18 -05:00
9dd30a1745 feat: Integrates precedence with command-line options, needs documentation.
All checks were successful
CI / Ubuntu (push) Successful in 2m31s
2024-12-28 23:43:56 -05:00
f1eb883b93 feat: Integrates a precedence configuration setting, needs a command-line option.
All checks were successful
CI / Ubuntu (push) Successful in 2m47s
2024-12-28 22:15:24 -05:00
9631c62ee3 feat: Updates internal version container used to derive next version in cli-client. 2024-12-28 17:05:33 -05:00
6fe459c39e feat: Prep to update current version container in cli-client. 2024-12-28 10:09:34 -05:00
b9cf913528 feat: Begin cleaning up cli-client for better separation for current and next versions.
All checks were successful
CI / Ubuntu (push) Successful in 2m33s
2024-12-28 00:12:49 -05:00
f9710e5992 feat: Pre-release prefix now works with git-tag straegy. Working on todo items.
All checks were successful
CI / Ubuntu (push) Successful in 2m12s
2024-12-27 17:34:25 -05:00
a26d8695f5 fix: Fixes github ci after removing Makefile
All checks were successful
CI / Ubuntu (push) Successful in 2m20s
2024-12-27 16:51:01 -05:00
20f430fb8f feat: Renames some options, adds a require-configuration option that fails if configuration is not found.
All checks were successful
CI / Ubuntu (push) Successful in 2m19s
2024-12-27 16:42:48 -05:00
4420bd428a feat: Working on command-line documentation.
Some checks failed
CI / Ubuntu (push) Has been cancelled
2024-12-27 14:51:55 -05:00
8d73287a60 feat: Renames update-version plugin to bump-version, removes generate plugin as the bump-version can be called with the generate arguments.
All checks were successful
CI / Ubuntu (push) Successful in 2m42s
2024-12-26 16:07:57 -05:00
5bac7aa577 feat: Adds release file for a reminder of whats needed, bumps our current version.
All checks were successful
CI / Ubuntu (push) Successful in 2m15s
2024-12-26 14:51:51 -05:00
51 changed files with 1511 additions and 724 deletions

View File

@@ -1,11 +1,17 @@
{ {
"target" : {
"module" : { "name" : "bump-version" }
},
"strategy" : { "strategy" : {
"semvar" : { "semvar" : {
"preRelease" : { "strategy": { "gitTag" : {} } }, "allowPreRelease" : true,
"strategy" : { "gitTag": { "exactMatch": false } } "strategy" : {
"gitTag" : {
"exactMatch" : false
}
}
}
},
"target" : {
"module" : {
"name" : "BumpVersion"
} }
} }
} }

27
.bump-version.pre.json Normal file
View File

@@ -0,0 +1,27 @@
{
"strategy" : {
"semvar" : {
"allowPreRelease" : true,
"preRelease" : {
"prefix" : "rc",
"strategy" : {
"gitTag" : {
}
}
},
"requireExistingFile" : false,
"requireExistingSemVar" : false,
"strategy" : {
"gitTag" : {
"exactMatch" : false
}
}
}
},
"target" : {
"module" : {
"name" : "BumpVersion"
}
}
}

View File

@@ -14,12 +14,14 @@ jobs:
config: ['debug', 'release'] config: ['debug', 'release']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install just.
run: brew install just
- name: Select Xcode ${{ matrix.xcode }} - name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Swift Version - name: Swift Version
run: swift --version run: swift --version
- name: Run ${{ matrix.xcode }} Tests - name: Run ${{ matrix.xcode }} Tests
run: make CONFIG=${{ matrix.config }} test-library run: just test --configuration ${{ matrix.config }}
ubuntu: ubuntu:
name: Ubuntu name: Ubuntu

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
**/*.docc/*.md

View File

@@ -1,4 +1,4 @@
version: 1 version: 1
builder: builder:
configs: configs:
- documentation_targets: [CliVersion] - documentation_targets: [BumpVersion]

View File

@@ -1,42 +0,0 @@
PLATFORM_MACOS = macOS
CONFIG := debug
DOCC_TARGET ?= CliClient
DOCC_BASEPATH = $(shell basename "$(PWD)")
DOCC_DIR ?= ./docs
SWIFT_VERSION ?= "5.10"
clean:
rm -rf .build
build-documentation:
swift package \
--allow-writing-to-directory "$(DOCC_DIR)" \
generate-documentation \
--target "$(DOCC_TARGET)" \
--disable-indexing \
--transform-for-static-hosting \
--hosting-base-path "$(DOCC_BASEPATH)" \
--output-path "$(DOCC_DIR)"
preview-documentation:
swift package \
--disable-sandbox \
preview-documentation \
--target "$(DOCC_TARGET)"
test-linux:
docker run --rm \
--volume "$(PWD):$(PWD)" \
--workdir "$(PWD)" \
"swift:$(SWIFT_VERSION)" \
swift test
test-library:
swift test -c $(CONFIG)
update-version:
swift package \
--disable-sandbox \
--allow-writing-to-package-directory \
update-version \
git-version

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "3640b52c8069b868611efbfbd9b7545872526454802225747f7cd878062df1db", "originHash" : "9fe004cf869b34d1fe07e8b58a90b044281e8e94805df6723d4604ba5a6400d9",
"pins" : [ "pins" : [
{ {
"identity" : "combine-schedulers", "identity" : "combine-schedulers",

View File

@@ -8,15 +8,14 @@ let package = Package(
.macOS(.v13) .macOS(.v13)
], ],
products: [ products: [
.executable(name: "bump-version", targets: ["bump-version"]), .executable(name: "bump-version", targets: ["BumpVersion"]),
.library(name: "CliClient", targets: ["CliClient"]), .library(name: "CliClient", targets: ["CliClient"]),
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]), .library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
.library(name: "FileClient", targets: ["FileClient"]), .library(name: "FileClient", targets: ["FileClient"]),
.library(name: "GitClient", targets: ["GitClient"]), .library(name: "GitClient", targets: ["GitClient"]),
.library(name: "LoggingExtensions", targets: ["LoggingExtensions"]), .library(name: "LoggingExtensions", targets: ["LoggingExtensions"]),
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]), .plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]), .plugin(name: "BumpVersionPlugin", targets: ["BumpVersionPlugin"])
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"),
@@ -29,7 +28,7 @@ let package = Package(
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
name: "bump-version", name: "BumpVersion",
dependencies: [ dependencies: [
"CliClient", "CliClient",
.product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ArgumentParser", package: "swift-argument-parser"),
@@ -56,6 +55,7 @@ let package = Package(
name: "ConfigurationClient", name: "ConfigurationClient",
dependencies: [ dependencies: [
"FileClient", "FileClient",
"LoggingExtensions",
.product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies") .product(name: "DependenciesMacros", package: "swift-dependencies")
@@ -88,6 +88,7 @@ let package = Package(
.target( .target(
name: "LoggingExtensions", name: "LoggingExtensions",
dependencies: [ dependencies: [
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client") .product(name: "ShellClient", package: "swift-shell-client")
] ]
@@ -97,37 +98,22 @@ let package = Package(
name: "BuildWithVersionPlugin", name: "BuildWithVersionPlugin",
capability: .buildTool(), capability: .buildTool(),
dependencies: [ dependencies: [
"bump-version" "BumpVersion"
] ]
), ),
.plugin( .plugin(
name: "GenerateVersionPlugin", name: "BumpVersionPlugin",
capability: .command( capability: .command(
intent: .custom( intent: .custom(
verb: "generate-version", verb: "bump-version",
description: "Generates a version file in the given target." description: "Bumps a version file in the given target."
),
permissions: [
.writeToPackageDirectory(reason: "Generate a version file in the target's directory.")
]
),
dependencies: [
"bump-version"
]
),
.plugin(
name: "UpdateVersionPlugin",
capability: .command(
intent: .custom(
verb: "update-version",
description: "Updates a version file in the given target."
), ),
permissions: [ permissions: [
.writeToPackageDirectory(reason: "Update a version file in the target's directory.") .writeToPackageDirectory(reason: "Update a version file in the target's directory.")
] ]
), ),
dependencies: [ dependencies: [
"bump-version" "BumpVersion"
] ]
) )
] ]

View File

@@ -13,7 +13,7 @@ struct GenerateVersionBuildPlugin: BuildToolPlugin {
.deletingLastPathComponent() .deletingLastPathComponent()
.deletingLastPathComponent() .deletingLastPathComponent()
let tool = try context.tool(named: "cli-version") let tool = try context.tool(named: "bump-version")
let outputPath = context.pluginWorkDirectoryURL let outputPath = context.pluginWorkDirectoryURL
let outputFile = outputPath.appending(path: "Version.swift") let outputFile = outputPath.appending(path: "Version.swift")
@@ -25,7 +25,7 @@ struct GenerateVersionBuildPlugin: BuildToolPlugin {
arguments: [ arguments: [
"build", "--verbose", "build", "--verbose",
"--git-directory", gitDirectoryPath.absoluteString, "--git-directory", gitDirectoryPath.absoluteString,
"--target", outputPath.absoluteString "--target-file-path", outputPath.absoluteString
], ],
environment: [:], environment: [:],
inputFiles: target.sourceFiles.map(\.url), inputFiles: target.sourceFiles.map(\.url),

View File

@@ -0,0 +1,24 @@
import Foundation
import PackagePlugin
@main
struct BumpVersionPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
print("Starting bump-version plugin")
let tool = try context.tool(named: "bump-version")
print("arguments: \(arguments)")
let process = Process()
process.executableURL = tool.url
process.arguments = arguments
try process.run()
process.waitUntilExit()
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
Diagnostics.error("Reason: \(process.terminationReason), status: \(process.terminationStatus)")
return
}
}
}

View File

@@ -1,29 +0,0 @@
import Foundation
import PackagePlugin
@main
struct GenerateVersionPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
let gitVersion = try context.tool(named: "cli-version")
let arguments = ["generate"] + arguments
for target in context.package.targets {
guard let target = target as? SourceModuleTarget,
arguments.first(where: { $0.contains(target.name) }) != nil
else { continue }
let process = Process()
process.executableURL = gitVersion.url
process.arguments = arguments
try process.run()
process.waitUntilExit()
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
Diagnostics.error("Reason: \(process.terminationReason), status: \(process.terminationStatus)")
return
}
}
}
}

View File

@@ -1,29 +0,0 @@
import Foundation
import PackagePlugin
@main
struct UpdateVersionPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
let gitVersion = try context.tool(named: "cli-version")
let arguments = ["update"] + arguments
for target in context.package.targets {
guard let target = target as? SourceModuleTarget,
arguments.first(where: { $0.contains(target.name) }) != nil
else { continue }
let process = Process()
process.executableURL = gitVersion.url
process.arguments = arguments
try process.run()
process.waitUntilExit()
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
Diagnostics.error("Reason: \(process.terminationReason), status: \(process.terminationStatus)")
return
}
}
}
}

26
Release.md Normal file
View File

@@ -0,0 +1,26 @@
# Release Workflow Steps
This is a reminder of the steps used to create a release and update the homebrew formula.
> Note: These steps apply to the version hosted on `gitea`, on `github` more of these steps can be
> automated in `ci`, but there are no `macOS` host runners currently in `gitea`, so the bottles need
> built on `macOS`.
1. Update the version in `Sources/bump-version/Version.swift`.
1. Tag the commit with the next version tag.
1. Push the tagged commit, this will initiate the release being created.
1. Get the `sha` of the `*.tar.gz` in the release.
1. `just get-release-sha`
1. Update the homebrew formula url, sha256, and version at top of the homebrew formula.
1. `cd $(brew --repo michael/formula)`
1. Create a branch for a pull-request `git checkout -b bump-version`
1. `n Formula/bump-version.rb`
1. Build and generate a homebrew bottle.
1. `just bottle`
1. Update the `bottle do` section of homebrew formula with output during previous step.
1. Also make sure the `root_url` in the bottle section points to the new release.
1. Upload the bottle `*.tar.gz` file that was created to the release.
1. Generate a pull-request to the formula repo.
1. Generate a pull-request to this repo to merge into main.
1. Remove the bottle from current directory.
1. `just remove-bottles`

View File

@@ -14,13 +14,11 @@ struct BumpCommand: CommandRepresentable {
discussion: Discussion.default(examples: [ discussion: Discussion.default(examples: [
makeExample( makeExample(
label: "Basic usage, bump the minor version.", label: "Basic usage, bump the minor version.",
example: "--minor", example: "--minor"
includesAppName: false
), ),
makeExample( makeExample(
label: "Dry run, just show what the bumped version would be.", label: "Dry run, just show what the bumped version would be.",
example: "--minor --dry-run", example: "--minor --print"
includesAppName: false
) )
]) ])
) )

View File

@@ -60,6 +60,7 @@ extension ConfigCommand {
@OptionGroup var globals: ConfigCommandOptions @OptionGroup var globals: ConfigCommandOptions
func run() async throws { func run() async throws {
@Dependency(\.logger) var logger
let configuration = try await globals let configuration = try await globals
.shared(command: Self.commandName) .shared(command: Self.commandName)
.runClient(\.parsedConfiguration) .runClient(\.parsedConfiguration)
@@ -149,7 +150,6 @@ extension ConfigCommand {
} }
} }
// TODO: Add verbose.
@dynamicMemberLookup @dynamicMemberLookup
struct ConfigCommandOptions: ParsableArguments { struct ConfigCommandOptions: ParsableArguments {
@@ -184,7 +184,11 @@ private extension ConfigCommand.DumpConfig {
private extension ConfigCommand.ConfigCommandOptions { private extension ConfigCommand.ConfigCommandOptions {
func shared(command: String) throws -> CliClient.SharedOptions { func shared(command: String) throws -> CliClient.SharedOptions {
try configOptions.shared(command: command, extraOptions: extraOptions, verbose: verbose) try configOptions.shared(
command: command,
extraOptions: extraOptions,
verbose: verbose
)
} }
func handlePrintJson(_ configuration: Configuration) throws { func handlePrintJson(_ configuration: Configuration) throws {
@@ -209,12 +213,6 @@ private extension ConfigCommand.ConfigCommandOptions {
case .swift: case .swift:
customDump(configuration) customDump(configuration)
} }
// guard printJson else {
// customDump(configuration)
// return
// }
// try handlePrintJson(configuration)
} }
} }

View File

@@ -0,0 +1,103 @@
# Basic Configuration
Basic configuration examples.
## Overview
Generating a configuration file for your application is the easiest way to use the command-line
tool. The configuration specifies the location of the version file, either by a path to the file or
by the module that a `Version.swift` file resides in. It also declares the strategy for generating
new versions.
The command-line tool comes with a command to generate the configuration file for you, this should
be ran from the root of your project or by specifying the path to write the configuration file to
using the `-f | --configuration-file` option. The below examples assume that you're running in the
root project directory.
```bash
bump-version config generate --target-module my-tool
```
The above command produces the following in a file named `.bump-version.json` with the generated
default settings. This will generate semvar style version (example: `1.0.0`).
```json
{
"strategy": {
"semvar": {
"allowPreRelease": true,
"strategy": {
"gitTag": {
"exactMatch": false
}
}
}
},
"target": {
"module": {
"name": "my-tool"
}
}
}
```
> Note: The above does not add a pre-release strategy although it "allows" it if you pass an option
> to command later, if you set "allowPreRelease" to false it will ignore any attempts to add a
> pre-release strategy when bumping the version.
Most commands accept the same options for configuration as the above `config generate` command.
Those get merged with your project configuration when calling a command, that allows you to override
any of your defaults depending on your use case. You can also generate several configuration files
and specify them by passing the `-f | --configuration-file` to the command.
## Inspecting parsed configuration.
You can inspect the configuration that get's parsed by using the `config dump` command. The dump
command will print the parsed `json` to `stdout`, which can be helpful in confirming that your
configuration is valid and does not work unexpectedly.
```bash
bump-version config dump <options / overrides>
```
The dump command can also be used to generate a different configuration that is merged with your
default.
```bash
bump-version config dump --pre-release-git-tag-style > .bump-version.prerelease.json
```
Which would produce the following in `.bump-version.prerelease.json`
```json
{
"strategy": {
"semvar": {
"allowPreRelease": true,
"preRelease": {
"strategy": {
"gitTag": {}
}
},
"strategy": {
"gitTag": {
"exactMatch": false
}
}
}
},
"target": {
"module": {
"name": "my-tool"
}
}
}
```
You could then use this file when bumping your version.
```bash
bump-version bump -f .bump-version.prerelease.json
```
> See Also: <doc:ConfigurationReference>

View File

@@ -0,0 +1,115 @@
# Command Reference
Learn about the provided commands.
## Overview
The command-line tool provides the following commands.
> See Also:
>
> 1. <doc:BasicConfiguration>
> 1. <doc:ConfigurationReference>
All commands output the path of the file they generate or write to, to allow them to be piped into
other commands, however this will not work if you specify `--verbose` because then other output is
also written to `stdout`.
### Bump Command
This bumps the version to the next version based on the project configuration or passed in options.
This is the default command when calling the `bump-version` tool, so specifying the `bump` command
is not required, but will be shown in examples below for clarity.
> See Also: <doc:OptionsReference>
The following options are used to declare which part of a semvar to bump to the next version, they
are ignored if your configuration or options specify to use a `branch` strategy.
| Long | Description |
| ------------- | --------------------------------------- |
| --major | Bump the major portion of the semvar |
| --minor | Bump the minor portion of the semvar |
| --patch | Bump the patch portion of the semvar |
| --pre-release | Bump the pre-release suffix of a semvar |
#### Bump Command Usage Examples
```bash
bump-version bump --minor
```
If you want to use the default configuration without generating your own project configuration, then
you can specify the path or module to the bump command. The default configuration will use the
`gitTag` strategy without any pre-release strategy.
```bash
bump-version bump --minor --target-module my-tool
```
Show the output, but don't update the version file.
```bash
bump-version bump --major --print
```
### Generate Command
This generates a version file based on your configuration, setting it's initial value based on your
projects configuration strategy. This is generally only ran once after setting up a project.
```bash
bump-version generate
```
### Configuration Commands
The following commands are used to work with project configuration.
#### Generate Command
Generates a configuration file based on the passed in options.
> See Also: <doc:OptionsReference>
The following options are used to declare strategy used for deriving the version.
| Long | Description |
| -------- | --------------------------------- |
| --branch | Use the branch strategy |
| --semvar | Use the semvar strategy (default) |
##### Generate Configuration Example
```bash
bump-version config generate -m my-tool
```
The above generates a configuration file using the default version strategy for a target module
named 'my-tool'.
#### Dump Command
Dumps the parsed configuration to `stdout`.
> See Also: <doc:OptionsReference>
The following options are used to declare what output gets printed.
| Long | Description |
| ------- | -------------------- |
| --json | Print json (default) |
| --swift | Print swift struct |
##### Dump Configuration Example
```bash
bump-version config dump --disable-pre-release
```
This command can also be used to extend a configuration file with new configuration by sending the
output to a new file.
```bash
bump-version config dump --disable-pre-release > .bump-version.prod.json
```

View File

@@ -0,0 +1,226 @@
# Configuration Reference
Learn about the configuration.
## Target
The target declares where a version file lives that can be bumped by the command-line tool.
A target can be specified either as a path from the root of the project to a file that contains the
version or as a module that contains the version file.
> Note: A version file should not contain any other code aside from the version as the entire file
> contents get over written when bumping the version.
### Target - Path Example
```json
{
"target": {
"path": "Sources/my-tool/Version.swift"
}
}
```
### Target - Module Example
When using the module style a file name is not required if you use the default file name of
`Version.swift`, however it can be customized in your target specification.
```json
{
"target": {
"module": {
"fileName": "CustomVersion.swift",
"name": "my-tool"
}
}
}
```
The above will parse the path to the file as `Sources/my-tool/CustomVersion.swift`.
## Strategy
The strategy declares how to generate the next version of your project. There are currently two
strategies, `branch` and `semvar`, that we will discuss.
### Branch Strategy
This is the most basic strategy, which will derive the version via the git branch and optionally the
short version of the commit sha.
An example of this style may look like: `main-8d73287a60`.
```json
{
"strategy": {
"branch": {
"includeCommitSha": true
}
}
}
```
If you set `'"includeCommitSha" : false'` then only the branch name will be used.
### Semvar Strategy
This is the most common strategy to use. It has support for generating the next version using either
`gitTag` or a custom `command` strategy.
#### Git Tag Strategy
The `gitTag` strategy derives the next version using the output of `git describe --tags` command.
This requires a commit to have a semvar style tag in it's history, otherwise we will use `0.0.0` as
the tag until a commit is tagged.
```json
{
"strategy": {
"semvar": {
"allowPreRelease": true,
"strategy": {
"gitTag": {
"exactMatch": false
}
}
}
}
}
```
If you set `'"exactMatch": true'` then bumping the version will fail on commits that are not
specifically tagged.
#### Custom Command Strategy
The custom `command` strategy allows you to call an external command to derive the next version. The
external command should return something that can be parsed as a semvar.
```json
{
"strategy": {
"semvar": {
"allowPreRelease": true,
"strategy": {
"command": {
"arguments": ["my-command", "--some-option", "foo"]
}
}
}
}
}
```
> Note: All arguments to custom commands need to be a separate string in the arguments array
> otherwise they may not get passed appropriately to the command, so
> `"my-command --some-option foo"` will likely not work as expected.
#### Pre-Release
Semvar strategies can also include a pre-release strategy that adds a suffix to the semvar version
that can be used. In order for pre-release suffixes to be allowed the `'"allowPreRelease": true'`
must be set on the semvar strategy, you must also supply a pre-release strategy either when calling
the bump-version command or in your configuration.
Currently there are three pre-release strategies, `branch`, `gitTag`, and custom `command`, which we
will discuss.
A pre-release semvar example: `1.0.0-1-8d73287a60`
##### Branch
This will use the branch and optionally short version of the commit sha in order to derive the
pre-release suffix.
```json
{
"strategy": {
"semvar": {
"allowPreRelease": true,
"preRelease": {
"strategy": {
"branch": {
"includeCommitSha": true
}
}
},
...
}
}
}
```
This would produce something similar to: `1.0.0-main-8d73287a60`
##### Git Tag
This will use the full output of `git describe --tags` to include the pre-release suffix.
```json
{
"strategy" : {
"semvar" : {
"allowPreRelease" : true,
"preRelease" : {
"strategy" : {
"gitTag" : {}
}
},
...
}
}
}
```
This would produce something similar to: `1.0.0-10-8d73287a60`
##### Custom Command
This allows you to call an external command to generate the pre-release suffix. We will use whatever
the output is as the suffix.
```json
{
"strategy": {
"semvar": {
"allowPreRelease": true,
"preRelease": {
"strategy": {
"command": {
"arguments": ["my-command", "--some-option", "foo"],
"allowPrefix": true
}
}
}
}
}
}
```
> Note: All arguments to custom commands need to be a separate string in the arguments array
> otherwise they may not get passed appropriately to the command, so
> `"my-command --some-option foo"` will likely not work as expected.
##### Pre-Release Prefixes
All pre-release strategies can also accept a `prefix` that will appended prior to the generated
pre-release suffix. This can also be used without providing a pre-release strategy to only append
the `prefix` to the semvar.
```json
{
"strategy" : {
"semvar" : {
"allowPreRelease" : true,
"preRelease" : {
"prefix": "rc"
},
...
}
}
}
```
This would produce something similar to: `1.0.0-rc`

View File

@@ -0,0 +1,54 @@
# Options Reference
Common options used for the commands.
## Overview
The commands mostly all accept similar options, below is a list of those options and a description
of their usage.
### General Options
| Short | Long | Argument | Description |
| ----- | ------------------- | -------- | -------------------------------------------------------------------- |
| N/A | --print | N/A | Perform the command, but don't write any output files |
| N/A | --project-directory | <path> | The path to the root of your project, defaults to current directory |
| -h | --help | N/A | Show help for a command |
| -v | --verbose | N/A | Increase logging level, can be passed multiple times (example: -vvv) |
| N/A | --version | N/A | Show the version of the command line tool |
### Configuration Options
| Short | Long | Argument | Description |
| ----- | ---------------------------------- | ------------ | ----------------------------------------------------------------------------------- |
| -f | --configuration-file | <path> | The path to the configuration to use. |
| -m | --target-module | <name> | The target module name inside your project |
| -n | --target-file-name | <name> | The file name for the version to be found inside the module |
| -p | --target-file-path | <path> | Path to a version file in your project |
| N/A | --enable-git-tag/--disable-git-tag | N/A | Use the git-tag version strategy |
| N/A | --require-exact-match | N/A | Fail if a tag is not specifically set on the commit |
| N/A | --require-existing-semvar | N/A | Fail if an existing semvar is not found in the version file. |
| -c | --custom-command | <arguments> | Use a custom command strategy for the version (any options need to proceed a '--') |
| N/A | --commit-sha/--no-commit-sha | N/A | Use the commit sha with branch version or pre-release strategy |
| N/A | --require-configuration | N/A | Fail if a configuration file is not found |
| N/A | --precedence | <precedence> | The precedence for when a file exists (values: ['file', 'strategy'], default: file) |
> Note: Precedence is used as tie breaker if the version in the file does not agree with the version
> from the configured strategy. This can happen if a file was edited / bumped manually or the value
> returned from the external command is not similar to the version in the file. By default the file
> will take precedence over what is returned from the strategy.
#### Pre-Release Options
| Short | Long | Argument | Description |
| ----- | ---------------------------- | ----------- | ------------------------------------------------------------ |
| -d | --disable-pre-release | N/A | Disable pre-relase suffixes from being used |
| -b | --pre-release-branch-style | N/A | Use the branch and commit sha style for pre-release suffixes |
| N/A | --commit-sha/--no-commit-sha | N/A | Use the commit sha with branch pre-release strategy |
| -g | --pre-release-git-tag-style | N/A | Use the git tag style for pre-release suffixes |
| N/A | --pre-release-prefix | <prefix> | A prefix to use before a pre-release suffix |
| N/A | --custom-pre-release | <arguments> | Use custom command strategy for pre-release suffix |
> Note: When using one of the `--custom-*` options then any arguments passed will be used for
> arguments when calling your custom strategy, if the external tool you use requires options they
> must proceed a '--' otherwise you will get an error that an 'unexpected option' is being used.

View File

@@ -0,0 +1,77 @@
# Plugins Reference
Learn about using the provided package plugins.
## Overview
There are two provided plugins that can be used, this describes their usage.
### Build with Version
The `BuildWithVersion` plugin uses your project configuration to automatically generate a version
file when swift builds your project. You can use the plugin by declaring it as dependency in your
project.
```swift
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
platforms:[.macOS(.v13)],
dependencies: [
...,
.package(url: "https://github.com/m-housh/swift-bump-version.git", from: "0.2.0")
],
targets: [
.executableTarget(
name: "<target name>",
dependencies: [...],
plugins: [
.plugin(name: "BuildWithVersionPlugin", package: "swift-bump-version")
]
)
]
)
```
### Manual Plugin
There is also a `BumpVersionPlugin` that allows you to run the `bump-version` tool without
installing the command-line tool on your system, however it does make the usage much more verbose.
Include as dependency in your project.
```swift
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
platforms:[.macOS(.v13)],
dependencies: [
...,
.package(url: "https://github.com/m-housh/swift-bump-version.git", from: "0.2.0")
],
targets: [
...
]
)
```
Then you can use the manual plugin.
```
swift package \
--disable-sandbox \
--allow-writing-to-package-directory \
bump-version \
bump \
--minor
```
> Note: Anything after the `'bump-version'` in the above get's passed directly to the bump-version
> command-line tool, so you can use this to run any of the provided commands, the above shows
> bumping the minor semvar as a reference example.
>
> See Also: <doc:CommandReference>

View File

@@ -0,0 +1,62 @@
# ``BumpVersion``
@Metadata {
@DisplayName("bump-version")
@DocumentationExtension(mergeBehavior: override)
}
A command-line tool for managing swift application versions.
## Overview
This tool aims to provide a way to manage application versions in your build
pipeline. It can be used as a stand-alone command-line tool or by using one of
the provided swift package plugins.
## Installation
The command-line tool can be installed via homebrew.
```bash
brew tap m-housh/formula
brew install bump-version
```
## Package Plugins
Package plugins can be used in a swift package manager project.
```swift
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
platforms:[.macOS(.v13)],
dependencies: [
...,
.package(url: "https://github.com/m-housh/swift-bump-version.git", from: "0.2.0")
],
targets: [
.executableTarget(
name: "<target name>",
dependencies: [...],
plugins: [
.plugin(name: "BuildWithVersionPlugin", package: "swift-bump-version")
]
)
]
)
```
> See Also: <doc:PluginsReference>
## Topics
### Articles
- <doc:BasicConfiguration>
- <doc:ConfigurationReference>
- <doc:CommandReference>
- <doc:OptionsReference>
- <doc:PluginsReference>

View File

@@ -13,13 +13,13 @@ struct GlobalOptions: ParsableArguments {
var configOptions: ConfigurationOptions var configOptions: ConfigurationOptions
@Option( @Option(
name: .customLong("git-directory"), name: .customLong("project-directory"),
help: "The git directory for the version (default: current directory)" help: "The project directory. (default: current directory)"
) )
var gitDirectory: String? var projectDirectory: String?
@Flag( @Flag(
name: .customLong("dry-run"), name: .customLong("print"),
help: "Print's what would be written to a target version file." help: "Print's what would be written to a target version file."
) )
var dryRun: Bool = false var dryRun: Bool = false
@@ -41,6 +41,7 @@ struct GlobalOptions: ParsableArguments {
} }
struct ConfigurationOptions: ParsableArguments { struct ConfigurationOptions: ParsableArguments {
@Option( @Option(
name: [.customShort("f"), .long], name: [.customShort("f"), .long],
help: "Specify the path to a configuration file. (default: .bump-version.json)", help: "Specify the path to a configuration file. (default: .bump-version.json)",
@@ -61,6 +62,14 @@ struct ConfigurationOptions: ParsableArguments {
) )
var commitSha: Bool = true var commitSha: Bool = true
@Flag(
name: .long,
help: """
Require a configuration file, otherwise fail.
"""
)
var requireConfiguration: Bool = false
} }
struct TargetOptions: ParsableArguments { struct TargetOptions: ParsableArguments {
@@ -126,7 +135,6 @@ struct PreReleaseOptions: ParsableArguments {
} }
// TODO: Add custom command strategy.
struct SemVarOptions: ParsableArguments { struct SemVarOptions: ParsableArguments {
@Flag( @Flag(
@@ -165,5 +173,19 @@ struct SemVarOptions: ParsableArguments {
) )
var customCommand: Bool = false var customCommand: Bool = false
@Option(
name: .long,
help: """
Set the precence to prefer version from file or strategy.
"""
)
var precedence: Configuration.SemVar.Precedence?
@OptionGroup var preRelease: PreReleaseOptions @OptionGroup var preRelease: PreReleaseOptions
} }
extension Configuration.SemVar.Precedence: ExpressibleByArgument {
public init?(argument: String) {
self.init(rawValue: argument)
}
}

View File

@@ -64,7 +64,7 @@ extension GlobalOptions {
command: command, command: command,
dryRun: dryRun, dryRun: dryRun,
extraOptions: extraOptions, extraOptions: extraOptions,
gitDirectory: gitDirectory, gitDirectory: projectDirectory,
verbose: verbose verbose: verbose
) )
} }
@@ -129,7 +129,6 @@ extension SemVarOptions {
) throws -> Configuration.SemVar { ) throws -> Configuration.SemVar {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
// TODO: Update when / if there's an update config command.
if customCommand && preRelease.customPreRelease { if customCommand && preRelease.customPreRelease {
logger.warning(""" logger.warning("""
Custom pre-release can not be used at same time as custom command. Custom pre-release can not be used at same time as custom command.
@@ -137,8 +136,11 @@ extension SemVarOptions {
""") """)
} }
logger.trace("precedence: \(String(describing: precedence))")
return try .init( return try .init(
allowPreRelease: !preRelease.disablePreRelease, allowPreRelease: !preRelease.disablePreRelease,
precedence: precedence,
preRelease: customCommand ? nil : preRelease.configPreReleaseStrategy( preRelease: customCommand ? nil : preRelease.configPreReleaseStrategy(
includeCommitSha: includeCommitSha, includeCommitSha: includeCommitSha,
extraOptions: extraOptions extraOptions: extraOptions
@@ -167,6 +169,15 @@ extension ConfigurationOptions {
) )
} }
private func configurationToMerge(extraOptions: [String]) throws -> Configuration {
try .init(
target: target(),
strategy: semvarOptions.gitTag
? .semvar(semvarOptions(extraOptions: extraOptions))
: .branch(includeCommitSha: commitSha)
)
}
func shared( func shared(
command: String, command: String,
dryRun: Bool = true, dryRun: Bool = true,
@@ -177,12 +188,11 @@ extension ConfigurationOptions {
try .init( try .init(
allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease, allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease,
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, projectDirectory: gitDirectory,
loggingOptions: .init(command: command, verbose: verbose), loggingOptions: .init(command: command, verbose: verbose),
target: target(), configurationToMerge: configurationToMerge(extraOptions: extraOptions),
branch: semvarOptions.gitTag ? nil : .init(includeCommitSha: commitSha), configurationFile: configurationFile,
semvar: semvarOptions(extraOptions: extraOptions), requireConfigurationFile: requireConfiguration
configurationFile: configurationFile
) )
} }
} }

View File

@@ -1,2 +1,2 @@
// Do not set this variable, it is set during the build process. // Do not set this variable, it is set during the build process.
let VERSION: String? = "0.1.1" let VERSION: String? = "0.2.0-rc-5-ga26d869"

View File

@@ -9,6 +9,7 @@ import ShellClient
public extension DependencyValues { public extension DependencyValues {
/// The cli-client that runs the command line tool commands.
var cliClient: CliClient { var cliClient: CliClient {
get { self[CliClient.self] } get { self[CliClient.self] }
set { self[CliClient.self] = newValue } set { self[CliClient.self] = newValue }
@@ -23,7 +24,7 @@ public struct CliClient: Sendable {
public var build: @Sendable (SharedOptions) async throws -> String public var build: @Sendable (SharedOptions) async throws -> String
/// Bump the existing version. /// Bump the existing version.
public var bump: @Sendable (BumpOption?, SharedOptions) async throws -> String public var bump: @Sendable (BumpOption, SharedOptions) async throws -> String
/// Generate a version file with an optional version that can be set manually. /// Generate a version file with an optional version that can be set manually.
public var generate: @Sendable (SharedOptions) async throws -> String public var generate: @Sendable (SharedOptions) async throws -> String
@@ -35,41 +36,53 @@ public struct CliClient: Sendable {
case major, minor, patch, preRelease case major, minor, patch, preRelease
} }
/// Represents options that are used by all the commands.
public struct SharedOptions: Equatable, Sendable { public struct SharedOptions: Equatable, Sendable {
/// Whether to allow pre-release suffixes.
let allowPreReleaseTag: Bool let allowPreReleaseTag: Bool
/// Flag on if we write to files or not.
let dryRun: Bool let dryRun: Bool
let gitDirectory: String?
/// Specify a path to the project directory.
let projectDirectory: String?
/// The logging options to use.
let loggingOptions: LoggingOptions let loggingOptions: LoggingOptions
let target: Configuration.Target?
let branch: Configuration.Branch? /// Configuration that gets merged with the loaded (or default) configuration.
let semvar: Configuration.SemVar? let configurationToMerge: Configuration?
/// Path to the configuration file to load.
let configurationFile: String? let configurationFile: String?
/// Fail if a configuration file is not found.
let requireConfigurationFile: Bool
public init( public init(
allowPreReleaseTag: Bool = true, allowPreReleaseTag: Bool = true,
dryRun: Bool = false, dryRun: Bool = false,
gitDirectory: String? = nil, projectDirectory: String? = nil,
loggingOptions: LoggingOptions, loggingOptions: LoggingOptions,
target: Configuration.Target? = nil, configurationToMerge: Configuration? = nil,
branch: Configuration.Branch? = nil, configurationFile: String? = nil,
semvar: Configuration.SemVar? = nil, requireConfigurationFile: Bool = false
configurationFile: String? = nil
) { ) {
self.allowPreReleaseTag = allowPreReleaseTag self.allowPreReleaseTag = allowPreReleaseTag
self.dryRun = dryRun self.dryRun = dryRun
self.gitDirectory = gitDirectory self.projectDirectory = projectDirectory
self.loggingOptions = loggingOptions self.loggingOptions = loggingOptions
self.target = target
self.branch = branch
self.semvar = semvar
self.configurationFile = configurationFile self.configurationFile = configurationFile
self.configurationToMerge = configurationToMerge
self.requireConfigurationFile = requireConfigurationFile
} }
} }
} }
extension CliClient: DependencyKey { extension CliClient: DependencyKey {
public static let testValue: CliClient = Self() public static let testValue: CliClient = Self()
public static func live(environment: [String: String]) -> Self { public static func live(environment: [String: String]) -> Self {

View File

@@ -1,8 +1,12 @@
import ConfigurationClient
enum CliClientError: Error { enum CliClientError: Error {
case gitDirectoryNotFound case gitDirectoryNotFound
case fileExists(path: String) case fileExists(path: String)
case fileDoesNotExist(path: String) case fileDoesNotExist(path: String)
case failedToParseVersionFile case failedToParseVersionFile
case semVarNotFound case semVarNotFound(message: String)
case strategyNotFound(configuration: Configuration)
case preReleaseParsingError(String) case preReleaseParsingError(String)
case versionStringNotFound
} }

View File

@@ -1,12 +1,12 @@
# Manual Plugins # Manual Plugins
There are two plugins that are included that can be ran manually, if the build tool plugin does not fit There are two plugins that are included that can be ran manually, if the build tool plugin does not
your use case. fit your use case.
## Generate Version ## Generate Version
The `generate-version` plugin will create a `Version.swift` file in the given target. You can The `generate-version` plugin will create a `Version.swift` file in the given target. You can run it
run it by running the following command. by running the following command.
```bash ```bash
swift package --disable-sandbox \ swift package --disable-sandbox \
@@ -32,21 +32,22 @@ swift package --disable-sandbox \
## Options ## Options
Both manual versions also allow the following options to customize the operation, the Both manual versions also allow the following options to customize the operation, the options need
options need to come after the plugin name. to come after the plugin name.
| Option | Description | | Option | Description |
| ------ | ----------- | | ---------- | --------------------------------------------------------------------- |
| --dry-run | Do not write to any files, but describe where values would be written | | --print | Do not write to any files, but describe where values would be written |
| --filename | Override the file name to be written in the target directory | | --filename | Override the file name to be written in the target directory |
| --verbose | Increase the logging output | | --verbose | Increase the logging output |
### Example with options ### Example with options
```bash ```bash
swift package \ swift package \
--allow-writing-to-package-directory \ --allow-writing-to-package-directory \
generate-version \ generate-version \
--dry-run \ --print \
--verbose \ --verbose \
<target> <target>
``` ```

View File

@@ -1,4 +1,4 @@
# CliClient # ``CliClient``
Derive a version for a command-line tool from git tags or a git sha. Derive a version for a command-line tool from git tags or a git sha.
@@ -9,8 +9,8 @@ Derive a version for a command-line tool from git tags or a git sha.
## Overview ## Overview
This tool exposes several plugins that can be used to derive a version for a command line program at This tool exposes several plugins that can be used to derive a version for a command line program at
build time or by manually running the plugin. The version is derived from git tags and falling back build time or by manually running the plugin. The version is generally derived from git tags,
to the branch and git sha if a tag is not set for the current worktree state. however it can be configured to run custom commands.
## Articles ## Articles

View File

@@ -4,36 +4,37 @@ import Dependencies
import FileClient import FileClient
import Foundation import Foundation
import GitClient import GitClient
import LoggingExtensions
@_spi(Internal) extension CliClient.SharedOptions {
public extension CliClient.SharedOptions {
/// All cli-client calls should run through this, it set's up logging, /// All cli-client calls should run through this, it set's up logging,
/// loads configuration, and generates the current version based on the /// loads configuration, and generates the current version based on the
/// configuration. /// configuration.
@discardableResult @discardableResult
func run( func run(
_ operation: (CurrentVersionContainer) async throws -> Void _ operation: (VersionContainer) async throws -> Void
) async rethrows -> String { ) async rethrows -> String {
try await loggingOptions.withLogger { try await loggingOptions.withLogger {
// Load the default configuration, if it exists. // Load the default configuration, if it exists.
try await withMergedConfiguration { configuration in try await withMergedConfiguration { configuration in
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
var configurationString = "" guard let strategy = configuration.strategy else {
customDump(configuration, to: &configurationString) throw CliClientError.strategyNotFound(configuration: configuration)
logger.trace("\nConfiguration: \(configurationString)") }
logger.dump(configuration, level: .trace) {
"\nConfiguration: \($0)"
}
// This will fail if the target url is not set properly. // This will fail if the target url is not set properly.
let targetUrl = try configuration.targetUrl(gitDirectory: gitDirectory) let targetUrl = try configuration.targetUrl(projectDirectory: projectDirectory)
logger.debug("Target: \(targetUrl.cleanFilePath)") logger.debug("Target: \(targetUrl.cleanFilePath)")
// Perform the operation, which generates the new version and writes it. // Perform the operation, which generates the new version and writes it.
try await operation( try await operation(
configuration.currentVersion( .load(projectDirectory: projectDirectory, strategy: strategy, url: targetUrl)
targetUrl: targetUrl,
gitDirectory: gitDirectory
)
) )
// Return the file path we wrote the version to. // Return the file path we wrote the version to.
@@ -50,27 +51,19 @@ public extension CliClient.SharedOptions {
@Dependency(\.configurationClient) var configurationClient @Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
var strategy: Configuration.VersionStrategy? if configurationToMerge?.strategy?.branch != nil {
if let branch {
logger.trace("Merging branch strategy.") logger.trace("Merging branch strategy.")
strategy = .branch(branch) // strategy = .branch(branch)
} else if let semvar { } else if let semvar = configurationToMerge?.strategy?.semvar {
logger.trace("Merging semvar strategy.") logger.dump(semvar, level: .trace) {
var semvarString = "" "Merging semvar strategy:\n\($0)"
customDump(semvar, to: &semvarString) }
logger.trace("\(semvarString)")
strategy = .semvar(semvar)
} }
let configuration = Configuration(
target: target,
strategy: strategy
)
return try await configurationClient.withConfiguration( return try await configurationClient.withConfiguration(
path: configurationFile, path: configurationFile,
merging: configuration, merging: configurationToMerge,
strict: requireConfigurationFile,
operation: operation operation: operation
) )
} }
@@ -82,48 +75,52 @@ public extension CliClient.SharedOptions {
try await fileClient.write(string: string, to: url) try await fileClient.write(string: string, to: url)
} else { } else {
logger.debug("Skipping, due to dry-run being passed.") logger.debug("Skipping, due to dry-run being passed.")
logger.info("\n\(string)\n")
} }
} }
func write(_ currentVersion: CurrentVersionContainer) async throws { func write(_ currentVersion: VersionContainer) async throws {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
logger.trace("Begin writing version.")
let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag) // let hasChanges: Bool
logger.debug("Version: \(version)") let targetUrl: URL
let usesOptionalType: Bool
let versionString: String?
let template = currentVersion.usesOptionalType ? Template.optional(version) : Template.nonOptional(version) switch currentVersion {
case let .branch(branch):
// hasChanges = branch.hasChanges
targetUrl = branch.targetUrl
usesOptionalType = branch.usesOptionalType
versionString = branch.versionString
case let .semvar(semvar):
// hasChanges = semvar.hasChanges
targetUrl = semvar.targetUrl
usesOptionalType = semvar.usesOptionalType
versionString = semvar.versionString(withPreRelease: allowPreReleaseTag)
}
// if !hasChanges {
// logger.debug("No changes from loaded version, not writing next version.")
// return
// }
guard let versionString else {
throw CliClientError.versionStringNotFound
}
// let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag)
if !dryRun {
logger.debug("Version: \(versionString)")
} else {
logger.info("Version: \(versionString)")
}
let template = usesOptionalType ? Template.optional(versionString) : Template.nonOptional(versionString)
logger.trace("Template string: \(template)") logger.trace("Template string: \(template)")
try await write(template, to: currentVersion.targetUrl) try await write(template, to: targetUrl)
}
}
@_spi(Internal)
public struct CurrentVersionContainer: Sendable {
let targetUrl: URL
let version: Version
var usesOptionalType: Bool {
switch version {
case .string: return false
case let .semvar(_, usesOptionalType, _): return usesOptionalType
}
}
public enum Version: Sendable {
case string(String)
case semvar(SemVar, usesOptionalType: Bool = true, hasChanges: Bool)
func string(allowPreReleaseTag: Bool) throws -> String {
switch self {
case let .string(string):
return string
case let .semvar(semvar, usesOptionalType: _, hasChanges: _):
return semvar.versionString(withPreReleaseTag: allowPreReleaseTag)
}
}
} }
} }
@@ -135,33 +132,35 @@ extension CliClient.SharedOptions {
} }
} }
func bump(_ type: CliClient.BumpOption?) async throws -> String { func bump(_ type: CliClient.BumpOption) async throws -> String {
guard let type else {
return try await generate()
}
return try await run { container in return try await run { container in
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
switch container.version { switch container {
case .string: // When we did not parse a semvar, just write whatever we parsed for the current version. case .branch: // When we did not parse a semvar, just write whatever we parsed for the current version.
logger.debug("Failed to parse semvar, but got current version string.") logger.debug("Failed to parse semvar, but got current version string.")
try await write(container) try await write(container)
case let .semvar(semvar, usesOptionalType: usesOptionalType, hasChanges: hasChanges): case let .semvar(semvar):
logger.debug("Semvar prior to bumping: \(semvar)")
let bumped = semvar.bump(type)
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
guard bumped != semvar || hasChanges else { let version: SemVar?
logger.debug("No change, skipping.")
return switch semvar.precedence ?? .default {
case .file:
version = semvar.loadedVersion ?? semvar.strategyVersion
case .strategy:
version = semvar.strategyVersion ?? semvar.loadedVersion
} }
logger.debug("Bumped version: \(version)") // let version = semvar.loadedVersion ?? semvar.nextVersion
let template = usesOptionalType ? Template.optional(version) : Template.build(version) guard let version else {
try await write(template, to: container.targetUrl) throw CliClientError.semVarNotFound(message: "Failed to parse a valid semvar to bump.")
}
logger.dump(version, level: .debug) { "Version prior to bumping:\n\($0)" }
let bumped = version.bump(type)
logger.dump(bumped, level: .trace) { "Bumped version:\n\($0)" }
try await write(.semvar(semvar.withUpdateNextVersion(bumped)))
} }
} }
} }
@@ -172,3 +171,15 @@ extension CliClient.SharedOptions {
} }
} }
} }
private extension CurrentVersionContainer where Version == SemVar {
func withUpdateNextVersion(_ next: SemVar) -> Self {
.init(
targetUrl: targetUrl,
usesOptionalType: usesOptionalType,
loadedVersion: loadedVersion,
precedence: .strategy, // make sure to use the next version, since it was specified, as this is called from `bump`.
strategyVersion: next
)
}
}

View File

@@ -0,0 +1,57 @@
import ConfigurationClient
import CustomDump
import Dependencies
import Foundation
import GitClient
import ShellClient
extension Configuration {
func targetUrl(projectDirectory: String?) throws -> URL {
guard let target else {
throw ConfigurationParsingError.targetNotFound
}
return try target.url(projectDirectory: projectDirectory)
}
}
private extension Configuration.Target {
func url(projectDirectory: String?) throws -> URL {
@Dependency(\.logger) var logger
let filePath: String
if let path {
filePath = path
} else {
guard let module else {
throw ConfigurationParsingError.pathOrModuleNotSet
}
var path = module.name
logger.debug("module.name: \(path)")
if path.hasPrefix("./") {
path = String(path.dropFirst(2))
}
if !path.hasPrefix("Sources") {
logger.debug("no prefix")
path = "Sources/\(path)"
}
filePath = "\(path)/\(module.fileNameOrDefault)"
}
if let projectDirectory {
return URL(filePath: "\(projectDirectory)/\(filePath)")
}
return URL(filePath: filePath)
}
}
enum ConfigurationParsingError: Error {
case targetNotFound
case pathOrModuleNotSet
case versionStrategyError(message: String)
case versionStrategyNotFound
}

View File

@@ -1,248 +0,0 @@
import ConfigurationClient
import Dependencies
import Foundation
import GitClient
import ShellClient
extension Configuration {
func targetUrl(gitDirectory: String?) throws -> URL {
guard let target else {
throw ConfigurationParsingError.targetNotFound
}
return try target.url(gitDirectory: gitDirectory)
}
func currentVersion(targetUrl: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
guard let strategy else {
throw ConfigurationParsingError.versionNotFound
}
return try await strategy.currentVersion(
targetUrl: targetUrl,
gitDirectory: gitDirectory
)
}
}
extension Configuration.Target {
func url(gitDirectory: String?) throws -> URL {
@Dependency(\.logger) var logger
let filePath: String
if let path {
filePath = path
} else {
guard let module else {
throw ConfigurationParsingError.pathOrModuleNotSet
}
var path = module.name
logger.debug("module.name: \(path)")
if path.hasPrefix("./") {
path = String(path.dropFirst(2))
}
if !path.hasPrefix("Sources") {
logger.debug("no prefix")
path = "Sources/\(path)"
}
filePath = "\(path)/\(module.fileNameOrDefault)"
}
if let gitDirectory {
return URL(filePath: "\(gitDirectory)/\(filePath)")
}
return URL(filePath: filePath)
}
}
extension GitClient {
func version(includeCommitSha: Bool, gitDirectory: String?) async throws -> String {
@Dependency(\.gitClient) var gitClient
return try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .branch(commitSha: includeCommitSha)
)).description
}
}
extension Configuration.PreRelease {
// FIX: This needs to handle the pre-release type appropriatly.
func preReleaseString(gitDirectory: String?) async throws -> PreReleaseString? {
guard let strategy else { return nil }
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
var preReleaseString: String
var suffix = true
var allowsPrefix = true
switch strategy {
case let .branch(includeCommitSha: includeCommitSha):
logger.trace("Branch pre-release strategy, includeCommitSha: \(includeCommitSha).")
preReleaseString = try await gitClient.version(
includeCommitSha: includeCommitSha,
gitDirectory: gitDirectory
)
case let .command(arguments: arguments):
logger.trace("Command pre-release strategy, arguments: \(arguments).")
// TODO: What to do with allows prefix? Need a configuration setting for commands.
preReleaseString = try await asyncShellClient.background(.init(arguments))
case .gitTag:
logger.trace("Git tag pre-release strategy.")
logger.trace("This will ignore any set prefix.")
suffix = false
allowsPrefix = false
preReleaseString = try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .tag(exactMatch: false)
)).description
}
if let prefix {
if allowsPrefix {
preReleaseString = "\(prefix)-\(preReleaseString)"
} else {
logger.warning("Found prefix, but pre-release strategy may not work properly, ignoring prefix.")
}
}
guard suffix else { return .semvar(preReleaseString) }
return .suffix(preReleaseString)
// return preReleaseString
}
enum PreReleaseString: Sendable {
case suffix(String)
case semvar(String)
}
}
@_spi(Internal)
public extension Configuration.SemVar {
private func applyingPreRelease(_ semvar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
@Dependency(\.logger) var logger
logger.trace("Start apply pre-release to: \(semvar)")
guard let preReleaseStrategy = self.preRelease,
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
else {
logger.trace("No pre-release strategy, returning original semvar.")
return semvar
}
// let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
logger.trace("Pre-release string: \(preRelease)")
switch preRelease {
case let .suffix(string):
return semvar.applyingPreRelease(string)
case let .semvar(string):
guard let semvar = SemVar(string: string) else {
throw CliClientError.preReleaseParsingError(string)
}
return semvar
}
// return semVar.applyingPreRelease(preRelease)
}
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
@Dependency(\.fileClient) var fileClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
let fileOutput = try? await fileClient.semvar(file: file, gitDirectory: gitDirectory)
var semVar = fileOutput?.semVar
logger.trace("file output semvar: \(String(describing: semVar))")
let usesOptionalType = fileOutput?.usesOptionalType
// We parsed a semvar from the existing file, use it.
if semVar != nil {
let semvarWithPreRelease = try await applyingPreRelease(semVar!, gitDirectory)
return .semvar(
semvarWithPreRelease,
usesOptionalType: usesOptionalType ?? false,
hasChanges: semvarWithPreRelease != semVar
)
}
if requireExistingFile == true {
logger.debug("Failed to parse existing file, and caller requires it.")
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
}
logger.trace("Does not require existing file, checking git-tag.")
// Didn't have existing semVar loaded from file, so check for git-tag.
semVar = try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .tag(exactMatch: false)
)).semVar
if semVar != nil {
let semvarWithPreRelease = try await applyingPreRelease(semVar!, gitDirectory)
return .semvar(
semvarWithPreRelease,
usesOptionalType: usesOptionalType ?? false,
hasChanges: semvarWithPreRelease != semVar
)
}
if requireExistingSemVar == true {
logger.trace("Caller requires existing semvar and it was not found in file or git-tag.")
throw CliClientError.semVarNotFound
}
// Semvar doesn't exist, so create a new one.
logger.trace("Generating new semvar.")
return try await .semvar(
applyingPreRelease(.init(), gitDirectory),
usesOptionalType: usesOptionalType ?? false,
hasChanges: true
)
}
}
extension Configuration.VersionStrategy {
func currentVersion(targetUrl: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
@Dependency(\.gitClient) var gitClient
guard let branch else {
guard let semvar else {
throw ConfigurationParsingError.versionStrategyError(
message: "Neither branch nor semvar set on configuration."
)
}
return try await .init(
targetUrl: targetUrl,
version: semvar.currentVersion(file: targetUrl, gitDirectory: gitDirectory)
)
}
return try await .init(
targetUrl: targetUrl,
version: .string(
gitClient.version(includeCommitSha: branch.includeCommitSha, gitDirectory: gitDirectory)
)
)
}
}
enum ConfigurationParsingError: Error {
case targetNotFound
case pathOrModuleNotSet
case versionStrategyError(message: String)
case versionNotFound
}

View File

@@ -1,3 +0,0 @@
enum Constants {
static let defaultFileName = "Version.swift"
}

View File

@@ -5,9 +5,40 @@ import GitClient
@_spi(Internal) @_spi(Internal)
public extension FileClient { public extension FileClient {
func branch(
file: URL,
projectDirectory: String?,
requireExistingFile: Bool
) async throws -> (string: String, usesOptionalType: Bool)? {
let loaded = try? await getVersionString(fileUrl: file, projectDirectory: projectDirectory)
guard let loaded else {
if requireExistingFile {
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
}
return nil
}
return (loaded.0, loaded.1)
}
func semvar(
file: URL,
projectDirectory: String?,
requireExistingFile: Bool
) async throws -> (semVar: SemVar?, usesOptionalType: Bool)? {
let loaded = try? await getVersionString(fileUrl: file, projectDirectory: projectDirectory)
guard let loaded else {
if requireExistingFile {
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
}
return nil
}
let semvar = SemVar(string: loaded.0)
return (semvar, loaded.1)
}
private func getVersionString( private func getVersionString(
fileUrl: URL, fileUrl: URL,
gitDirectory: String? projectDirectory: String?
) async throws -> (version: String, usesOptionalType: Bool) { ) async throws -> (version: String, usesOptionalType: Bool) {
@Dependency(\.gitClient) var gitClient @Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@@ -38,15 +69,4 @@ public extension FileClient {
return (String(versionString), isOptional) return (String(versionString), isOptional)
} }
func semvar(
file: URL,
gitDirectory: String?
) async throws -> (semVar: SemVar?, usesOptionalType: Bool) {
@Dependency(\.logger) var logger
let (string, usesOptionalType) = try await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
let semvar = SemVar(string: string)
logger.debug("Semvar: \(String(describing: semvar))")
return (semvar, usesOptionalType)
}
} }

View File

@@ -0,0 +1,90 @@
import ConfigurationClient
import Foundation
import GitClient
import LoggingExtensions
import ShellClient
extension SemVar {
static func nextVersion(
configuration: Configuration.SemVar,
projectDirectory: String?
) async throws -> Self? {
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
guard let strategy = configuration.strategy else { return nil }
let semvarString: String
switch strategy {
case let .gitTag(exactMatch: exactMatch):
logger.trace("Loading semvar gitTag strategy...")
semvarString = try await gitClient.version(.init(
gitDirectory: projectDirectory,
style: .tag(exactMatch: exactMatch ?? false)
)).description
case let .command(arguments: arguments):
logger.trace("Loading semvar custom command strategy: \(arguments)")
semvarString = try await asyncShellClient.background(.init(arguments))
}
var preReleaseString: String?
if let preRelease = configuration.preRelease,
configuration.allowPreRelease ?? true
{
preReleaseString = try await preRelease.get(projectDirectory: projectDirectory)
}
let semvar = SemVar(string: semvarString)
if let preReleaseString {
return semvar?.applyingPreRelease(preReleaseString)
}
return semvar
}
}
private extension Configuration.PreRelease {
func get(projectDirectory: String?) async throws -> String? {
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
var allowsPrefix = true
var preReleaseString: String
guard let strategy else { return nil }
switch strategy {
case let .branch(includeCommitSha: includeCommitSha):
logger.trace("Loading pre-relase branch strategy...")
preReleaseString = try await gitClient.version(.init(
gitDirectory: projectDirectory,
style: .branch(commitSha: includeCommitSha)
)).description
case .gitTag:
logger.trace("Loading pre-relase gitTag strategy...")
preReleaseString = try await gitClient.version(.init(
gitDirectory: projectDirectory,
style: .tag(exactMatch: false)
)).description
case let .command(arguments: arguments, allowPrefix: allowPrefix):
logger.trace("Loading pre-relase custom command strategy...")
allowsPrefix = allowPrefix ?? false
preReleaseString = try await asyncShellClient.background(.init(arguments))
}
if let prefix, allowsPrefix {
preReleaseString = "\(prefix)-\(preReleaseString)"
}
logger.trace("Pre-release string: \(preReleaseString)")
return preReleaseString
}
}

View File

@@ -0,0 +1,172 @@
import ConfigurationClient
import CustomDump
import Dependencies
import FileClient
import Foundation
import LoggingExtensions
enum VersionContainer: Sendable {
case branch(CurrentVersionContainer<String>)
case semvar(CurrentVersionContainer<SemVar>)
static func load(
projectDirectory: String?,
strategy: Configuration.VersionStrategy,
url: URL
) async throws -> Self {
switch strategy {
case let .branch(includeCommitSha: includeCommitSha):
return try await .branch(.load(
branch: .init(includeCommitSha: includeCommitSha),
projectDirectory: projectDirectory,
url: url
))
case .semvar:
return try await .semvar(.load(
semvar: strategy.semvar!,
projectDirectory: projectDirectory,
url: url
))
}
}
}
// TODO: Add a precedence field for which version to prefer, should also be specified in
// configuration.
struct CurrentVersionContainer<Version> {
let targetUrl: URL
let usesOptionalType: Bool
let loadedVersion: Version?
let precedence: Configuration.SemVar.Precedence?
let strategyVersion: Version?
}
extension CurrentVersionContainer: Equatable where Version: Equatable {
var hasChanges: Bool {
switch (loadedVersion, strategyVersion) {
case (.none, .none):
return false
case (.some, .none),
(.none, .some):
return true
case let (.some(loaded), .some(next)):
return loaded == next
}
}
}
extension CurrentVersionContainer: Sendable where Version: Sendable {}
extension CurrentVersionContainer where Version == String {
static func load(
branch: Configuration.Branch,
projectDirectory: String?,
url: URL
) async throws -> Self {
@Dependency(\.fileClient) var fileClient
@Dependency(\.gitClient) var gitClient
let loaded = try await fileClient.branch(
file: url,
projectDirectory: projectDirectory,
requireExistingFile: false
)
let next = try await gitClient.version(.init(
gitDirectory: projectDirectory,
style: .branch(commitSha: branch.includeCommitSha)
))
return .init(
targetUrl: url,
usesOptionalType: loaded?.1 ?? true,
loadedVersion: loaded?.0,
precedence: nil,
strategyVersion: next.description
)
}
var versionString: String? {
loadedVersion ?? strategyVersion
}
}
extension CurrentVersionContainer where Version == SemVar {
// TODO: Update to use precedence and not fetch `nextVersion` if we loaded a file version.
static func load(semvar: Configuration.SemVar, projectDirectory: String?, url: URL) async throws -> Self {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
logger.trace("Begin loading semvar from: \(url.cleanFilePath)")
async let (loaded, usesOptionalType) = try await loadCurrentVersion(
semvar: semvar,
projectDirectory: projectDirectory,
url: url
)
async let next = try await loadNextVersion(semvar: semvar, projectDirectory: projectDirectory)
return try await .init(
targetUrl: url,
usesOptionalType: usesOptionalType,
loadedVersion: loaded,
precedence: semvar.precedence,
strategyVersion: next
)
}
static func loadCurrentVersion(
semvar: Configuration.SemVar,
projectDirectory: String?,
url: URL
) async throws -> (SemVar?, Bool) {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
logger.trace("Begin loading current version from: \(url.cleanFilePath)")
let loadedOptional = try await fileClient.semvar(
file: url,
projectDirectory: projectDirectory,
requireExistingFile: semvar.requireExistingFile ?? false
)
guard let loadedStrong = loadedOptional else {
if semvar.requireExistingFile ?? false {
throw CliClientError.semVarNotFound(message: "Required by configuration's 'requireExistingFile' variable.")
}
return (nil, true)
}
let (loaded, usesOptionalType) = loadedStrong
logger.dump(loaded) { "Loaded version:\n\($0)" }
return (loaded, usesOptionalType)
}
static func loadNextVersion(semvar: Configuration.SemVar, projectDirectory: String?) async throws -> SemVar? {
@Dependency(\.logger) var logger
let next = try await SemVar.nextVersion(
configuration: semvar,
projectDirectory: projectDirectory
)
logger.dump(next) { "Next version:\n\($0)" }
return next
}
func versionString(withPreRelease: Bool) -> String? {
let version: SemVar?
switch precedence ?? .default {
case .file:
version = loadedVersion ?? strategyVersion
case .strategy:
version = strategyVersion ?? loadedVersion
}
return version?.versionString(withPreReleaseTag: withPreRelease)
}
}

View File

@@ -1,6 +1,7 @@
import Dependencies import Dependencies
import FileClient import FileClient
import Foundation import Foundation
import LoggingExtensions
@_spi(Internal) @_spi(Internal)
public extension Configuration { public extension Configuration {
@@ -44,8 +45,12 @@ public extension Configuration.Branch {
@_spi(Internal) @_spi(Internal)
public extension Configuration.SemVar { public extension Configuration.SemVar {
func merging(_ other: Self?) -> Self { func merging(_ other: Self?) -> Self {
.init( @Dependency(\.logger) var logger
logger.dump(other, level: .trace) { "Merging semvar:\n\($0)" }
return .init(
allowPreRelease: other?.allowPreRelease ?? allowPreRelease, allowPreRelease: other?.allowPreRelease ?? allowPreRelease,
precedence: other?.precedence ?? precedence,
preRelease: preRelease == nil ? other?.preRelease : preRelease!.merging(other?.preRelease), preRelease: preRelease == nil ? other?.preRelease : preRelease!.merging(other?.preRelease),
requireExistingFile: other?.requireExistingFile ?? requireExistingFile, requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar, requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar,

View File

@@ -44,97 +44,6 @@ public struct Configuration: Codable, Equatable, Sendable {
public extension Configuration { public extension Configuration {
/// Represents a branch version or pre-release strategy.
///
/// This derives the version or pre-release suffix from the branch name and
/// optionally the short version of the commit sha.
struct Branch: Codable, Equatable, Sendable {
/// Include the commit sha in the output for this strategy.
public let includeCommitSha: Bool
/// Create a new branch strategy.
///
/// - Parameters:
/// - includeCommitSha: Whether to include the commit sha.
public init(includeCommitSha: Bool = true) {
self.includeCommitSha = includeCommitSha
}
}
/// Represents version strategy for pre-release.
///
/// This appends a suffix to the version that get's generated from the version strategy.
/// For example: `1.0.0-rc-1`
///
struct PreRelease: Codable, Equatable, Sendable {
public let prefix: String?
// TODO: Remove optional.
public let strategy: Strategy?
public init(
prefix: String? = nil,
strategy: Strategy? = nil
) {
self.prefix = prefix
self.strategy = strategy
}
public enum Strategy: Codable, Equatable, Sendable {
case branch(includeCommitSha: Bool = true)
case command(arguments: [String])
// TODO: Remove.
case gitTag
public var branch: Branch? {
guard case let .branch(includeCommitSha) = self
else { return nil }
return .init(includeCommitSha: includeCommitSha)
}
}
}
/// Represents a semvar version strategy.
///
/// ## Example: 1.0.0
///
struct SemVar: Codable, Equatable, Sendable {
public let allowPreRelease: Bool?
/// Optional pre-releas suffix strategy.
public let preRelease: PreRelease?
/// Fail if an existing version file does not exist in the target.
public let requireExistingFile: Bool?
/// Fail if an existing semvar is not parsed from the file or version generation strategy.
public let requireExistingSemVar: Bool?
public let strategy: Strategy?
public init(
allowPreRelease: Bool? = true,
preRelease: PreRelease? = nil,
requireExistingFile: Bool? = true,
requireExistingSemVar: Bool? = true,
strategy: Strategy? = nil
) {
self.allowPreRelease = allowPreRelease
self.preRelease = preRelease
self.requireExistingFile = requireExistingFile
self.requireExistingSemVar = requireExistingSemVar
self.strategy = strategy
}
public enum Strategy: Codable, Equatable, Sendable {
case command(arguments: [String])
case gitTag(exactMatch: Bool? = false)
}
}
/// Represents the target where we will bump the version in. /// Represents the target where we will bump the version in.
/// ///
/// This can either be a path to a version file or a module used to /// This can either be a path to a version file or a module used to
@@ -236,6 +145,7 @@ public extension Configuration {
case semvar( case semvar(
allowPreRelease: Bool? = nil, allowPreRelease: Bool? = nil,
precedence: SemVar.Precedence? = nil,
preRelease: PreRelease? = nil, preRelease: PreRelease? = nil,
requireExistingFile: Bool? = nil, requireExistingFile: Bool? = nil,
requireExistingSemVar: Bool? = nil, requireExistingSemVar: Bool? = nil,
@@ -250,10 +160,16 @@ public extension Configuration {
} }
public var semvar: SemVar? { public var semvar: SemVar? {
guard case let .semvar(allowPreRelease, preRelease, requireExistingFile, requireExistingSemVar, strategy) = self guard case let .semvar(
allowPreRelease,
precedence,
preRelease,
requireExistingFile, requireExistingSemVar, strategy
) = self
else { return nil } else { return nil }
return .init( return .init(
allowPreRelease: allowPreRelease, allowPreRelease: allowPreRelease,
precedence: precedence,
preRelease: preRelease, preRelease: preRelease,
requireExistingFile: requireExistingFile ?? false, requireExistingFile: requireExistingFile ?? false,
requireExistingSemVar: requireExistingSemVar ?? false, requireExistingSemVar: requireExistingSemVar ?? false,
@@ -268,6 +184,7 @@ public extension Configuration {
public static func semvar(_ value: SemVar) -> Self { public static func semvar(_ value: SemVar) -> Self {
.semvar( .semvar(
allowPreRelease: value.allowPreRelease, allowPreRelease: value.allowPreRelease,
precedence: value.precedence,
preRelease: value.preRelease, preRelease: value.preRelease,
requireExistingFile: value.requireExistingFile, requireExistingFile: value.requireExistingFile,
requireExistingSemVar: value.requireExistingSemVar, requireExistingSemVar: value.requireExistingSemVar,
@@ -285,4 +202,128 @@ public extension Configuration {
} }
} }
/// Represents a branch version or pre-release strategy.
///
/// This derives the version or pre-release suffix from the branch name and
/// optionally the short version of the commit sha.
struct Branch: Codable, Equatable, Sendable {
/// Include the commit sha in the output for this strategy.
public let includeCommitSha: Bool
/// Create a new branch strategy.
///
/// - Parameters:
/// - includeCommitSha: Whether to include the commit sha.
public init(includeCommitSha: Bool = true) {
self.includeCommitSha = includeCommitSha
}
}
/// Represents version strategy for pre-release.
///
/// This appends a suffix to the version that get's generated from the version strategy.
/// For example: `1.0.0-rc-1`
///
struct PreRelease: Codable, Equatable, Sendable {
public let prefix: String?
public let strategy: Strategy?
public init(
prefix: String? = nil,
strategy: Strategy? = nil
) {
self.prefix = prefix
self.strategy = strategy
}
public enum Strategy: Codable, Equatable, Sendable {
case branch(includeCommitSha: Bool = true)
case command(arguments: [String], allowPrefix: Bool? = nil)
case gitTag
public var branch: Branch? {
guard case let .branch(includeCommitSha) = self
else { return nil }
return .init(includeCommitSha: includeCommitSha)
}
}
}
/// Represents a semvar version strategy.
///
/// ## Example: 1.0.0
///
struct SemVar: Codable, Equatable, Sendable {
/// Allow semvar to include a pre-release suffix.
public let allowPreRelease: Bool?
/// Set the precedence of version loaded from file versus
/// the version returned by the strategy.
///
/// These can not always agree / reflect the same version,
/// so the default is to give the file version precedence over
/// the strategy.
public let precedence: Precedence?
/// Optional pre-releas suffix strategy.
public let preRelease: PreRelease?
/// Fail if an existing version file does not exist in the target.
public let requireExistingFile: Bool?
/// Fail if an existing semvar is not parsed from the file or version generation strategy.
public let requireExistingSemVar: Bool?
/// The strategy used to derive a version for the project.
public let strategy: Strategy?
public init(
allowPreRelease: Bool? = true,
precedence: Precedence? = nil,
preRelease: PreRelease? = nil,
requireExistingFile: Bool? = false,
requireExistingSemVar: Bool? = false,
strategy: Strategy? = nil
) {
self.allowPreRelease = allowPreRelease
self.precedence = precedence
self.preRelease = preRelease
self.requireExistingFile = requireExistingFile
self.requireExistingSemVar = requireExistingSemVar
self.strategy = strategy
}
/// Represents a strategy to derive a version for a project.
public enum Strategy: Codable, Equatable, Sendable {
/// A custom external command that should return a string that
/// can be parsed as a semvar.
case command(arguments: [String])
/// Use `git describe --tags` optionally as an exact match.
case gitTag(exactMatch: Bool? = false)
}
/// Represents the precedence for which version to use when a file
/// exists, as they don't always agree. For example, a file could be edited
/// manually or the tag doesn't represent what is parsed from calling the
/// strategy.
///
/// The default is to defer to the file (if it exists) as having precedence over
/// the strategy.
public enum Precedence: String, CaseIterable, Codable, Equatable, Sendable {
/// Give the file precedence over the strategy.
case file
/// Give the strategy precedence over the file.
case strategy
/// The default precedence.
public static var `default`: Self { .file }
}
}
} }

View File

@@ -3,6 +3,8 @@ import DependenciesMacros
import FileClient import FileClient
import Foundation import Foundation
// TODO: Add a method to get a semvar / handle a version strategy's ??
public extension DependencyValues { public extension DependencyValues {
/// Perform operations with configuration files. /// Perform operations with configuration files.
@@ -16,6 +18,12 @@ public extension DependencyValues {
@DependencyClient @DependencyClient
public struct ConfigurationClient: Sendable { public struct ConfigurationClient: Sendable {
fileprivate enum Constants {
static let defaultFileNameWithoutExtension = ".bump-version"
static let defaultExtension = "json"
static var defaultFileName: String { "\(defaultFileNameWithoutExtension).\(defaultExtension)" }
}
/// The default file name for a configuration file. /// The default file name for a configuration file.
public var defaultFileName: @Sendable () -> String = { "test.json" } public var defaultFileName: @Sendable () -> String = { "test.json" }
@@ -29,11 +37,26 @@ public struct ConfigurationClient: Sendable {
public var write: @Sendable (Configuration, URL) async throws -> Void public var write: @Sendable (Configuration, URL) async throws -> Void
/// Find a configuration file and load it if found. /// Find a configuration file and load it if found.
public func findAndLoad(_ url: URL? = nil) async throws -> Configuration { ///
/// - Parameters:
/// - url: The optional path to the configuration file.
/// - strict: Fail if a configuration file is not found, otherwise return default configuration.
public func findAndLoad(_ url: URL? = nil, strict: Bool = true) async throws -> Configuration {
guard let url = try? await find(url) else { guard let url = try? await find(url) else {
throw ConfigurationClientError.configurationNotFound if strict {
throw ConfigurationClientError.configurationNotFound
}
return .default
} }
return (try? await load(url)) ?? .default
let loaded = try? await load(url)
guard let loaded else {
if strict {
throw ConfigurationClientError.configurationNotFound
}
return .default
}
return loaded
} }
/// Loads configuration from the given path, or searches for the default file and loads it. /// Loads configuration from the given path, or searches for the default file and loads it.
@@ -47,10 +70,12 @@ public struct ConfigurationClient: Sendable {
public func withConfiguration<T>( public func withConfiguration<T>(
path: String?, path: String?,
merging other: Configuration? = nil, merging other: Configuration? = nil,
strict: Bool = true,
operation: (Configuration) async throws -> T operation: (Configuration) async throws -> T
) async throws -> T { ) async throws -> T {
let configuration = try await findAndLoad( let configuration = try await findAndLoad(
path != nil ? URL(filePath: path!) : nil path != nil ? URL(filePath: path!) : nil,
strict: strict
) )
return try await operation(configuration.merging(other)) return try await operation(configuration.merging(other))
} }
@@ -72,15 +97,13 @@ extension ConfigurationClient: DependencyKey {
private func findConfiguration(_ url: URL?) async throws -> URL? { private func findConfiguration(_ url: URL?) async throws -> URL? {
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient
let defaultFileName = ConfigurationClient.Constants.defaultFileNameWithoutExtension
var url: URL! = url var url: URL! = url
if url == nil { if url == nil {
url = try await URL(filePath: fileClient.currentDirectory()) url = try await URL(filePath: fileClient.currentDirectory())
} }
if try await fileClient.isDirectory(url.cleanFilePath) { if try await fileClient.isDirectory(url.cleanFilePath) {
url = url.appending(path: "\(defaultFileName).json") url = url.appending(path: ConfigurationClient.Constants.defaultFileName)
} }
if fileClient.fileExists(url) { if fileClient.fileExists(url) {

View File

@@ -1,7 +1,4 @@
import Foundation import Foundation
extension ConfigurationClient { extension ConfigurationClient {
enum Constants {
static let defaultFileNameWithoutExtension = ".bump-version"
}
} }

View File

@@ -1,4 +1,6 @@
import CustomDump
import Dependencies import Dependencies
import Logging
import ShellClient import ShellClient
public struct LoggingOptions: Equatable, Sendable { public struct LoggingOptions: Equatable, Sendable {
@@ -26,3 +28,15 @@ public struct LoggingOptions: Equatable, Sendable {
} }
} }
} }
public extension Logger {
func dump<T>(
_ type: T,
level: Level = .trace,
buildMessage: @escaping (String) -> String = { $0 }
) {
var message = ""
customDump(type, to: &message)
log(level: level, "\(buildMessage(message))")
}
}

View File

@@ -1,5 +0,0 @@
# Application
## Articles
- <doc:Installation>

View File

@@ -1,3 +0,0 @@
# Installation
You can install the command-line application by...

View File

@@ -15,10 +15,13 @@ struct CliClientTests {
arguments: TestArguments.testCases arguments: TestArguments.testCases
) )
func testBuild(target: String) async throws { func testBuild(target: String) async throws {
let template = Template.build("1.0.0")
try await run { try await run {
$0.fileClient.fileExists = { _ in true } $0.fileClient.fileExists = { _ in true }
$0.fileClient.read = { @Sendable _ in template }
} operation: { } operation: {
@Dependency(\.cliClient) var client @Dependency(\.cliClient) var client
let output = try await client.build(.testOptions( let output = try await client.build(.testOptions(
target: target, target: target,
versionStrategy: .semvar(requireExistingFile: false) versionStrategy: .semvar(requireExistingFile: false)
@@ -138,11 +141,12 @@ extension CliClient.SharedOptions {
) -> Self { ) -> Self {
return .init( return .init(
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, projectDirectory: gitDirectory,
loggingOptions: .init(command: "test", verbose: 2), loggingOptions: .init(command: "test", verbose: 2),
target: .init(module: .init(target)), configurationToMerge: .init(
branch: versionStrategy.branch, target: .init(module: .init(target)),
semvar: versionStrategy.semvar strategy: versionStrategy
)
) )
} }
} }

View File

@@ -1,71 +0,0 @@
@_spi(Internal) import CliClient
import Dependencies
import FileClient
import GitClient
import ShellClient
import TestSupport
import XCTest
// TODO: Remove
final class GitVersionTests: XCTestCase {
override func invokeTest() {
withDependencies({
$0.logger.logLevel = .debug
$0.logger = .liveValue
$0.asyncShellClient = .liveValue
$0.gitClient = .liveValue
$0.fileClient = .liveValue
}, operation: {
super.invokeTest()
})
}
var gitDir: String {
URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.cleanFilePath
}
// #if !os(Linux)
// func test_live() async throws {
// @Dependency(\.gitClient) var versionClient: GitClient
//
// let version = try await versionClient.currentVersion(in: gitDir)
// print("VERSION: \(version)")
// // can't really have a predictable result for the live client.
// XCTAssertNotEqual(version, "blob")
// }
// #endif
func test_file_client() async throws {
try await withTemporaryDirectory { tmpDir in
@Dependency(\.fileClient) var fileClient
let filePath = tmpDir.appendingPathComponent("blob.txt")
try await fileClient.write(string: "Blob", to: filePath)
let contents = try await fileClient.read(filePath)
.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(contents, "Blob")
}
}
func test_file_client_with_string_path() async throws {
try await withTemporaryDirectory { tmpDir in
@Dependency(\.fileClient) var fileClient
let filePath = tmpDir.appendingPathComponent("blob.txt")
let fileString = filePath.cleanFilePath
try await fileClient.write(string: "Blob", to: fileString)
let contents = try await fileClient.read(fileString)
.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(contents, "Blob")
}
}
}

View File

@@ -1,4 +1,5 @@
@_spi(Internal) import ConfigurationClient @_spi(Internal) import ConfigurationClient
import CustomDump
import Dependencies import Dependencies
import Foundation import Foundation
import Testing import Testing
@@ -91,8 +92,8 @@ struct ConfigurationClientTests {
let other = Configuration.VersionStrategy.semvar(.init( let other = Configuration.VersionStrategy.semvar(.init(
allowPreRelease: true, allowPreRelease: true,
preRelease: .init(prefix: "foo", strategy: .gitTag), preRelease: .init(prefix: "foo", strategy: .gitTag),
requireExistingFile: true, requireExistingFile: false,
requireExistingSemVar: true, requireExistingSemVar: false,
strategy: .gitTag() strategy: .gitTag()
)) ))
let merged = strategy1.merging(other) let merged = strategy1.merging(other)

1
gum Normal file
View File

@@ -0,0 +1 @@
foo,bar,baz table

View File

@@ -5,7 +5,8 @@ docker_tag := "test"
tap_url := "https://git.housh.dev/michael/homebrew-formula" tap_url := "https://git.housh.dev/michael/homebrew-formula"
tap := "michael/formula" tap := "michael/formula"
formula := "bump-version" formula := "bump-version"
release_base_url := "https://git.housh.dev/michael/bump-version/archive" release_base_url := "https://git.housh.dev/michael/swift-bump-version/archive"
version := "$(git describe --tags --exact-match)"
[private] [private]
default: default:
@@ -26,7 +27,8 @@ build configuration="debug":
--configuration {{configuration}} \ --configuration {{configuration}} \
--product {{product}} --product {{product}}
alias b := build # Bump our version of the command-line tool.
bump-version *ARGS: (run "bump" ARGS)
# Build a docker image. # Build a docker image.
build-docker configuration="debug": build-docker configuration="debug":
@@ -43,7 +45,9 @@ clean:
# Clean and build. # Clean and build.
clean-build configuration="debug": clean (build configuration) clean-build configuration="debug": clean (build configuration)
alias cb := clean-build # Remove bottles
remove-bottles:
rm -rf *.gz
# Test locally. # Test locally.
test *ARGS: test *ARGS:
@@ -53,14 +57,36 @@ test *ARGS:
test-docker: build-docker test-docker: build-docker
@docker run --rm {{docker_image}}:{{docker_tag}} @docker run --rm {{docker_image}}:{{docker_tag}}
# Run tests in docker without building a new image.
test-docker-without-building: test-docker-without-building:
@docker run --rm {{docker_image}}:{{docker_tag}} swift test @docker run --rm {{docker_image}}:{{docker_tag}} swift test
# Show the current git-tag version.
echo-version:
@echo "VERSION: {{version}}"
# Get the sha256 sum of the release and copy to clipboard. # Get the sha256 sum of the release and copy to clipboard.
get-release-sha prefix="": (build "release") get-release-sha prefix="": (build "release")
version=$(.build/release/hpa --version) && \ url="{{release_base_url}}/{{prefix}}${version}.tar.gz" && \
url="{{release_base_url}}/{{prefix}}${version}.tar.gz" && \
sha=$(curl "$url" | shasum -a 256) && \ sha=$(curl "$url" | shasum -a 256) && \
stripped="${sha% *}" && \ stripped="${sha% *}" && \
echo "$stripped" | pbcopy && \ echo "$stripped" | pbcopy && \
echo "Copied sha to clipboard: $stripped" echo "Copied sha to clipboard: $stripped"
# Preview the documentation locally.
preview-documentation target="BumpVersion":
swift package \
--disable-sandbox \
preview-documentation \
--target {{target}}
# Preview the documentation locally.
build-documentation dir="./docs" target="BumpVersion" basePath="bump-version":
swift package \
--allow-writing-to-directory {{dir}} \
generate-documentation \
--target {{target}} \
--disable-indexing \
--transform-for-static-hosting \
--hosting-base-path {{basePath}} \
--output-path {{dir}}