Compare commits
19 Commits
fbb4a22e98
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
016f0d6c3f
|
|||
|
147f6df1b3
|
|||
|
5c22250a63
|
|||
|
9dd30a1745
|
|||
|
f1eb883b93
|
|||
|
9631c62ee3
|
|||
|
6fe459c39e
|
|||
|
b9cf913528
|
|||
|
f9710e5992
|
|||
|
a26d8695f5
|
|||
|
20f430fb8f
|
|||
|
4420bd428a
|
|||
|
8d73287a60
|
|||
|
5bac7aa577
|
|||
|
47bad744d5
|
|||
|
2650bdc670
|
|||
|
a0f8611a76
|
|||
|
86a344fa9f
|
|||
|
56359f3488
|
@@ -1,10 +1,17 @@
|
||||
{
|
||||
"target" : {
|
||||
"module" : { "name" : "cli-version" }
|
||||
},
|
||||
"strategy" : {
|
||||
"semvar" : {
|
||||
"strategy" : { "gitTag": { "exactMatch": false } }
|
||||
"allowPreRelease" : true,
|
||||
"strategy" : {
|
||||
"gitTag" : {
|
||||
"exactMatch" : false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"target" : {
|
||||
"module" : {
|
||||
"name" : "BumpVersion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
.bump-version.pre.json
Normal file
27
.bump-version.pre.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -14,12 +14,14 @@ jobs:
|
||||
config: ['debug', 'release']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install just.
|
||||
run: brew install just
|
||||
- name: Select Xcode ${{ matrix.xcode }}
|
||||
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
|
||||
- name: Swift Version
|
||||
run: swift --version
|
||||
- name: Run ${{ matrix.xcode }} Tests
|
||||
run: make CONFIG=${{ matrix.config }} test-library
|
||||
run: just test --configuration ${{ matrix.config }}
|
||||
|
||||
ubuntu:
|
||||
name: Ubuntu
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
**/*.docc/*.md
|
||||
2
.spi.yml
2
.spi.yml
@@ -1,4 +1,4 @@
|
||||
version: 1
|
||||
builder:
|
||||
configs:
|
||||
- documentation_targets: [CliVersion]
|
||||
- documentation_targets: [BumpVersion]
|
||||
|
||||
42
Makefile
42
Makefile
@@ -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
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "6ab0a9c883cfa1490d249a344074ad27369033fab78e1a90272ef07339a8c0ab",
|
||||
"originHash" : "9fe004cf869b34d1fe07e8b58a90b044281e8e94805df6723d4604ba5a6400d9",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
@@ -28,6 +28,15 @@
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-cli-doc",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/m-housh/swift-cli-doc.git",
|
||||
"state" : {
|
||||
"revision" : "bbace73d974fd3e6985461431692bea773c7c5d8",
|
||||
"version" : "0.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-clocks",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -8,18 +8,19 @@ let package = Package(
|
||||
.macOS(.v13)
|
||||
],
|
||||
products: [
|
||||
.executable(name: "bump-version", targets: ["bump-version"]),
|
||||
.executable(name: "bump-version", targets: ["BumpVersion"]),
|
||||
.library(name: "CliClient", targets: ["CliClient"]),
|
||||
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
||||
.library(name: "FileClient", targets: ["FileClient"]),
|
||||
.library(name: "GitClient", targets: ["GitClient"]),
|
||||
.library(name: "LoggingExtensions", targets: ["LoggingExtensions"]),
|
||||
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
|
||||
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]),
|
||||
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])
|
||||
.plugin(name: "BumpVersionPlugin", targets: ["BumpVersionPlugin"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"),
|
||||
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.2"),
|
||||
.package(url: "https://github.com/m-housh/swift-cli-doc.git", from: "0.2.1"),
|
||||
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
|
||||
@@ -27,11 +28,12 @@ let package = Package(
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "bump-version",
|
||||
name: "BumpVersion",
|
||||
dependencies: [
|
||||
"CliClient",
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
.product(name: "CustomDump", package: "swift-custom-dump")
|
||||
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||
.product(name: "CliDoc", package: "swift-cli-doc")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
@@ -40,6 +42,7 @@ let package = Package(
|
||||
"ConfigurationClient",
|
||||
"FileClient",
|
||||
"GitClient",
|
||||
"LoggingExtensions",
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "CustomDump", package: "swift-custom-dump")
|
||||
]
|
||||
@@ -52,6 +55,7 @@ let package = Package(
|
||||
name: "ConfigurationClient",
|
||||
dependencies: [
|
||||
"FileClient",
|
||||
"LoggingExtensions",
|
||||
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
||||
@@ -81,42 +85,35 @@ let package = Package(
|
||||
name: "GitClientTests",
|
||||
dependencies: ["GitClient"]
|
||||
),
|
||||
.target(
|
||||
name: "LoggingExtensions",
|
||||
dependencies: [
|
||||
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "ShellClient", package: "swift-shell-client")
|
||||
]
|
||||
),
|
||||
.target(name: "TestSupport"),
|
||||
.plugin(
|
||||
name: "BuildWithVersionPlugin",
|
||||
capability: .buildTool(),
|
||||
dependencies: [
|
||||
"bump-version"
|
||||
"BumpVersion"
|
||||
]
|
||||
),
|
||||
.plugin(
|
||||
name: "GenerateVersionPlugin",
|
||||
name: "BumpVersionPlugin",
|
||||
capability: .command(
|
||||
intent: .custom(
|
||||
verb: "generate-version",
|
||||
description: "Generates 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."
|
||||
verb: "bump-version",
|
||||
description: "Bumps a version file in the given target."
|
||||
),
|
||||
permissions: [
|
||||
.writeToPackageDirectory(reason: "Update a version file in the target's directory.")
|
||||
]
|
||||
),
|
||||
dependencies: [
|
||||
"bump-version"
|
||||
"BumpVersion"
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
@@ -13,7 +13,7 @@ struct GenerateVersionBuildPlugin: BuildToolPlugin {
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
|
||||
let tool = try context.tool(named: "cli-version")
|
||||
let tool = try context.tool(named: "bump-version")
|
||||
let outputPath = context.pluginWorkDirectoryURL
|
||||
|
||||
let outputFile = outputPath.appending(path: "Version.swift")
|
||||
@@ -25,7 +25,7 @@ struct GenerateVersionBuildPlugin: BuildToolPlugin {
|
||||
arguments: [
|
||||
"build", "--verbose",
|
||||
"--git-directory", gitDirectoryPath.absoluteString,
|
||||
"--target", outputPath.absoluteString
|
||||
"--target-file-path", outputPath.absoluteString
|
||||
],
|
||||
environment: [:],
|
||||
inputFiles: target.sourceFiles.map(\.url),
|
||||
|
||||
24
Plugins/BumpVersionPlugin/BumpVersionPlugin.swift
Normal file
24
Plugins/BumpVersionPlugin/BumpVersionPlugin.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
26
Release.md
Normal 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`
|
||||
@@ -3,8 +3,10 @@ import Foundation
|
||||
|
||||
@main
|
||||
struct Application: AsyncParsableCommand {
|
||||
static let commandName = "bump-version"
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "bump-version",
|
||||
commandName: commandName,
|
||||
version: VERSION ?? "0.0.0",
|
||||
subcommands: [
|
||||
BuildCommand.self,
|
||||
@@ -1,15 +1,19 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import CliDoc
|
||||
import Foundation
|
||||
import ShellClient
|
||||
|
||||
// NOTE: This command is only used with the build with version plugin.
|
||||
struct BuildCommand: AsyncParsableCommand {
|
||||
static let commandName = "build"
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: Self.commandName,
|
||||
abstract: "Used for the build with version plugin.",
|
||||
discussion: "This should generally not be interacted with directly, outside of the build plugin.",
|
||||
abstract: Abstract.default("Used for the build with version plugin.").render(),
|
||||
discussion: Discussion {
|
||||
"This should generally not be interacted with directly, outside of the build plugin."
|
||||
},
|
||||
shouldDisplay: false
|
||||
)
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import CliDoc
|
||||
import Dependencies
|
||||
|
||||
struct BumpCommand: AsyncParsableCommand {
|
||||
struct BumpCommand: CommandRepresentable {
|
||||
|
||||
static let commandName = "bump"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: Self.commandName,
|
||||
abstract: "Bump version of a command-line tool."
|
||||
abstract: Abstract.default("Bump version of a command-line tool."),
|
||||
usage: Usage.default(commandName: nil),
|
||||
discussion: Discussion.default(examples: [
|
||||
makeExample(
|
||||
label: "Basic usage, bump the minor version.",
|
||||
example: "--minor"
|
||||
),
|
||||
makeExample(
|
||||
label: "Dry run, just show what the bumped version would be.",
|
||||
example: "--minor --print"
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
@@ -1,5 +1,6 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import CliDoc
|
||||
import ConfigurationClient
|
||||
import CustomDump
|
||||
import Dependencies
|
||||
@@ -7,9 +8,11 @@ import FileClient
|
||||
import Foundation
|
||||
|
||||
struct ConfigCommand: AsyncParsableCommand {
|
||||
static let commandName = "config"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "config",
|
||||
abstract: "Configuration commands",
|
||||
commandName: commandName,
|
||||
abstract: Abstract.default("Configuration commands").render(),
|
||||
subcommands: [
|
||||
DumpConfig.self,
|
||||
GenerateConfig.self
|
||||
@@ -19,35 +22,71 @@ struct ConfigCommand: AsyncParsableCommand {
|
||||
|
||||
extension ConfigCommand {
|
||||
|
||||
struct DumpConfig: AsyncParsableCommand {
|
||||
struct DumpConfig: CommandRepresentable {
|
||||
static let commandName = "dump"
|
||||
static let parentCommand = ConfigCommand.commandName
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: Self.commandName,
|
||||
abstract: "Inspect the parsed configuration.",
|
||||
discussion: """
|
||||
This will load any configuration and merge the options passed in. Then print it to stdout.
|
||||
The default style is to print the output in `swift`, however you can use the `--print` flag to
|
||||
print the output in `json`.
|
||||
""",
|
||||
abstract: Abstract.default("Inspect the parsed configuration."),
|
||||
usage: Usage.default(parentCommand: ConfigCommand.commandName, commandName: Self.commandName),
|
||||
discussion: Discussion.default(
|
||||
notes: [
|
||||
"""
|
||||
The default style is to print the output in `json`, however you can use the `--swift` flag to
|
||||
print the output in `swift`.
|
||||
"""
|
||||
],
|
||||
examples: [
|
||||
makeExample(label: "Show the project configuration.", example: ""),
|
||||
makeExample(
|
||||
label: "Update a configuration file with the dumped output",
|
||||
example: "--disable-pre-release > .bump-version.prod.json"
|
||||
)
|
||||
]
|
||||
) {
|
||||
"""
|
||||
Loads the project configuration file (if applicable) and merges the options passed in,
|
||||
then prints the configuration to stdout.
|
||||
"""
|
||||
},
|
||||
aliases: ["d"]
|
||||
)
|
||||
@Flag(
|
||||
help: "Change the style of what get's printed."
|
||||
)
|
||||
fileprivate var printStyle: PrintStyle = .json
|
||||
|
||||
@OptionGroup var globals: ConfigCommandOptions
|
||||
|
||||
func run() async throws {
|
||||
@Dependency(\.logger) var logger
|
||||
let configuration = try await globals
|
||||
.shared(command: Self.commandName)
|
||||
.runClient(\.parsedConfiguration)
|
||||
|
||||
try globals.printConfiguration(configuration)
|
||||
try globals.printConfiguration(configuration, style: printStyle)
|
||||
}
|
||||
}
|
||||
|
||||
struct GenerateConfig: AsyncParsableCommand {
|
||||
struct GenerateConfig: CommandRepresentable {
|
||||
static let commandName = "generate"
|
||||
static let parentCommand = ConfigCommand.commandName
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "generate",
|
||||
abstract: "Generate a configuration file.",
|
||||
commandName: commandName,
|
||||
abstract: Abstract.default("Generate a configuration file, based on the given options.").render(),
|
||||
usage: Usage.default(parentCommand: ConfigCommand.commandName, commandName: commandName),
|
||||
discussion: Discussion.default(examples: [
|
||||
makeExample(
|
||||
label: "Generate a configuration file for the 'foo' target.",
|
||||
example: "-m foo"
|
||||
),
|
||||
makeExample(
|
||||
label: "Show the output and don't write to a file.",
|
||||
example: "-m foo --print"
|
||||
)
|
||||
]),
|
||||
aliases: ["g"]
|
||||
)
|
||||
|
||||
@@ -56,6 +95,12 @@ extension ConfigCommand {
|
||||
)
|
||||
var style: ConfigCommand.Style = .semvar
|
||||
|
||||
@Flag(
|
||||
name: .customLong("print"),
|
||||
help: "Print json to stdout."
|
||||
)
|
||||
var printJson: Bool = false
|
||||
|
||||
@OptionGroup var globals: ConfigCommandOptions
|
||||
|
||||
func run() async throws {
|
||||
@@ -67,7 +112,7 @@ extension ConfigCommand {
|
||||
extraOptions: globals.extraOptions
|
||||
)
|
||||
|
||||
switch globals.printJson {
|
||||
switch printJson {
|
||||
case true:
|
||||
try globals.handlePrintJson(configuration)
|
||||
case false:
|
||||
@@ -108,14 +153,14 @@ extension ConfigCommand {
|
||||
@dynamicMemberLookup
|
||||
struct ConfigCommandOptions: ParsableArguments {
|
||||
|
||||
@Flag(
|
||||
name: .customLong("print"),
|
||||
help: "Print style to stdout."
|
||||
)
|
||||
var printJson: Bool = false
|
||||
|
||||
@OptionGroup var configOptions: ConfigurationOptions
|
||||
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Increase logging level, can be passed multiple times (example: -vvv)."
|
||||
)
|
||||
var verbose: Int
|
||||
|
||||
@Argument(
|
||||
help: """
|
||||
Arguments / options used for custom pre-release, options / flags must proceed a '--' in
|
||||
@@ -130,10 +175,20 @@ extension ConfigCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConfigCommand.DumpConfig {
|
||||
enum PrintStyle: EnumerableFlag {
|
||||
case json, swift
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConfigCommand.ConfigCommandOptions {
|
||||
|
||||
func shared(command: String) throws -> CliClient.SharedOptions {
|
||||
try configOptions.shared(command: command, extraOptions: extraOptions)
|
||||
try configOptions.shared(
|
||||
command: command,
|
||||
extraOptions: extraOptions,
|
||||
verbose: verbose
|
||||
)
|
||||
}
|
||||
|
||||
func handlePrintJson(_ configuration: Configuration) throws {
|
||||
@@ -148,12 +203,16 @@ private extension ConfigCommand.ConfigCommandOptions {
|
||||
print(string)
|
||||
}
|
||||
|
||||
func printConfiguration(_ configuration: Configuration) throws {
|
||||
guard printJson else {
|
||||
func printConfiguration(
|
||||
_ configuration: Configuration,
|
||||
style: ConfigCommand.DumpConfig.PrintStyle
|
||||
) throws {
|
||||
switch style {
|
||||
case .json:
|
||||
try handlePrintJson(configuration)
|
||||
case .swift:
|
||||
customDump(configuration)
|
||||
return
|
||||
}
|
||||
try handlePrintJson(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
29
Sources/BumpVersion/Commands/GenerateCommand.swift
Normal file
29
Sources/BumpVersion/Commands/GenerateCommand.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import CliDoc
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ShellClient
|
||||
|
||||
struct GenerateCommand: CommandRepresentable {
|
||||
static let commandName = "generate"
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: Self.commandName,
|
||||
abstract: Abstract.default("Generates a version file in your project."),
|
||||
usage: Usage.default(commandName: Self.commandName),
|
||||
discussion: Discussion.default(
|
||||
examples: [
|
||||
makeExample(label: "Basic usage.", example: "")
|
||||
]
|
||||
) {
|
||||
"This command can be interacted with directly, outside of the plugin usage context."
|
||||
}
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
|
||||
func run() async throws {
|
||||
try await globals.run(\.generate, command: Self.commandName)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
```
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
62
Sources/BumpVersion/Documentation.docc/BumpVersion.md
Normal file
62
Sources/BumpVersion/Documentation.docc/BumpVersion.md
Normal 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>
|
||||
@@ -5,19 +5,21 @@ import Dependencies
|
||||
import Foundation
|
||||
import Rainbow
|
||||
|
||||
// TODO: Add an option to not load project configuration.
|
||||
|
||||
struct GlobalOptions: ParsableArguments {
|
||||
|
||||
@OptionGroup
|
||||
var configOptions: ConfigurationOptions
|
||||
|
||||
@Option(
|
||||
name: .customLong("git-directory"),
|
||||
help: "The git directory for the version (default: current directory)"
|
||||
name: .customLong("project-directory"),
|
||||
help: "The project directory. (default: current directory)"
|
||||
)
|
||||
var gitDirectory: String?
|
||||
var projectDirectory: String?
|
||||
|
||||
@Flag(
|
||||
name: .customLong("dry-run"),
|
||||
name: .customLong("print"),
|
||||
help: "Print's what would be written to a target version file."
|
||||
)
|
||||
var dryRun: Bool = false
|
||||
@@ -39,9 +41,10 @@ struct GlobalOptions: ParsableArguments {
|
||||
}
|
||||
|
||||
struct ConfigurationOptions: ParsableArguments {
|
||||
|
||||
@Option(
|
||||
name: [.customShort("f"), .long],
|
||||
help: "Specify the path to a configuration file.",
|
||||
help: "Specify the path to a configuration file. (default: .bump-version.json)",
|
||||
completion: .file(extensions: ["json"])
|
||||
)
|
||||
var configurationFile: String?
|
||||
@@ -59,6 +62,14 @@ struct ConfigurationOptions: ParsableArguments {
|
||||
)
|
||||
var commitSha: Bool = true
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help: """
|
||||
Require a configuration file, otherwise fail.
|
||||
"""
|
||||
)
|
||||
var requireConfiguration: Bool = false
|
||||
|
||||
}
|
||||
|
||||
struct TargetOptions: ParsableArguments {
|
||||
@@ -124,7 +135,6 @@ struct PreReleaseOptions: ParsableArguments {
|
||||
|
||||
}
|
||||
|
||||
// TODO: Add custom command strategy.
|
||||
struct SemVarOptions: ParsableArguments {
|
||||
|
||||
@Flag(
|
||||
@@ -163,5 +173,19 @@ struct SemVarOptions: ParsableArguments {
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
extension Configuration.SemVar.Precedence: ExpressibleByArgument {
|
||||
public init?(argument: String) {
|
||||
self.init(rawValue: argument)
|
||||
}
|
||||
}
|
||||
311
Sources/BumpVersion/Helpers/DocHelpers.swift
Normal file
311
Sources/BumpVersion/Helpers/DocHelpers.swift
Normal file
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
This file contains helpers for generating the documentation for the commands.
|
||||
|
||||
*/
|
||||
import ArgumentParser
|
||||
import CliDoc
|
||||
import Rainbow
|
||||
|
||||
protocol CommandRepresentable: AsyncParsableCommand {
|
||||
static var commandName: String { get }
|
||||
static var parentCommand: String? { get }
|
||||
}
|
||||
|
||||
extension CommandRepresentable {
|
||||
|
||||
static var parentCommand: String? { nil }
|
||||
|
||||
static func makeExample(
|
||||
label: String,
|
||||
example: String,
|
||||
includesAppName: Bool = true
|
||||
) -> AppExample {
|
||||
.init(
|
||||
label: label,
|
||||
parentCommand: parentCommand,
|
||||
commandName: commandName,
|
||||
includesAppName: includesAppName,
|
||||
example: example
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Abstract where Content == String {
|
||||
static func `default`(_ string: String) -> Self {
|
||||
.init { string.blue }
|
||||
}
|
||||
}
|
||||
|
||||
struct Note<Content: TextNode>: TextNode {
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
@TextBuilder _ content: () -> Content
|
||||
) {
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some TextNode {
|
||||
LabeledContent {
|
||||
content.italic()
|
||||
} label: {
|
||||
"Note:".yellow.bold
|
||||
}
|
||||
.style(.vertical())
|
||||
}
|
||||
}
|
||||
|
||||
extension Note where Content == AnyTextNode {
|
||||
|
||||
static func `default`(
|
||||
notes: [String],
|
||||
usesConfigurationFileNote: Bool = true,
|
||||
usesConfigurationMergingNote: Bool = true
|
||||
) -> Self {
|
||||
var notes = notes
|
||||
|
||||
if usesConfigurationFileNote {
|
||||
notes.insert(
|
||||
"Most options are not required when a configuration file is setup for your project.",
|
||||
at: 0
|
||||
)
|
||||
}
|
||||
|
||||
if usesConfigurationMergingNote {
|
||||
if usesConfigurationFileNote {
|
||||
notes.insert(
|
||||
"Any configuration options get merged with the loaded project configuration file.",
|
||||
at: 1
|
||||
)
|
||||
} else {
|
||||
notes.insert(
|
||||
"Any configuration options get merged with the loaded project configuration file.",
|
||||
at: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return .init {
|
||||
VStack {
|
||||
notes.enumerated().map { "\($0 + 1). \($1)" }
|
||||
}
|
||||
.eraseToAnyTextNode()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Discussion where Content == AnyTextNode {
|
||||
static func `default`<Preamble: TextNode, Trailing: TextNode>(
|
||||
notes: [String] = [],
|
||||
examples: [AppExample]? = nil,
|
||||
usesExtraOptions: Bool = true,
|
||||
usesConfigurationFileNote: Bool = true,
|
||||
usesConfigurationMergingNote: Bool = true,
|
||||
@TextBuilder preamble: () -> Preamble,
|
||||
@TextBuilder trailing: () -> Trailing
|
||||
) -> Self {
|
||||
Discussion {
|
||||
VStack {
|
||||
preamble().italic()
|
||||
|
||||
Note.default(
|
||||
notes: notes,
|
||||
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||
usesConfigurationMergingNote: usesConfigurationMergingNote
|
||||
)
|
||||
|
||||
if let examples {
|
||||
ExampleSection.default(examples: examples, usesExtraOptions: usesExtraOptions)
|
||||
}
|
||||
|
||||
trailing()
|
||||
}
|
||||
.separator(.newLine(count: 2))
|
||||
.eraseToAnyTextNode()
|
||||
}
|
||||
}
|
||||
|
||||
static func `default`<Preamble: TextNode>(
|
||||
notes: [String] = [],
|
||||
examples: [AppExample]? = nil,
|
||||
usesExtraOptions: Bool = true,
|
||||
usesConfigurationFileNote: Bool = true,
|
||||
usesConfigurationMergingNote: Bool = true,
|
||||
@TextBuilder preamble: () -> Preamble
|
||||
) -> Self {
|
||||
.default(
|
||||
notes: notes,
|
||||
examples: examples,
|
||||
usesExtraOptions: usesExtraOptions,
|
||||
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||
usesConfigurationMergingNote: usesConfigurationMergingNote,
|
||||
preamble: preamble,
|
||||
trailing: {
|
||||
if usesExtraOptions {
|
||||
ImportantNote.extraOptionsNote
|
||||
} else {
|
||||
Empty()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
static func `default`(
|
||||
notes: [String] = [],
|
||||
examples: [AppExample]? = nil,
|
||||
usesExtraOptions: Bool = true,
|
||||
usesConfigurationFileNote: Bool = true,
|
||||
usesConfigurationMergingNote: Bool = true
|
||||
) -> Self {
|
||||
.default(
|
||||
notes: notes,
|
||||
examples: examples,
|
||||
usesExtraOptions: usesExtraOptions,
|
||||
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||
usesConfigurationMergingNote: usesConfigurationMergingNote,
|
||||
preamble: { Empty() },
|
||||
trailing: { Empty() }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ExampleSection where Header == String, Label == String {
|
||||
static func `default`(
|
||||
examples: [AppExample] = [],
|
||||
usesExtraOptions: Bool = true
|
||||
) -> some TextNode {
|
||||
var examples: [AppExample] = examples
|
||||
if usesExtraOptions {
|
||||
examples = examples.appendingExtraOptionsExample()
|
||||
}
|
||||
|
||||
return Self(
|
||||
"Examples:",
|
||||
label: "A few common usage examples.",
|
||||
examples: examples.map(\.example)
|
||||
)
|
||||
.style(AppExampleSectionStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct AppExampleSectionStyle: ExampleSectionStyle {
|
||||
|
||||
func render(content: ExampleSectionConfiguration) -> some TextNode {
|
||||
Section {
|
||||
VStack {
|
||||
content.examples.map { example in
|
||||
VStack {
|
||||
example.label.color(.green).bold()
|
||||
ShellCommand(example.example).style(.default)
|
||||
}
|
||||
}
|
||||
}
|
||||
.separator(.newLine(count: 2))
|
||||
} header: {
|
||||
HStack {
|
||||
content.title.color(.blue).bold()
|
||||
content.label.italic()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppExample {
|
||||
let label: String
|
||||
let parentCommand: String?
|
||||
let commandName: String
|
||||
let includesAppName: Bool
|
||||
let exampleText: String
|
||||
|
||||
init(
|
||||
label: String,
|
||||
parentCommand: String? = nil,
|
||||
commandName: String,
|
||||
includesAppName: Bool = true,
|
||||
example exampleText: String
|
||||
) {
|
||||
self.label = label
|
||||
self.parentCommand = parentCommand
|
||||
self.commandName = commandName
|
||||
self.includesAppName = includesAppName
|
||||
self.exampleText = exampleText
|
||||
}
|
||||
|
||||
var example: Example {
|
||||
var exampleString = "\(commandName) \(exampleText)"
|
||||
if let parentCommand {
|
||||
exampleString = "\(parentCommand) \(exampleString)"
|
||||
}
|
||||
if includesAppName {
|
||||
exampleString = "\(Application.commandName) \(exampleString)"
|
||||
}
|
||||
return (label: label, example: exampleString)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == AppExample {
|
||||
|
||||
func appendingExtraOptionsExample() -> Self {
|
||||
guard let first = first else { return self }
|
||||
var output = self
|
||||
output.append(.init(
|
||||
label: "Passing extra options to custom strategy.",
|
||||
parentCommand: first.parentCommand,
|
||||
commandName: first.commandName,
|
||||
includesAppName: first.includesAppName,
|
||||
example: "--custom-command -- git describe --tags --exact-match"
|
||||
))
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
struct ImportantNote<Content: TextNode>: TextNode {
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
@TextBuilder _ content: () -> Content
|
||||
) {
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some TextNode {
|
||||
LabeledContent {
|
||||
content.italic()
|
||||
} label: {
|
||||
"Important Note:".red.bold
|
||||
}
|
||||
.style(.vertical())
|
||||
}
|
||||
}
|
||||
|
||||
extension ImportantNote where Content == String {
|
||||
static var extraOptionsNote: Self {
|
||||
.init {
|
||||
"""
|
||||
Extra options / flags for custom strategies must proceed a `--` or you may get an undefined option error.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Usage where Content == AnyTextNode {
|
||||
static func `default`(parentCommand: String? = nil, commandName: String?) -> Self {
|
||||
var commandString = commandName == nil ? "" : "\(commandName!)"
|
||||
if let parentCommand {
|
||||
commandString = "\(parentCommand) \(commandString)"
|
||||
}
|
||||
commandString = commandString == "" ? "\(Application.commandName)" : "\(Application.commandName) \(commandString)"
|
||||
|
||||
return .init {
|
||||
HStack {
|
||||
commandString.blue
|
||||
"[<options>]".green
|
||||
"--"
|
||||
"[<extra-options> ...]".cyan
|
||||
}
|
||||
.eraseToAnyTextNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ extension GlobalOptions {
|
||||
command: command,
|
||||
dryRun: dryRun,
|
||||
extraOptions: extraOptions,
|
||||
gitDirectory: gitDirectory,
|
||||
gitDirectory: projectDirectory,
|
||||
verbose: verbose
|
||||
)
|
||||
}
|
||||
@@ -129,7 +129,6 @@ extension SemVarOptions {
|
||||
) throws -> Configuration.SemVar {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
// TODO: Update when / if there's an update config command.
|
||||
if customCommand && preRelease.customPreRelease {
|
||||
logger.warning("""
|
||||
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(
|
||||
allowPreRelease: !preRelease.disablePreRelease,
|
||||
precedence: precedence,
|
||||
preRelease: customCommand ? nil : preRelease.configPreReleaseStrategy(
|
||||
includeCommitSha: includeCommitSha,
|
||||
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(
|
||||
command: String,
|
||||
dryRun: Bool = true,
|
||||
@@ -177,12 +188,11 @@ extension ConfigurationOptions {
|
||||
try .init(
|
||||
allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease,
|
||||
dryRun: dryRun,
|
||||
gitDirectory: gitDirectory,
|
||||
projectDirectory: gitDirectory,
|
||||
loggingOptions: .init(command: command, verbose: verbose),
|
||||
target: target(),
|
||||
branch: .init(includeCommitSha: commitSha),
|
||||
semvar: semvarOptions(extraOptions: extraOptions),
|
||||
configurationFile: configurationFile
|
||||
configurationToMerge: configurationToMerge(extraOptions: extraOptions),
|
||||
configurationFile: configurationFile,
|
||||
requireConfigurationFile: requireConfiguration
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
// 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"
|
||||
@@ -4,10 +4,12 @@ import DependenciesMacros
|
||||
import FileClient
|
||||
import Foundation
|
||||
import GitClient
|
||||
import LoggingExtensions
|
||||
import ShellClient
|
||||
|
||||
public extension DependencyValues {
|
||||
|
||||
/// The cli-client that runs the command line tool commands.
|
||||
var cliClient: CliClient {
|
||||
get { self[CliClient.self] }
|
||||
set { self[CliClient.self] = newValue }
|
||||
@@ -22,7 +24,7 @@ public struct CliClient: Sendable {
|
||||
public var build: @Sendable (SharedOptions) async throws -> String
|
||||
|
||||
/// 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.
|
||||
public var generate: @Sendable (SharedOptions) async throws -> String
|
||||
@@ -34,59 +36,53 @@ public struct CliClient: Sendable {
|
||||
case major, minor, patch, preRelease
|
||||
}
|
||||
|
||||
// TODO: Need a quiet option, as default log level is warning, need a way to set it to ignore logs.
|
||||
public struct LoggingOptions: Equatable, Sendable {
|
||||
|
||||
let command: String
|
||||
let executableName: String
|
||||
let verbose: Int
|
||||
|
||||
public init(
|
||||
executableName: String = "bump-version",
|
||||
command: String,
|
||||
verbose: Int
|
||||
) {
|
||||
self.executableName = executableName
|
||||
self.command = command
|
||||
self.verbose = verbose
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents options that are used by all the commands.
|
||||
public struct SharedOptions: Equatable, Sendable {
|
||||
|
||||
/// Whether to allow pre-release suffixes.
|
||||
let allowPreReleaseTag: Bool
|
||||
|
||||
/// Flag on if we write to files or not.
|
||||
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 target: Configuration.Target?
|
||||
let branch: Configuration.Branch?
|
||||
let semvar: Configuration.SemVar?
|
||||
|
||||
/// Configuration that gets merged with the loaded (or default) configuration.
|
||||
let configurationToMerge: Configuration?
|
||||
|
||||
/// Path to the configuration file to load.
|
||||
let configurationFile: String?
|
||||
|
||||
/// Fail if a configuration file is not found.
|
||||
let requireConfigurationFile: Bool
|
||||
|
||||
public init(
|
||||
allowPreReleaseTag: Bool = true,
|
||||
dryRun: Bool = false,
|
||||
gitDirectory: String? = nil,
|
||||
projectDirectory: String? = nil,
|
||||
loggingOptions: LoggingOptions,
|
||||
target: Configuration.Target? = nil,
|
||||
branch: Configuration.Branch? = nil,
|
||||
semvar: Configuration.SemVar? = nil,
|
||||
configurationFile: String? = nil
|
||||
configurationToMerge: Configuration? = nil,
|
||||
configurationFile: String? = nil,
|
||||
requireConfigurationFile: Bool = false
|
||||
) {
|
||||
self.allowPreReleaseTag = allowPreReleaseTag
|
||||
self.dryRun = dryRun
|
||||
self.gitDirectory = gitDirectory
|
||||
self.projectDirectory = projectDirectory
|
||||
self.loggingOptions = loggingOptions
|
||||
self.target = target
|
||||
self.branch = branch
|
||||
self.semvar = semvar
|
||||
self.configurationFile = configurationFile
|
||||
self.configurationToMerge = configurationToMerge
|
||||
self.requireConfigurationFile = requireConfigurationFile
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CliClient: DependencyKey {
|
||||
|
||||
public static let testValue: CliClient = Self()
|
||||
|
||||
public static func live(environment: [String: String]) -> Self {
|
||||
@@ -95,7 +91,9 @@ extension CliClient: DependencyKey {
|
||||
bump: { try await $1.bump($0) },
|
||||
generate: { try await $0.generate() },
|
||||
parsedConfiguration: { options in
|
||||
try await options.withMergedConfiguration { $0 }
|
||||
try await options.loggingOptions.withLogger {
|
||||
try await options.withMergedConfiguration { $0 }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import ConfigurationClient
|
||||
|
||||
enum CliClientError: Error {
|
||||
case gitDirectoryNotFound
|
||||
case fileExists(path: String)
|
||||
case fileDoesNotExist(path: String)
|
||||
case failedToParseVersionFile
|
||||
case semVarNotFound
|
||||
case semVarNotFound(message: String)
|
||||
case strategyNotFound(configuration: Configuration)
|
||||
case preReleaseParsingError(String)
|
||||
case versionStringNotFound
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Manual Plugins
|
||||
|
||||
There are two plugins that are included that can be ran manually, if the build tool plugin does not fit
|
||||
your use case.
|
||||
There are two plugins that are included that can be ran manually, if the build tool plugin does not
|
||||
fit your use case.
|
||||
|
||||
## Generate Version
|
||||
|
||||
The `generate-version` plugin will create a `Version.swift` file in the given target. You can
|
||||
run it by running the following command.
|
||||
The `generate-version` plugin will create a `Version.swift` file in the given target. You can run it
|
||||
by running the following command.
|
||||
|
||||
```bash
|
||||
swift package --disable-sandbox \
|
||||
@@ -32,21 +32,22 @@ swift package --disable-sandbox \
|
||||
|
||||
## Options
|
||||
|
||||
Both manual versions also allow the following options to customize the operation, the
|
||||
options need to come after the plugin name.
|
||||
Both manual versions also allow the following options to customize the operation, the options need
|
||||
to come after the plugin name.
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| --dry-run | 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 |
|
||||
| --verbose | Increase the logging output |
|
||||
| Option | Description |
|
||||
| ---------- | --------------------------------------------------------------------- |
|
||||
| --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 |
|
||||
| --verbose | Increase the logging output |
|
||||
|
||||
### Example with options
|
||||
|
||||
```bash
|
||||
swift package \
|
||||
--allow-writing-to-package-directory \
|
||||
generate-version \
|
||||
--dry-run \
|
||||
--print \
|
||||
--verbose \
|
||||
<target>
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# CliClient
|
||||
# ``CliClient``
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
to the branch and git sha if a tag is not set for the current worktree state.
|
||||
build time or by manually running the plugin. The version is generally derived from git tags,
|
||||
however it can be configured to run custom commands.
|
||||
|
||||
## Articles
|
||||
|
||||
@@ -4,36 +4,37 @@ import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
import GitClient
|
||||
import LoggingExtensions
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.SharedOptions {
|
||||
extension CliClient.SharedOptions {
|
||||
|
||||
/// All cli-client calls should run through this, it set's up logging,
|
||||
/// loads configuration, and generates the current version based on the
|
||||
/// configuration.
|
||||
@discardableResult
|
||||
func run(
|
||||
_ operation: (CurrentVersionContainer) async throws -> Void
|
||||
_ operation: (VersionContainer) async throws -> Void
|
||||
) async rethrows -> String {
|
||||
try await loggingOptions.withLogger {
|
||||
// Load the default configuration, if it exists.
|
||||
try await withMergedConfiguration { configuration in
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
var configurationString = ""
|
||||
customDump(configuration, to: &configurationString)
|
||||
logger.trace("\nConfiguration: \(configurationString)")
|
||||
guard let strategy = configuration.strategy else {
|
||||
throw CliClientError.strategyNotFound(configuration: configuration)
|
||||
}
|
||||
|
||||
logger.dump(configuration, level: .trace) {
|
||||
"\nConfiguration: \($0)"
|
||||
}
|
||||
|
||||
// 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)")
|
||||
|
||||
// Perform the operation, which generates the new version and writes it.
|
||||
try await operation(
|
||||
configuration.currentVersion(
|
||||
targetUrl: targetUrl,
|
||||
gitDirectory: gitDirectory
|
||||
)
|
||||
.load(projectDirectory: projectDirectory, strategy: strategy, url: targetUrl)
|
||||
)
|
||||
|
||||
// Return the file path we wrote the version to.
|
||||
@@ -47,18 +48,24 @@ public extension CliClient.SharedOptions {
|
||||
func withMergedConfiguration<T>(
|
||||
operation: (Configuration) async throws -> T
|
||||
) async throws -> T {
|
||||
try await withConfiguration(path: configurationFile) { configuration in
|
||||
var configuration = configuration
|
||||
configuration = configuration.mergingTarget(target)
|
||||
@Dependency(\.configurationClient) var configurationClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
if configuration.strategy?.branch != nil, let branch {
|
||||
configuration = configuration.mergingStrategy(.branch(branch))
|
||||
} else if let semvar {
|
||||
configuration = configuration.mergingStrategy(.semvar(semvar))
|
||||
if configurationToMerge?.strategy?.branch != nil {
|
||||
logger.trace("Merging branch strategy.")
|
||||
// strategy = .branch(branch)
|
||||
} else if let semvar = configurationToMerge?.strategy?.semvar {
|
||||
logger.dump(semvar, level: .trace) {
|
||||
"Merging semvar strategy:\n\($0)"
|
||||
}
|
||||
|
||||
return try await operation(configuration)
|
||||
}
|
||||
|
||||
return try await configurationClient.withConfiguration(
|
||||
path: configurationFile,
|
||||
merging: configurationToMerge,
|
||||
strict: requireConfigurationFile,
|
||||
operation: operation
|
||||
)
|
||||
}
|
||||
|
||||
func write(_ string: String, to url: URL) async throws {
|
||||
@@ -68,48 +75,52 @@ public extension CliClient.SharedOptions {
|
||||
try await fileClient.write(string: string, to: url)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
logger.debug("\n\(string)\n")
|
||||
}
|
||||
}
|
||||
|
||||
func write(_ currentVersion: CurrentVersionContainer) async throws {
|
||||
func write(_ currentVersion: VersionContainer) async throws {
|
||||
@Dependency(\.logger) var logger
|
||||
logger.trace("Begin writing version.")
|
||||
|
||||
let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag)
|
||||
logger.debug("Version: \(version)")
|
||||
// let hasChanges: Bool
|
||||
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)")
|
||||
|
||||
try await write(template, to: currentVersion.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)
|
||||
|
||||
func string(allowPreReleaseTag: Bool) throws -> String {
|
||||
switch self {
|
||||
case let .string(string):
|
||||
return string
|
||||
case let .semvar(semvar, usesOptionalType: _):
|
||||
return semvar.versionString(withPreReleaseTag: allowPreReleaseTag)
|
||||
}
|
||||
}
|
||||
try await write(template, to: targetUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,31 +132,35 @@ extension CliClient.SharedOptions {
|
||||
}
|
||||
}
|
||||
|
||||
func bump(_ type: CliClient.BumpOption?) async throws -> String {
|
||||
guard let type else {
|
||||
return try await generate()
|
||||
}
|
||||
|
||||
func bump(_ type: CliClient.BumpOption) async throws -> String {
|
||||
return try await run { container in
|
||||
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
switch container.version {
|
||||
case .string: // When we did not parse a semvar, just write whatever we parsed for the current version.
|
||||
switch container {
|
||||
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.")
|
||||
try await write(container)
|
||||
|
||||
case let .semvar(semvar, usesOptionalType: usesOptionalType):
|
||||
logger.debug("Semvar prior to bumping: \(semvar)")
|
||||
let bumped = semvar.bump(type)
|
||||
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
|
||||
guard bumped != semvar else {
|
||||
logger.debug("No change, skipping.")
|
||||
return
|
||||
case let .semvar(semvar):
|
||||
|
||||
let version: SemVar?
|
||||
|
||||
switch semvar.precedence ?? .default {
|
||||
case .file:
|
||||
version = semvar.loadedVersion ?? semvar.strategyVersion
|
||||
case .strategy:
|
||||
version = semvar.strategyVersion ?? semvar.loadedVersion
|
||||
}
|
||||
logger.debug("Bumped version: \(version)")
|
||||
let template = usesOptionalType ? Template.optional(version) : Template.build(version)
|
||||
try await write(template, to: container.targetUrl)
|
||||
|
||||
// let version = semvar.loadedVersion ?? semvar.nextVersion
|
||||
guard let version else {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
|
||||
extension Configuration {
|
||||
|
||||
func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
|
||||
.init(
|
||||
target: otherTarget ?? target,
|
||||
strategy: strategy
|
||||
)
|
||||
}
|
||||
|
||||
func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
|
||||
.init(
|
||||
target: target,
|
||||
strategy: strategy?.merging(otherStrategy)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration.PreRelease {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
.init(
|
||||
prefix: other?.prefix ?? prefix,
|
||||
strategy: other?.strategy ?? strategy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration.Branch {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration.SemVar {
|
||||
// TODO: Merge strategy ??
|
||||
func merging(_ other: Self?) -> Self {
|
||||
.init(
|
||||
allowPreRelease: other?.allowPreRelease ?? allowPreRelease,
|
||||
preRelease: preRelease?.merging(other?.preRelease),
|
||||
requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
|
||||
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar,
|
||||
strategy: other?.strategy ?? strategy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration.VersionStrategy {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
guard let branch else {
|
||||
guard let semvar else { return self }
|
||||
return .semvar(semvar.merging(other?.semvar))
|
||||
}
|
||||
return .branch(branch.merging(other?.branch))
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
var output = self
|
||||
output = output.mergingTarget(other?.target)
|
||||
output = output.mergingStrategy(other?.strategy)
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func withConfiguration<T>(
|
||||
path: String?,
|
||||
_ operation: (Configuration) async throws -> T
|
||||
) async throws -> T {
|
||||
@Dependency(\.configurationClient) var configurationClient
|
||||
|
||||
let configuration = try await configurationClient.findAndLoad(
|
||||
path != nil ? URL(filePath: path!) : nil
|
||||
)
|
||||
|
||||
return try await operation(configuration)
|
||||
}
|
||||
57
Sources/CliClient/Internal/Configuration+targetUrl.swift
Normal file
57
Sources/CliClient/Internal/Configuration+targetUrl.swift
Normal 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
|
||||
}
|
||||
@@ -1,242 +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 {
|
||||
return try await .semvar(
|
||||
applyingPreRelease(semVar!, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
return try await .semvar(
|
||||
applyingPreRelease(semVar!, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
enum Constants {
|
||||
static let defaultFileName = "Version.swift"
|
||||
}
|
||||
@@ -5,9 +5,40 @@ import GitClient
|
||||
|
||||
@_spi(Internal)
|
||||
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(
|
||||
fileUrl: URL,
|
||||
gitDirectory: String?
|
||||
projectDirectory: String?
|
||||
) async throws -> (version: String, usesOptionalType: Bool) {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
@Dependency(\.logger) var logger
|
||||
@@ -38,15 +69,4 @@ public extension FileClient {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
90
Sources/CliClient/Internal/Semvar+nextVersion.swift
Normal file
90
Sources/CliClient/Internal/Semvar+nextVersion.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public struct Template: Sendable {
|
||||
return """
|
||||
// Do not set this variable, it is set during the build process.
|
||||
let VERSION: \(type.rawValue) = \(versionString)
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
172
Sources/CliClient/Internal/VersionContainer.swift
Normal file
172
Sources/CliClient/Internal/VersionContainer.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
76
Sources/ConfigurationClient/Configuration+merge.swift
Normal file
76
Sources/ConfigurationClient/Configuration+merge.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
import LoggingExtensions
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Configuration {
|
||||
|
||||
func merging(_ other: Self?) -> Self {
|
||||
mergingTarget(other?.target).mergingStrategy(other?.strategy)
|
||||
}
|
||||
|
||||
private func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
|
||||
.init(
|
||||
target: otherTarget ?? target,
|
||||
strategy: strategy
|
||||
)
|
||||
}
|
||||
|
||||
private func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
|
||||
.init(
|
||||
target: target,
|
||||
strategy: strategy?.merging(otherStrategy)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Configuration.PreRelease {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
return .init(
|
||||
prefix: other?.prefix ?? prefix,
|
||||
strategy: other?.strategy ?? strategy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Configuration.Branch {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Configuration.SemVar {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
@Dependency(\.logger) var logger
|
||||
logger.dump(other, level: .trace) { "Merging semvar:\n\($0)" }
|
||||
|
||||
return .init(
|
||||
allowPreRelease: other?.allowPreRelease ?? allowPreRelease,
|
||||
precedence: other?.precedence ?? precedence,
|
||||
preRelease: preRelease == nil ? other?.preRelease : preRelease!.merging(other?.preRelease),
|
||||
requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
|
||||
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar,
|
||||
strategy: other?.strategy ?? strategy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Configuration.VersionStrategy {
|
||||
func merging(_ other: Self?) -> Self {
|
||||
guard let other else { return self }
|
||||
|
||||
switch other {
|
||||
case .branch:
|
||||
guard let branch else { return other }
|
||||
return .branch(branch.merging(other.branch))
|
||||
case .semvar:
|
||||
guard let semvar else { return other }
|
||||
return .semvar(semvar.merging(other.semvar))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import CustomDump
|
||||
import Foundation
|
||||
|
||||
// TODO: Add pre-release strategy that just bumps an integer.
|
||||
|
||||
/// Represents configuration that can be set via a file, generally in the root of the
|
||||
/// project directory.
|
||||
///
|
||||
@@ -42,97 +44,6 @@ public struct Configuration: Codable, Equatable, Sendable {
|
||||
|
||||
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.
|
||||
///
|
||||
/// This can either be a path to a version file or a module used to
|
||||
@@ -234,6 +145,7 @@ public extension Configuration {
|
||||
|
||||
case semvar(
|
||||
allowPreRelease: Bool? = nil,
|
||||
precedence: SemVar.Precedence? = nil,
|
||||
preRelease: PreRelease? = nil,
|
||||
requireExistingFile: Bool? = nil,
|
||||
requireExistingSemVar: Bool? = nil,
|
||||
@@ -248,10 +160,16 @@ public extension Configuration {
|
||||
}
|
||||
|
||||
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 }
|
||||
return .init(
|
||||
allowPreRelease: allowPreRelease,
|
||||
precedence: precedence,
|
||||
preRelease: preRelease,
|
||||
requireExistingFile: requireExistingFile ?? false,
|
||||
requireExistingSemVar: requireExistingSemVar ?? false,
|
||||
@@ -266,6 +184,7 @@ public extension Configuration {
|
||||
public static func semvar(_ value: SemVar) -> Self {
|
||||
.semvar(
|
||||
allowPreRelease: value.allowPreRelease,
|
||||
precedence: value.precedence,
|
||||
preRelease: value.preRelease,
|
||||
requireExistingFile: value.requireExistingFile,
|
||||
requireExistingSemVar: value.requireExistingSemVar,
|
||||
@@ -283,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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import DependenciesMacros
|
||||
import FileClient
|
||||
import Foundation
|
||||
|
||||
// TODO: Add a method to get a semvar / handle a version strategy's ??
|
||||
|
||||
public extension DependencyValues {
|
||||
|
||||
/// Perform operations with configuration files.
|
||||
@@ -16,6 +18,15 @@ public extension DependencyValues {
|
||||
@DependencyClient
|
||||
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.
|
||||
public var defaultFileName: @Sendable () -> String = { "test.json" }
|
||||
|
||||
/// Find a configuration file in the given directory or in current working directory.
|
||||
public var find: @Sendable (URL?) async throws -> URL?
|
||||
|
||||
@@ -26,11 +37,47 @@ public struct ConfigurationClient: Sendable {
|
||||
public var write: @Sendable (Configuration, URL) async throws -> Void
|
||||
|
||||
/// 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 {
|
||||
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.
|
||||
/// Optionally merges other configuration, then perform an operation with the loaded configuration.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: Optional file path of the configuration to load.
|
||||
/// - other: Optional configuration to merge with the loaded configuration.
|
||||
/// - operation: The operation to perform with the loaded configuration.
|
||||
@discardableResult
|
||||
public func withConfiguration<T>(
|
||||
path: String?,
|
||||
merging other: Configuration? = nil,
|
||||
strict: Bool = true,
|
||||
operation: (Configuration) async throws -> T
|
||||
) async throws -> T {
|
||||
let configuration = try await findAndLoad(
|
||||
path != nil ? URL(filePath: path!) : nil,
|
||||
strict: strict
|
||||
)
|
||||
return try await operation(configuration.merging(other))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +86,7 @@ extension ConfigurationClient: DependencyKey {
|
||||
|
||||
public static var liveValue: ConfigurationClient {
|
||||
.init(
|
||||
defaultFileName: { "\(Constants.defaultFileNameWithoutExtension).json" },
|
||||
find: { try await findConfiguration($0) },
|
||||
load: { try await loadConfiguration($0) },
|
||||
write: { try await writeConfiguration($0, to: $1) }
|
||||
@@ -49,15 +97,13 @@ extension ConfigurationClient: DependencyKey {
|
||||
private func findConfiguration(_ url: URL?) async throws -> URL? {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
let defaultFileName = ConfigurationClient.Constants.defaultFileNameWithoutExtension
|
||||
|
||||
var url: URL! = url
|
||||
if url == nil {
|
||||
url = try await URL(filePath: fileClient.currentDirectory())
|
||||
}
|
||||
|
||||
if try await fileClient.isDirectory(url.cleanFilePath) {
|
||||
url = url.appending(path: "\(defaultFileName).json")
|
||||
url = url.appending(path: ConfigurationClient.Constants.defaultFileName)
|
||||
}
|
||||
|
||||
if fileClient.fileExists(url) {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import Foundation
|
||||
|
||||
extension ConfigurationClient {
|
||||
enum Constants {
|
||||
static let defaultFileNameWithoutExtension = ".bump-version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,27 +5,19 @@ import LoggingFormatAndPipe
|
||||
import Rainbow
|
||||
import ShellClient
|
||||
|
||||
// MARK: Custom colors.
|
||||
|
||||
extension String {
|
||||
var orange: Self {
|
||||
bit24(255, 165, 0)
|
||||
}
|
||||
|
||||
var magena: Self {
|
||||
// bit24(186, 85, 211)
|
||||
var magenta: Self {
|
||||
bit24(238, 130, 238)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Logger.Level {
|
||||
|
||||
init(verbose: Int) {
|
||||
switch verbose {
|
||||
case 1: self = .debug
|
||||
case 2...: self = .trace
|
||||
default: self = .warning
|
||||
}
|
||||
}
|
||||
extension Logger.Level {
|
||||
|
||||
var coloredString: String {
|
||||
switch self {
|
||||
@@ -45,7 +37,7 @@ public extension Logger.Level {
|
||||
}
|
||||
}
|
||||
|
||||
struct LevelFormatter: LoggingFormatAndPipe.Formatter {
|
||||
private struct LevelFormatter: LoggingFormatAndPipe.Formatter {
|
||||
|
||||
let basic: BasicFormatter
|
||||
|
||||
@@ -138,11 +130,11 @@ struct LevelFormatter: LoggingFormatAndPipe.Formatter {
|
||||
|
||||
}
|
||||
|
||||
extension CliClient.LoggingOptions {
|
||||
extension LoggingOptions {
|
||||
|
||||
func makeLogger() -> Logger {
|
||||
let formatters: [LogComponent] = [
|
||||
.text(executableName.magena),
|
||||
.text(executableName.magenta),
|
||||
.text(command.blue),
|
||||
.level,
|
||||
.group([
|
||||
@@ -161,12 +153,4 @@ extension CliClient.LoggingOptions {
|
||||
}
|
||||
}
|
||||
|
||||
func withLogger<T>(_ operation: () async throws -> T) async rethrows -> T {
|
||||
try await withDependencies {
|
||||
$0.logger = makeLogger()
|
||||
$0.logger.logLevel = .init(verbose: verbose)
|
||||
} operation: {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Sources/LoggingExtensions/LogLevel+verbose.swift
Normal file
13
Sources/LoggingExtensions/LogLevel+verbose.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
import Logging
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Logger.Level {
|
||||
|
||||
init(verbose: Int) {
|
||||
switch verbose {
|
||||
case 1: self = .debug
|
||||
case 2...: self = .trace
|
||||
default: self = .info
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Sources/LoggingExtensions/LoggingOptions.swift
Normal file
42
Sources/LoggingExtensions/LoggingOptions.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
import CustomDump
|
||||
import Dependencies
|
||||
import Logging
|
||||
import ShellClient
|
||||
|
||||
public struct LoggingOptions: Equatable, Sendable {
|
||||
|
||||
let command: String
|
||||
let executableName: String
|
||||
let verbose: Int
|
||||
|
||||
public init(
|
||||
executableName: String = "bump-version",
|
||||
command: String,
|
||||
verbose: Int
|
||||
) {
|
||||
self.executableName = executableName
|
||||
self.command = command
|
||||
self.verbose = verbose
|
||||
}
|
||||
|
||||
public func withLogger<T>(_ operation: () async throws -> T) async rethrows -> T {
|
||||
try await withDependencies {
|
||||
$0.logger = makeLogger()
|
||||
$0.logger.logLevel = .init(verbose: verbose)
|
||||
} operation: {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))")
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ShellClient
|
||||
|
||||
struct GenerateCommand: AsyncParsableCommand {
|
||||
static let commandName = "generate"
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: Self.commandName,
|
||||
abstract: "Generates a version file in a command line tool that can be set via the git tag or git sha.",
|
||||
discussion: "This command can be interacted with directly, outside of the plugin usage context."
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
|
||||
func run() async throws {
|
||||
try await globals.run(\.generate, command: Self.commandName)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Application
|
||||
|
||||
## Articles
|
||||
|
||||
- <doc:Installation>
|
||||
@@ -1,3 +0,0 @@
|
||||
# Installation
|
||||
|
||||
You can install the command-line application by...
|
||||
@@ -15,10 +15,13 @@ struct CliClientTests {
|
||||
arguments: TestArguments.testCases
|
||||
)
|
||||
func testBuild(target: String) async throws {
|
||||
let template = Template.build("1.0.0")
|
||||
try await run {
|
||||
$0.fileClient.fileExists = { _ in true }
|
||||
$0.fileClient.read = { @Sendable _ in template }
|
||||
} operation: {
|
||||
@Dependency(\.cliClient) var client
|
||||
|
||||
let output = try await client.build(.testOptions(
|
||||
target: target,
|
||||
versionStrategy: .semvar(requireExistingFile: false)
|
||||
@@ -138,11 +141,12 @@ extension CliClient.SharedOptions {
|
||||
) -> Self {
|
||||
return .init(
|
||||
dryRun: dryRun,
|
||||
gitDirectory: gitDirectory,
|
||||
projectDirectory: gitDirectory,
|
||||
loggingOptions: .init(command: "test", verbose: 2),
|
||||
target: .init(module: .init(target)),
|
||||
branch: versionStrategy.branch,
|
||||
semvar: versionStrategy.semvar
|
||||
configurationToMerge: .init(
|
||||
target: .init(module: .init(target)),
|
||||
strategy: versionStrategy
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import ConfigurationClient
|
||||
@_spi(Internal) import ConfigurationClient
|
||||
import CustomDump
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Testing
|
||||
@@ -74,6 +75,64 @@ struct ConfigurationClientTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func mergingBranch() {
|
||||
let branch = Configuration.Branch(includeCommitSha: false)
|
||||
let branch2 = Configuration.Branch(includeCommitSha: true)
|
||||
let merged = branch.merging(branch2)
|
||||
#expect(merged == branch2)
|
||||
|
||||
let merged2 = branch.merging(nil)
|
||||
#expect(merged2 == branch)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mergingSemvar() {
|
||||
let strategy1 = Configuration.VersionStrategy.semvar(.init())
|
||||
let other = Configuration.VersionStrategy.semvar(.init(
|
||||
allowPreRelease: true,
|
||||
preRelease: .init(prefix: "foo", strategy: .gitTag),
|
||||
requireExistingFile: false,
|
||||
requireExistingSemVar: false,
|
||||
strategy: .gitTag()
|
||||
))
|
||||
let merged = strategy1.merging(other)
|
||||
#expect(merged == other)
|
||||
|
||||
let otherMerged = other.merging(strategy1)
|
||||
#expect(otherMerged == other)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mergingTarget() {
|
||||
let config1 = Configuration(target: .init(path: "foo"))
|
||||
let config2 = Configuration(target: .init(module: .init("bar")))
|
||||
|
||||
let merged = config1.merging(config2)
|
||||
#expect(merged.target! == .init(module: .init("bar")))
|
||||
|
||||
let merged2 = merged.merging(config1)
|
||||
#expect(merged2.target! == .init(path: "foo"))
|
||||
|
||||
let merged3 = merged2.merging(nil)
|
||||
#expect(merged3 == merged2)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mergingVersionStrategy() {
|
||||
let version = Configuration.VersionStrategy.semvar(.init())
|
||||
let version2 = Configuration.VersionStrategy.branch(.init())
|
||||
|
||||
let merged = version.merging(version2)
|
||||
#expect(merged == version2)
|
||||
|
||||
let merged2 = merged.merging(.branch(includeCommitSha: false))
|
||||
#expect(merged2.branch!.includeCommitSha == false)
|
||||
|
||||
let merged3 = version2.merging(version)
|
||||
#expect(merged3 == version)
|
||||
}
|
||||
|
||||
func run(
|
||||
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
||||
operation: () async throws -> Void
|
||||
|
||||
36
justfile
36
justfile
@@ -5,7 +5,8 @@ docker_tag := "test"
|
||||
tap_url := "https://git.housh.dev/michael/homebrew-formula"
|
||||
tap := "michael/formula"
|
||||
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]
|
||||
default:
|
||||
@@ -26,7 +27,8 @@ build configuration="debug":
|
||||
--configuration {{configuration}} \
|
||||
--product {{product}}
|
||||
|
||||
alias b := build
|
||||
# Bump our version of the command-line tool.
|
||||
bump-version *ARGS: (run "bump" ARGS)
|
||||
|
||||
# Build a docker image.
|
||||
build-docker configuration="debug":
|
||||
@@ -43,7 +45,9 @@ clean:
|
||||
# Clean and build.
|
||||
clean-build configuration="debug": clean (build configuration)
|
||||
|
||||
alias cb := clean-build
|
||||
# Remove bottles
|
||||
remove-bottles:
|
||||
rm -rf *.gz
|
||||
|
||||
# Test locally.
|
||||
test *ARGS:
|
||||
@@ -53,14 +57,36 @@ test *ARGS:
|
||||
test-docker: build-docker
|
||||
@docker run --rm {{docker_image}}:{{docker_tag}}
|
||||
|
||||
# Run tests in docker without building a new image.
|
||||
test-docker-without-building:
|
||||
@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-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) && \
|
||||
stripped="${sha% *}" && \
|
||||
echo "$stripped" | pbcopy && \
|
||||
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}}
|
||||
|
||||
Reference in New Issue
Block a user