14 Commits
0.2.0 ... dev

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

View File

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

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

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

View File

@@ -14,12 +14,14 @@ jobs:
config: ['debug', 'release']
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
View File

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

View File

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

View File

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

View File

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

View File

@@ -8,15 +8,14 @@ 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"),
@@ -29,7 +28,7 @@ let package = Package(
],
targets: [
.executableTarget(
name: "bump-version",
name: "BumpVersion",
dependencies: [
"CliClient",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
@@ -56,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")
@@ -88,6 +88,7 @@ let package = Package(
.target(
name: "LoggingExtensions",
dependencies: [
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client")
]
@@ -97,37 +98,22 @@ let package = Package(
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"
]
)
]

View File

@@ -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),

View File

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

View File

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

View File

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

26
Release.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,13 +13,13 @@ struct GlobalOptions: ParsableArguments {
var configOptions: ConfigurationOptions
@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
@@ -41,6 +41,7 @@ struct GlobalOptions: ParsableArguments {
}
struct ConfigurationOptions: ParsableArguments {
@Option(
name: [.customShort("f"), .long],
help: "Specify the path to a configuration file. (default: .bump-version.json)",
@@ -61,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 {
@@ -126,7 +135,6 @@ struct PreReleaseOptions: ParsableArguments {
}
// TODO: Add custom command strategy.
struct SemVarOptions: ParsableArguments {
@Flag(
@@ -165,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)
}
}

View File

@@ -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: semvarOptions.gitTag ? nil : .init(includeCommitSha: commitSha),
semvar: semvarOptions(extraOptions: extraOptions),
configurationFile: configurationFile
configurationToMerge: configurationToMerge(extraOptions: extraOptions),
configurationFile: configurationFile,
requireConfigurationFile: requireConfiguration
)
}
}

View File

@@ -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"

View File

@@ -9,6 +9,7 @@ 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 }
@@ -23,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
@@ -35,41 +36,53 @@ public struct CliClient: Sendable {
case major, minor, patch, preRelease
}
/// 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 {

View File

@@ -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
}

View File

@@ -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>
```

View File

@@ -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

View File

@@ -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.
@@ -50,27 +51,19 @@ public extension CliClient.SharedOptions {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
var strategy: Configuration.VersionStrategy?
if let branch {
if configurationToMerge?.strategy?.branch != nil {
logger.trace("Merging branch strategy.")
strategy = .branch(branch)
} else if let semvar {
logger.trace("Merging semvar strategy.")
var semvarString = ""
customDump(semvar, to: &semvarString)
logger.trace("\(semvarString)")
strategy = .semvar(semvar)
// strategy = .branch(branch)
} else if let semvar = configurationToMerge?.strategy?.semvar {
logger.dump(semvar, level: .trace) {
"Merging semvar strategy:\n\($0)"
}
}
let configuration = Configuration(
target: target,
strategy: strategy
)
return try await configurationClient.withConfiguration(
path: configurationFile,
merging: configuration,
merging: configurationToMerge,
strict: requireConfigurationFile,
operation: operation
)
}
@@ -82,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.info("\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, hasChanges: Bool)
func string(allowPreReleaseTag: Bool) throws -> String {
switch self {
case let .string(string):
return string
case let .semvar(semvar, usesOptionalType: _, hasChanges: _):
return semvar.versionString(withPreReleaseTag: allowPreReleaseTag)
}
}
try await write(template, to: targetUrl)
}
}
@@ -135,33 +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, hasChanges: hasChanges):
logger.debug("Semvar prior to bumping: \(semvar)")
let bumped = semvar.bump(type)
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
case let .semvar(semvar):
guard bumped != semvar || hasChanges else {
logger.debug("No change, skipping.")
return
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)))
}
}
}
@@ -172,3 +171,15 @@ extension CliClient.SharedOptions {
}
}
}
private extension CurrentVersionContainer where Version == SemVar {
func withUpdateNextVersion(_ next: SemVar) -> Self {
.init(
targetUrl: targetUrl,
usesOptionalType: usesOptionalType,
loadedVersion: loadedVersion,
precedence: .strategy, // make sure to use the next version, since it was specified, as this is called from `bump`.
strategyVersion: next
)
}
}

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,40 @@ import GitClient
@_spi(Internal)
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ import DependenciesMacros
import FileClient
import 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,12 @@ 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" }
@@ -29,11 +37,26 @@ 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.
@@ -47,10 +70,12 @@ public struct ConfigurationClient: Sendable {
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
path != nil ? URL(filePath: path!) : nil,
strict: strict
)
return try await operation(configuration.merging(other))
}
@@ -72,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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,10 +15,13 @@ struct CliClientTests {
arguments: TestArguments.testCases
)
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
)
)
}
}

View File

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

View File

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

1
gum Normal file
View File

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

View File

@@ -5,7 +5,8 @@ docker_tag := "test"
tap_url := "https://git.housh.dev/michael/homebrew-formula"
tap := "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}}